Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/ghostJournal.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Disk writer for ghost entries.
//
// Ghost entries are minted, updated, superseded, and orphaned in the
// renderer (via agent-transcript-parser + `src/renderer/src/workspace/ghosts.ts`).
// renderer (via agent-transcript-parser + `src/renderer/src/session-runtime/ghosts.ts`).
// This file handles the durable side: append each produced ghost to
// an Agent Code-owned JSONL file so the live view survives a crash, a
// window reload, or an Electron restart.
Expand Down
2 changes: 1 addition & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const lspManager = new LspManager()
// Ghost log writer — one queue per session. Writes are batched at
// 100 ms and persisted under `<userData>/ghost-logs/<sessionId>.ghost.jsonl`.
// See `./ghostJournal.ts` for the full rationale; see
// `src/renderer/src/workspace/ghosts.ts` for the renderer side.
// `src/renderer/src/session-runtime/ghosts.ts` for the renderer side.
const ghostJournals = new GhostJournalRegistry()
// Session recorder — one folder per recording under session-recordings/.
// Constructed and installed as the outbound-IPC observer whenever the
Expand Down
2 changes: 1 addition & 1 deletion src/remote-client/src/ui/SessionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from

import { Feed } from '@renderer/features/feed/ui/Feed'
import { SessionFeedProvider } from '@renderer/features/sessionFeed/SessionFeedContext'
import { useLedgerFeedItems } from '@renderer/rendering/view/useLedgerFeedItems'
import { useLedgerFeedItems } from '@renderer/features/feed/ledger/useLedgerFeedItems'
import type { RuntimeRenderInput } from '@renderer/session-runtime/state'
import type { GhostEntry } from 'agent-transcript-parser/ghost'
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { createSessionLedger } from '@renderer/rendering/model/ledger'
import {
ledgerFeedContextFromRuntime,
ledgerToFeedItems,
} from '@renderer/rendering/view/ledgerFeedItems'
} from '@renderer/features/feed/ledger/ledgerFeedItems'

// ---------------------------------------------------------------------------
// View bridge: the ledger must drive Feed's EXISTING row components. These
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { createSessionLedger } from '@renderer/rendering/model/ledger'
import {
ledgerFeedContextFromRuntime,
ledgerToFeedItems,
} from '@renderer/rendering/view/ledgerFeedItems'
} from '@renderer/features/feed/ledger/ledgerFeedItems'

// ---------------------------------------------------------------------------
// The producer hook: runtime → adapter → ledger → view bridge →
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/features/feed/lib/streamingWriteInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// While a Write tool_use block streams, the renderer only has
// `block.inputJson` — the raw concatenation of every
// `input_json_delta.partial_json` fragment the model has emitted so
// far (see workspace/semantic/foldEvent.ts:421). That string is
// far (see session-runtime/semantic/foldEvent.ts:421). That string is
// almost never valid JSON mid-stream: the `content` string is
// unterminated, the closing brace is missing, and a fragment can
// end anywhere — mid-key, mid-value, even mid-escape. So the
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/features/feed/ui/Feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,12 @@ type Props = {
// live owner.
//
// That duplicate class is now prevented at its source. The ghost
// reducer (`reconcileUpstream` in src/renderer/src/workspace/ghosts.ts)
// reducer (`reconcileUpstream` in src/renderer/src/session-runtime/ghosts.ts)
// supersedes Codex text ghosts by rollout response id once the
// rollout mapper stamps `codexTurnId` on committed entries
// (src/renderer/src/workspace/workspaceStore.ts::codexTurnIdFromRollout
// + stampCodexTurnId). The live view and the merged feed are split
// by turn ownership (src/renderer/src/workspace/mergedEntries.ts), so
// by turn ownership (src/renderer/src/session-runtime/mergedEntries.ts), so
// there is no longer any path by which the same assistant text can
// reach both surfaces at once.
//
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/rendering/observations/ghosts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { GhostLedgerCandidate } from '@renderer/rendering/model/ledger'
// ---------------------------------------------------------------------------
// Ghost collector: runtime ghost map → ledger ghost plane.
//
// The runtime's ghost reducer (workspace/ghosts.ts) already does the
// The runtime's ghost reducer (session-runtime/ghosts.ts) already does the
// LIFECYCLE work — mint, reconcile-supersede, orphan, gc. This collector
// does NONE of that; it only translates each ghost record's current state
// into (paintable candidate + predicate facts) pairs so the five-rule gate
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/rendering/observations/semantic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export type SemanticBlockLike = {
* flow through the adapter unchanged (non-tool blocks pass by reference,
* the tool-block `{...b}` spread preserves them) — they were just untyped,
* so the collector could not read them and a thinking-only turn vanished.
* Source of truth for the field names: workspace/workspaceState.ts
* Source of truth for the field names: session-runtime/state.ts
* SemanticLiveBlock. */
thinking?: string
reasoningSummary?: string
Expand Down Expand Up @@ -189,7 +189,7 @@ function collectTurn(
// candidates and disappeared from the feed. Keep the block when ANY
// content field has non-empty trimmed text; reject as empty-thinking only
// when all four are hollow (e.g. encrypted, genuinely unrenderable).
// Mirrors the ghost builder's content probe (workspace/ghosts.ts:
// Mirrors the ghost builder's content probe (session-runtime/ghosts.ts:
// thinking || reasoningSummary || reasoningText) so live and ghost agree
// on what counts as a paintable reasoning block.
if (kind === 'thinking') {
Expand Down
15 changes: 14 additions & 1 deletion src/renderer/src/rendering/recordingCorpus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,20 @@ import { createSessionLedger } from '@renderer/rendering/model/ledger'
import {
parseRecording,
replayRecording,
type ReplayItemsProjection,
} from '@renderer/rendering/replay/recordedSession'
import {
ledgerFeedContextFromRuntime,
ledgerToFeedItems,
} from '@renderer/features/feed/ledger/ledgerFeedItems'

// The REAL feed-owned view bridge, injected through the harness's
// projectItems seam (#493 PR-3): the harness itself may not import
// features/, but this test's whole point is exercising the full stack —
// including the #239 dropped-candidate accounting — so it supplies the
// production bridge.
const projectItems: ReplayItemsProjection = (ledger, view, provider) =>
ledgerToFeedItems(ledger, ledgerFeedContextFromRuntime(view, provider))
import type { GhostLike } from '@renderer/rendering/observations/ghosts'
import {
diffShadowUnits,
Expand Down Expand Up @@ -128,7 +141,7 @@ describe.skipIf(files.length === 0)(`recording corpus (${files.length} recording
// structurally the harness's RecordedLine; cast across the local type.
events: fixture.events as unknown as Parameters<typeof parseRecording>[0]['events'],
})
const replayed = replayRecording(parsed)
const replayed = replayRecording(parsed, { projectItems })
const finalLedger =
replayed.ticks.length > 0
? replayed.ticks[replayed.ticks.length - 1].ledger
Expand Down
15 changes: 14 additions & 1 deletion src/renderer/src/rendering/replay/invariants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,22 @@ import {
replayRecording,
type RecordingHeader,
type RecordedLine,
type ReplayItemsProjection,
type ReplayResult,
type ReplayTick,
} from '@renderer/rendering/replay/recordedSession'
import {
ledgerFeedContextFromRuntime,
ledgerToFeedItems,
} from '@renderer/features/feed/ledger/ledgerFeedItems'

// The REAL feed-owned view bridge, injected through the harness's
// projectItems seam (#493 PR-3): the harness itself may not import
// features/, but this test's whole point is exercising the full stack —
// including the #239 dropped-candidate accounting — so it supplies the
// production bridge.
const projectItems: ReplayItemsProjection = (ledger, view, provider) =>
ledgerToFeedItems(ledger, ledgerFeedContextFromRuntime(view, provider))
import { assertInvariants } from '@renderer/rendering/replay/invariants'

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -172,7 +185,7 @@ describe('invariant replay — green over a realistic recording', () => {
it('finds no violations across a prompt → tool → completion → exit stream', () => {
vi.useFakeTimers()
const recording = parseRecording({ header: HEADER, events: realisticRecording() })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w) })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w), projectItems })

// Sanity: the stream actually exercised the transitions we care about —
// otherwise "no violations" is vacuous.
Expand Down
8 changes: 4 additions & 4 deletions src/renderer/src/rendering/replay/reconstructSlices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ import type { RuntimeLedgerSlices } from '@renderer/rendering/adapter/collectLed
// THE SAME PURE LEAF REDUCERS the fold calls, in the same composition.
// This is NOT a second fold reimplementation — every state transition that
// matters to the ledger is delegated to the production reducer:
// - semantic turn structure → foldSemanticEvent (workspace/semantic/foldEvent.ts)
// - stream phase → reduceStreamPhase (workspace/semantic/streamPhaseMachine.ts)
// - ghost bridge from semantic → ghostsFromSemanticTurn (workspace/ghosts.ts)
// - ghost→committed handoff → reconcileUpstream (workspace/ghosts.ts)
// - semantic turn structure → foldSemanticEvent (session-runtime/semantic/foldEvent.ts)
// - stream phase → reduceStreamPhase (session-runtime/semantic/streamPhaseMachine.ts)
// - ghost bridge from semantic → ghostsFromSemanticTurn (session-runtime/ghosts.ts)
// - ghost→committed handoff → reconcileUpstream (session-runtime/ghosts.ts)
// - raw JSONL line → Entry → the provider's real TranscriptEntryMapper
// Only the thin ORCHESTRATION glue (dedup set, entries append, the
// lastJsonlEntryAt cursor, exit teardown) is re-expressed here, mirrored
Expand Down
21 changes: 17 additions & 4 deletions src/renderer/src/rendering/replay/recordedSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,20 @@ import {
replayRecording,
type RecordingHeader,
type RecordedLine,
type ReplayItemsProjection,
} from '@renderer/rendering/replay/recordedSession'
import {
ledgerFeedContextFromRuntime,
ledgerToFeedItems,
} from '@renderer/features/feed/ledger/ledgerFeedItems'

// The REAL feed-owned view bridge, injected through the harness's
// projectItems seam (#493 PR-3): the harness itself may not import
// features/, but this test's whole point is exercising the full stack —
// including the #239 dropped-candidate accounting — so it supplies the
// production bridge.
const projectItems: ReplayItemsProjection = (ledger, view, provider) =>
ledgerToFeedItems(ledger, ledgerFeedContextFromRuntime(view, provider))

// ---------------------------------------------------------------------------
// Slice 4 — replay-harness round trip. Hand-build a small recording (the 9
Expand Down Expand Up @@ -135,7 +148,7 @@ describe('replay harness — recorded-session round trip', () => {
it('replays a hand-built recording and produces the expected rows tick by tick', () => {
vi.useFakeTimers()
const recording = parseRecording({ header: HEADER, events: buildRecording() })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w) })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w), projectItems })

const rowIds = replay.ticks.map(t => t.rows.map(r => r.candidate.id))

Expand Down Expand Up @@ -179,7 +192,7 @@ describe('replay harness — recorded-session round trip', () => {
it('reuses the SAME ledger object on ticks whose inputs did not change (D11)', () => {
vi.useFakeTimers()
const recording = parseRecording({ header: HEADER, events: buildRecording() })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w) })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w), projectItems })

// Tick 6 is the screen frame that follows the tool block_started (tick 5).
// A screen event moves no ledger slice, so the adapter's plane caches all
Expand Down Expand Up @@ -209,7 +222,7 @@ describe('replay harness — recorded-session round trip', () => {
// The torn line is dropped; every complete line survives.
expect(recording.lines).toHaveLength(buildRecording().length)

const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w) })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w), projectItems })
expect(replay.ticks.at(-1)?.rows.map(r => r.candidate.id)).toEqual(['entry:u1', 'entry:a1'])
})

Expand All @@ -223,7 +236,7 @@ describe('replay harness — recorded-session round trip', () => {
{ t: 90, wall: T + 90, ch: '__truncated', reason: 'size-cap', bytes: 1234 },
]
const recording = parseRecording({ header: HEADER, events: withNotes })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w) })
const replay = replayRecording(recording, { onBeforeFold: w => vi.setSystemTime(w), projectItems })

// Notes/tombstones are preserved in the parsed line list (triage tooling
// reads them) but produce NO pipeline tick — the tick count equals the
Expand Down
50 changes: 33 additions & 17 deletions src/renderer/src/rendering/replay/recordedSession.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import type { AgentProviderKind } from '@shared/types/providerKind'
import { isAgentProviderKind } from '@shared/types/providerKind'
import type { FeedRenderItem } from '@renderer/features/feed/model/renderModel'
import type { RuntimeRenderInput } from '@renderer/session-runtime/state'
import {
createLedgerInputAdapter,
type RuntimeLedgerSlices,
} from '@renderer/rendering/adapter/collectLedgerInput'
import { createSessionLedger } from '@renderer/rendering/model/ledger'
import type { RenderLedger, OwnershipDecision } from '@renderer/rendering/model/types'
import {
ledgerFeedContextFromRuntime,
ledgerToFeedItems,
} from '@renderer/rendering/view/ledgerFeedItems'
import {
applyFeedEvent,
createReplayFoldState,
Expand All @@ -34,9 +29,10 @@ import {
// │ createSessionLedger() ← constructed ONCE, holds last-call cache
// ▼
// RenderLedger { rows, decisions }
// │ ledgerToFeedItems(...) ← the view bridge (exercises #239 drops)
// ▼
// FeedRenderItem[]
// │ options.projectItems ← INJECTED view bridge (#493 PR-3: the
// ▼ bridge lives in features/feed, which
// items (opaque to this harness) this decide-layer harness must not
// import; tests inject the real one)
//
// The adapter and ledger are built ONCE and fed each tick (rec-research-seam.md
// §(c)): that is what makes the D11 identity caches real. Building them per tick
Expand Down Expand Up @@ -97,10 +93,15 @@ export type ReplayTick = {
ledger: RenderLedger
/** Convenience alias for `ledger.rows` (what paints). */
rows: RenderLedger['rows']
/** The view-bridge output — exercises the full stack through FeedRenderItem. */
feedItems: FeedRenderItem[]
/** Candidate ids the view bridge could not resolve (the #239 "present but
* invisible" class). Non-empty is itself a smell the invariants can flag. */
/** The injected view bridge's output, opaque to this harness (#493 PR-3:
* the RenderRow→FeedRenderItem bridge is feed-owned; rendering/ must not
* import it). Tests that assert on painted items inject the real bridge
* and narrow the type themselves. */
feedItems: unknown[]
/** Candidate ids the injected bridge could not resolve (the #239 "present
* but invisible" class). Non-empty is itself a smell the invariants flag.
* Always real: `projectItems` is a required option, so there is no
* un-projected run for this to be vacuously empty on. */
dropped: string[]
/** Rejections the ADAPTER made at collection time (hidden meta rows,
* compaction-synthesis kills, duplicate-turn drops) — the half of the
Expand All @@ -116,6 +117,18 @@ export type ReplayResult = {
ticks: ReplayTick[]
}

/** The view-bridge seam. The harness runs slices → adapter → ledger and stops
* at RenderLedger (the decide layer's own output); projecting rows into feed
* items is the RENDER layer's job, so the projection is injected by callers
* (tests import the real `ledgerToFeedItems`+`ledgerFeedContextFromRuntime`
* from features/feed/ledger and adapt them here). This is what lets the
* replay harness live inside rendering/ without importing features/. */
export type ReplayItemsProjection = (
ledger: RenderLedger,
runtimeView: RuntimeRenderInput,
provider: AgentProviderKind,
) => { items: unknown[]; dropped: string[] }

export type ReplayOptions = {
/**
* Clock-injection seam (plan §6 "inject wall as the fold clock"). Called with
Expand All @@ -130,6 +143,12 @@ export type ReplayOptions = {
* entry.timestamp), only the receipt-time stamps drift.
*/
onBeforeFold?: (wall: number) => void
/** See ReplayItemsProjection. REQUIRED, not optional (4-agent review of
* #503): an omitted projection would be indistinguishable from "projected
* with zero drops", silently vacating the #239 dropped-candidate invariant
* for that run. Making the field mandatory turns the forgotten-injection
* mistake into a compile error instead of a green-but-hollow test. */
projectItems: ReplayItemsProjection
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -229,7 +248,7 @@ function runtimeViewFor(
*/
export function replayRecording(
recording: ParsedRecording,
options: ReplayOptions = {},
options: ReplayOptions,
): ReplayResult {
const provider: AgentProviderKind = isAgentProviderKind(recording.header.provider)
? recording.header.provider
Expand All @@ -255,10 +274,7 @@ export function replayRecording(
const slices = slicesFromState(state)
const bundle = adapter(slices)
const rl = ledger(bundle.input)
const { items, dropped } = ledgerToFeedItems(
rl,
ledgerFeedContextFromRuntime(runtimeViewFor(state), provider),
)
const { items, dropped } = options.projectItems(rl, runtimeViewFor(state), provider)

ticks.push({
index,
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/workspace/tile-tree/TileLeaf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { useClaudeImagePaste } from '@renderer/workspace/tile-tree/TileLeaf/useC
import { registerComposerEnterTarget } from '@renderer/workspace/tile-tree/TileLeaf/composerEnterRegistry'
import { recordHtmlTraceSnapshot } from '@renderer/features/debug/renderTrace'
import { isSessionExited } from '@renderer/workspace/providerSessionIdentity'
import { useLedgerFeedItems } from '@renderer/rendering/view/useLedgerFeedItems'
import { useLedgerFeedItems } from '@renderer/features/feed/ledger/useLedgerFeedItems'

// Claude paste-state-machine constants + helpers moved to
// ./TileLeaf/claudePaste.ts. Image helpers moved to
Expand Down
2 changes: 1 addition & 1 deletion src/shared/types/providerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export type TranscriptEntryMapper = {

/**
* Per-provider turn-ownership policy for the renderer's semantic reducer
* (`workspace/semantic/foldEvent.ts`).
* (`session-runtime/semantic/foldEvent.ts`).
*
* WHY this exists: foldSemanticEvent used to hard-gate turn replacement and
* block soft-open on `sessionKind === 'codex'` / `'claude'` literals at five
Expand Down