feat(transcript): add unified transcript layer, drop the /api/v2 RPC surface#1888
Conversation
🦋 Changeset detectedLatest commit: 55b743f The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
- add packages/transcript: agent-granular L1 store, idempotent L2 ops,
off/turn/block/delta L3 granularity, L4 view registry, and turn-cursor
pagination; sole owner of all transcript wire types
- kap-server: engine-event-driven TranscriptService with history backfill,
GET /sessions/{id}/transcript, and transcript.ops WS deltas with
per-connection granularity control
- kimi-inspect: render ChatView from the transcript surface (REST pages +
delta-only WS) instead of context memory
- sync flake.nix workspace lists and document packages/transcript and
packages/server-e2e in AGENTS.md
341402b to
c2c7450
Compare
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 341402bbf2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| ordinal: n, | ||
| state: 'running', | ||
| origin: mapTurnOrigin(event.origin), | ||
| startedAt: nowIso(), |
There was a problem hiding this comment.
Project live user prompts into turns
When a user sends a prompt after the transcript store has been bound, the live turn header is built from turn.started, which carries only the turn id/origin; this projector also does not observe the later context.append_message that contains the user text, and live REST reads return this in-memory store rather than rebuilding from context. The new ChatView clears the textarea after submit, so every live user bubble is missing from the transcript until a cold rebuild. Please add a live projection/optimistic upsert for the prompt text, or include the prompt in the turn header before emitting turn.upsert.
Useful? React with 👍 / 👎.
| if (!changed) return { state, changed: false }; | ||
| return { state: { ...state, items }, changed: true }; | ||
| } | ||
| return { state: { ...state, items: [...state.items, item] }, changed: true }; |
There was a problem hiding this comment.
Keep standalone items ordered during backfill
When a live session is backfilled while new live turns are arriving, historical turns are inserted by ordinal, but historical markers/taskrefs replayed by snapshotToOps hit this append-only path. A marker or taskref that originally belonged near an old turn (for example a skill/compaction marker or background-task ref) is therefore appended after newer live turns, corrupting the transcript order and pagination window. Please preserve a segment/order anchor for standalone items, or merge the backfill as an ordered snapshot rather than appending these items blindly.
Useful? React with 👍 / 👎.
- agent-core-v2: carry the extracted prompt text on turn.started so the transcript projector can render the user input at turn open (the context append with the same text is not a bus event and lands later) - kap-server: keep the prompt through turn.ended; anchor backfilled markers/taskrefs to their following snapshot turn so replaying history after live turns arrived keeps the historical order - transcript: add an optional beforeTurn placement anchor to marker.upsert / taskref.upsert; anchored inserts land before the first turn at or past the anchor instead of appending blindly Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: aa9ca9739f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| let open = kind === 'assistant' ? this.openText : this.openThinking; | ||
| if (open === undefined) { | ||
| const frameId = `${step.stepId}.f${++this.frameOrdinal}`; | ||
| open = { frameId, offset: 0, text: '' }; |
There was a problem hiding this comment.
Preserve stream offsets when attaching mid-turn
When the transcript service is first opened after a turn has already streamed some text, the history backfill can seed tN.1.f1 with the text already persisted, but this fresh projector did not observe those earlier deltas and starts the next append at offset 0. The reducer treats offset < local.length as an overlap and can replace the seeded text with only the later chunk, so opening kimi-inspect mid-response or after a reconnect can corrupt the live transcript until another full rebuild. The projector needs to initialize from the backfilled frame state or otherwise force a reset/gap instead of emitting offset-0 appends for an existing frame.
Useful? React with 👍 / 👎.
| const originKind = message.origin?.kind; | ||
|
|
||
| if (message.role === 'user') { | ||
| if (originKind !== undefined && HIDDEN_USER_ORIGINS.has(originKind)) continue; |
There was a problem hiding this comment.
Start a new turn for hidden system prompts
For persisted histories that contain a hidden system_trigger user message which actually opened a new turn (for example goal continuation uses a MessageStepRequest with origin: { kind: 'system_trigger' } and admission: 'newTurn'), this continue leaves the previous turn active. The following assistant messages are then folded into the prior visible user turn during cold rebuild/backfill, losing the system-triggered turn boundary and ordinal; hide the prompt if needed, but still start/advance a promptless turn or clear the current turn before consuming its assistant output.
Useful? React with 👍 / 👎.
- kap-server: on mid-stream attach, adopt the backfill-seeded stream frame (id + offset) instead of opening an empty one whose upsert clobbered the seeded text and whose offset-0 appends could not land - transcript: group a turn-opening system_trigger (goal continuation) into its own turn so cold rebuilds keep the turn boundary and stay ordinal-aligned with the engine's live turn numbering Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 049054661e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| * materialized in this process — comes back established. | ||
| */ | ||
| async ensureAgentHistory(sessionId: string, agentId: string): Promise<void> { | ||
| if (agentId === MAIN_AGENT_ID) return this.whenReady(sessionId); |
There was a problem hiding this comment.
Re-read main backfill after mid-turn attachment
When the first transcript read/subscription happens after a turn has started but before the LLM request finishes, the initial backfill can only see records already flushed to wire.jsonl; LoopService.appendResponseContent() persists content.part records only after request.result completes, while earlier assistant.delta events were missed by the newly-created projector. Because the main-agent path here never schedules another backfill after ready, subsequent REST refreshes keep the suffix-only live store and the answer prefix is missing until process restart. Re-run/merge main history after the step/turn completes, or allow refresh to force a cold resnapshot.
Useful? React with 👍 / 👎.
| */ | ||
| forSessionLive(sessionId: string): TranscriptStore | undefined { | ||
| const existing = this.live.get(sessionId); | ||
| if (existing !== undefined) return existing.store; |
There was a problem hiding this comment.
Drop live transcript stores when sessions close
After any REST/WS transcript access creates a live entry, this early return serves that store forever even if ISessionLifecycleService.close()/archive() later deletes and disposes the session handle. In that scenario the binding receives agent disposals and removes roster entries, so a subsequent GET /transcript for the closed/archived session hits this stale store and returns empty or stale data instead of falling through to readColdSnapshot from disk. Drop live entries on close/archive or re-check the lifecycle before returning an existing store.
Useful? React with 👍 / 👎.
- drop a session's live transcript store when the session closes or archives (lifecycle events plus a re-check on the cached-entry path) so reads fall back to the cold rebuild instead of serving a stale store - re-read an ended turn from the persisted wire records (debounced per agent) and merge it back live-first: headers keep live state/timestamps while origin/prompt recover from disk, and truncated text/thinking frames from a mid-turn attach are restored only when the persisted text is longer — a fresh live frame or a lagging flush is never reverted Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75278067f0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| return ( | ||
| interaction.origin.agentId ?? | ||
| (typeof payloadAgent === 'string' ? payloadAgent : undefined) ?? | ||
| MAIN_AGENT_ID |
There was a problem hiding this comment.
Route subagent questions to their owning transcript
When a subagent uses AskUserQuestion, SessionQuestionService records only origin: { turnId } and QuestionRequest has no agentId, so this fallback always assigns the interaction to main. In that context the question frame/resolution is added to the main transcript while the subagent transcript that owns the turn/tool call never shows the pending question.
Useful? React with 👍 / 👎.
| isError?: boolean; | ||
| }): TranscriptOperation[] { | ||
| const hit = this.toolFrames.get(event.toolCallId); | ||
| if (hit === undefined) return []; |
There was a problem hiding this comment.
Rehydrate missed tool results after mid-turn binding
If the transcript binds after tool.call.started but before tool.result, the backfill may seed the tool frame in the store but this projector map is still empty, so the result is dropped here; the later post-turn heal only replays text/thinking frames, leaving the tool output absent for live inspect clients until a cold rebuild/reload.
Useful? React with 👍 / 👎.
- kap-server: adopt the backfill-seeded tool frame when a tool.result arrives for a call that started before the projector attached, so the output lands instead of being dropped (the producer-store lookups now ride one projector options object) - agent-core-v2: record the owning agent on question interactions (ISessionQuestionService.request gains an agentId option, passed by AskUserQuestionTool from its agent scope) so a subagent's questions route to its own transcript and WS events instead of 'main' Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b3948c621f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const snapshot = transcript.snapshot({ tailTurns: TRANSCRIPT_RESET_TAIL_TURNS }); | ||
| target.send( | ||
| this.buildTranscriptEnvelope(state, 'transcript.reset', { | ||
| agent_id: transcript.agentId, | ||
| snapshot, |
There was a problem hiding this comment.
Filter transcript reset snapshots by grade
When a client subscribes at turn or block grade, live transcript.ops are filtered by filterOpsForGrade, but the initial/upgrade transcript.reset still sends transcript.snapshot(...) unchanged. In those subscription modes the first frame can include full steps, frames, and text content even though turn is documented as headers/global state only, so consumers requesting a lower-detail stream still receive the full transcript on every reset; pass the grade into the reset path and redact/filter the snapshot consistently.
Useful? React with 👍 / 👎.
| private markerOp(marker: string, payload: unknown): TranscriptOperation { | ||
| this.markerSeq += 1; | ||
| const item: TranscriptMarker = { | ||
| kind: 'marker', | ||
| markerId: `m${this.markerSeq}`, |
There was a problem hiding this comment.
Avoid colliding live marker IDs with backfilled markers
For resumed or mid-bind live sessions, historical markers rebuilt by groupMessagesIntoSnapshot/snapshotToOps use IDs like m1, m2, but a new AgentTranscriptProjector always starts markerSeq at zero. The first live goal/skill/compaction/notice marker after backfill therefore also gets m1, and applyItemUpsert treats it as the same item and replaces the historical marker instead of appending, corrupting the timeline; seed the counter from existing markers or use a live-specific stable namespace.
Useful? React with 👍 / 👎.
| // Link the spawning tool call to the new agent (Agent / AgentSwarm tool | ||
| // frames). The spawned payload carries no task id of its own — the | ||
| // subagent task above is keyed by the agent id instead. | ||
| const hit = this.toolFrames.get(event.parentToolCallId); | ||
| if (hit !== undefined) { |
There was a problem hiding this comment.
Adopt seeded parent tool frames before linking subagents
If transcript binding starts after an Agent/AgentSwarm tool call was persisted/backfilled but before subagent.spawned is emitted, the parent tool frame exists in the store while this.toolFrames is still empty. This lookup then skips the frame, so the spawned subagent's agentRefs is never written and the parent tool card cannot link to the child even though adoptToolFrame handles the same mid-bind case elsewhere; use the lookup fallback here too.
Useful? React with 👍 / 👎.
…resets - kap-server: fall back to store adoption when subagent.spawned links a parent tool call that started before the projector attached, so the agentRefs link is not lost on mid-bind attaches - kap-server: id live markers in their own namespace (live-mN) — the cold rebuild numbers its markers m1... too, and a colliding id made the store's upsert replace the historical marker instead of appending - transcript/kap-server: redact transcript.reset snapshots to the subscriber's grade (below 'block' the step/frame detail is stripped), so a 'turn'-grade subscription no longer receives full content on resets Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4eb8ec0ad9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| case 'cancelled': | ||
| case 'blocked': | ||
| return 'cancelled'; |
There was a problem hiding this comment.
When a turn.ended event arrives with reason blocked (the TurnEndReason contract says blocked folds into failed, and the existing v1 mapTurnReason does the same), this mapper records the transcript turn as cancelled. In blocked hook/goal scenarios, the transcript will incorrectly present the run as a user cancellation and failed-turn consumers will miss it; map blocked to failed here instead.
Useful? React with 👍 / 👎.
| // Record the owning agent so question events/frames route to this | ||
| // agent's surfaces (a subagent's question must not land on 'main'). |
There was a problem hiding this comment.
Move inline comment into v2 file header
packages/agent-core-v2/AGENTS.md requires comments in this package to live only in the top-of-file /** */ block and never beside methods or statements; this new inline note inside executeQuestion violates that scoped convention. Move the rationale into the file header or remove it so the v2 comment rule remains enforceable.
Useful? React with 👍 / 👎.
- kap-server: map turn.ended reason 'blocked' to the 'failed' transcript state, matching the engine's TurnEndReason wire contract and the v1 mapTurnReason folding (it was presented as a user cancellation) - agent-core-v2: move the question agentId rationale into the ask-user file header — package rules keep comments in the top-of-file block only Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9b9d4b6a1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| kind: 'question', | ||
| payload: req, | ||
| origin: { turnId: req.turnId }, | ||
| origin: { turnId: req.turnId, agentId: options?.agentId }, |
There was a problem hiding this comment.
Preserve the owning agent when resolving questions
When AskUserQuestion runs inside a subagent, passing agentId here makes the requested event use the subagent, but the v1 broadcaster only remembers InteractionKind and interactionResolvedEvent() still hard-codes resolved/dismissed question events to agentId: 'main'. With an agent_filter or per-agent UI subscribed to the subagent, the request is delivered but the answer/dismissal is filtered to main, leaving the subagent question pending in the client. Store the interaction's agent id alongside the kind and pass it into the resolved event.
Useful? React with 👍 / 👎.
| // Land the roster entry last, so roster-driven resets already see the | ||
| // backfilled content — this also guarantees a reset target for agents | ||
| // whose engine instance has not been created yet. | ||
| store.describeAgent({ agentId, type: agentId === MAIN_AGENT_ID ? 'main' : 'sub' }); |
There was a problem hiding this comment.
Keep persisted subagent descriptors on backfill
When a subagent history is fetched after backfillMain() has already seeded store.agents() from session metadata, this unconditional descriptor replaces the richer descriptorFromMeta() entry with only {agentId, type}. Opening a subagent transcript therefore strips its parentAgentId/label from subsequent transcript REST responses and roster notifications. Reuse the existing metadata descriptor, or avoid downgrading an existing descriptor here.
Useful? React with 👍 / 👎.
- kap-server: keep the metadata-seeded roster descriptor (parentAgentId /
label) when an on-demand history backfill lands its roster entry, instead
of downgrading it to { agentId, type }
- kap-server: remember each interaction's owning agent and stamp it on the
resolved question/approval v1 events — they were hard-coded to 'main', so
an agent-filtered subscriber saw a subagent's question open but never
close
Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6056376d98
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| for (const pending of interactions.listPending()) { | ||
| if (knownInteractions.has(pending.id)) continue; | ||
| knownInteractions.add(pending.id); | ||
| announceInteraction(pending); |
There was a problem hiding this comment.
Announce pending interactions after backfill
When the transcript binding is created while an approval/question is already pending, this loop announces the interaction before TranscriptService.backfillAgent has replayed the persisted tool-call frame. In that mid-turn attach case, mapInteractionRequested falls back to a synthetic step, and mapInteractionResolved cannot add the approvalId back-link to the tool frame, so approval/plan cards render detached from the tool until a cold rebuild.
Useful? React with 👍 / 👎.
| store.describeAgent({ | ||
| agentId, | ||
| type: existing?.type ?? (agentId === MAIN_AGENT_ID ? 'main' : 'sub'), |
There was a problem hiding this comment.
Avoid adding empty typo agents to the roster
When a live transcript is requested for an agent id with no wire records, readColdSnapshot returns an empty snapshot, but this unconditional describeAgent still adds that id to the in-memory roster. A typo or probe such as agent_id=nope therefore pollutes later agents responses and wildcard transcript subscribers with a ghost subagent until the session is dropped.
Useful? React with 👍 / 👎.
| // The value import also loads the `DomainEventMap` augmentation for the | ||
| // `turn.*` / delta events this service publishes (the augmentation lives with | ||
| // the event definitions; without an import it would not enter every | ||
| // consumer's program). |
There was a problem hiding this comment.
Move import rationale into the file header
packages/agent-core-v2/AGENTS.md says comments in this tree must live solely in the top-of-file /** */ block and never beside functions, methods, or statements. This newly added import-adjacent comment violates that local rule; please fold the rationale into the file header or remove it.
Useful? React with 👍 / 👎.
- kap-server: announce interactions pending at bind time only after the initial backfill (new TranscriptBinding.seedPendingInteractions), and adopt the seeded tool frame on the request/resolve paths, so a pre-existing approval lands next to its backfilled tool call and keeps the approvalId back-link - kap-server: skip the roster entry when a probed agent id has neither a roster presence nor persisted content (agent_id=nope no longer conjures a ghost subagent) - agent-core-v2: fold the turnEvents import rationale into the loop file header (package rule: comments live in the top-of-file block only) Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cec3cecf34
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| summary.workspaceId, | ||
| sessionId, | ||
| AGENTS_DIR, | ||
| agentId, |
There was a problem hiding this comment.
Reject path traversal in transcript agent IDs
When /api/v1/sessions/{sid}/transcript is called with an agent_id containing .. or path separators, this unvalidated value is passed directly into the filesystem path before readWireRecords(). For both cold reads and live-session backfills, an authenticated client can therefore make the server read a wire.jsonl outside the intended agents/<agentId> directory; validate the id against the session's agent registry or reject path segments before joining it into the path.
Useful? React with 👍 / 👎.
| const grade = gradeFor(sub.transcriptGrades, agentId); | ||
| const filtered = filterOpsForGrade(grade, ops); |
There was a problem hiding this comment.
Apply agent filters to transcript fan-out
For clients that combine the existing v1 agent_filter with a transcript wildcard grade, transcript events bypass the allowlist: the normal event path later in dispatch() checks matchesAgentFilter(...), but this transcript fan-out only checks transcriptGrades, so a connection filtered to main can still receive subagent transcript ops/resets when '*': 'delta' is set. Apply the same agent filter before sending transcript frames, or document that transcript grades intentionally override the legacy filter.
Useful? React with 👍 / 👎.
- transcript/kap-server: validate agent ids as plain names (no separators, no dot segments) at the REST query layer and again before the id is joined into the wire-records path — an authenticated client could otherwise read a wire.jsonl outside the agents directory - kap-server: compose the legacy v1 agent allowlist with transcript grades on every fan-out path (initial resets, per-ops fan-out, roster-driven resets), so a filtered connection no longer receives other agents' transcript frames Addresses review feedback on MoonshotAI#1888.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 843c903734
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // The failure text was never streamed — emit it before completing, | ||
| // so live consumers (transcript tasks) see the output too. |
There was a problem hiding this comment.
Remove inline comments from agent-core-v2 code
packages/agent-core-v2/AGENTS.md requires comments in this package to live solely in the top-of-file block and says to never place them beside functions, methods, or statements. This newly added explanatory comment inside run (and the similar new inline comments in this file) violates that convention; please move any needed context into the file header or remove it.
Useful? React with 👍 / 👎.
| const deferred = state.deferredTranscriptSeeds.get(target); | ||
| if (deferred === undefined) return; | ||
| state.deferredTranscriptSeeds.delete(target); | ||
| await this.subscribeTranscript(state, target, deferred.spec, deferred.prevGrades, deferred.prevFilter); |
There was a problem hiding this comment.
Force a reset after cursor-based transcript replay
When an already-subscribed connection re-subscribes with a cursor at the same transcript grade, subscribe(..., deferTranscriptReset: true) removes the target from transcriptSeeded, so any volatile transcript.ops arriving during the durable replay are dropped. flushTranscriptSeed then passes the previous grades here, so subscribeTranscript sees no upgrade and sends no reset before marking the target seeded again; that leaves the client missing the dropped transcript ops. Cursor-based flushes should force a baseline reset even when the grade/filter did not change.
Useful? React with 👍 / 👎.
A cursor re-subscribe at unchanged grades deferred its baseline and then compared against the previous grades on flush, so no reset was sent — while volatile ops fanned out during the deferral had been dropped, leaving the client with a permanent gap. flushTranscriptSeed now always seeds a full baseline (previous grades no longer tracked in the deferred record), and a regression test covers the same-grade cursor resubscribe. Also drop the inline comments added to shellCommandService.ts — the agent-core-v2 convention keeps commentary in the top-of-file block; the context moved there.
|
@codex review |
|
Codex Review: Didn't find any major issues. Bravo. Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 35351131f7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| await Promise.all( | ||
| Object.keys(spec) | ||
| .filter((agentId) => agentId !== '*' && gradeFor(spec, agentId) !== 'off') | ||
| .map((agentId) => service.ensureAgentHistory(state.sessionId, agentId)), |
There was a problem hiding this comment.
Backfill wildcard-selected roster agents
When a client subscribes with a wildcard grade such as { '*': 'delta' } after the live store has been seeded from session metadata, this only backfills explicitly named agents and skips '*'. Historical subagents can already be present in store.agents() from the roster, but their AgentTranscript is never materialized, so the reset loop later sees no transcript and sends no baseline for them; wildcard subscribers silently miss existing subagent transcripts unless they enumerate every agent id. Ensure admitted roster agents are backfilled before sending wildcard resets.
Useful? React with 👍 / 👎.
| const agentFilter = state.targets.get(target)?.agentFilter; | ||
| for (const descriptor of store.agents()) { | ||
| if (agentFilter !== undefined && !agentFilter.has(descriptor.agentId)) continue; | ||
| const grade = gradeFor(spec, descriptor.agentId); |
There was a problem hiding this comment.
Re-check subscription before sending resets
If two subscribe/unsubscribe frames for the same target overlap while whenReady() or ensureAgentHistory() is awaiting, this resumes with the stale spec from the earlier call and sends resets even if state.targets now has a downgraded/off subscription or no target at all. The WS handler starts subscribe work asynchronously, so a slow history backfill can deliver a delta/block reset after a client has downgraded or unsubscribed. Re-read the current subscription after the awaits and return unless it still matches before computing grades and sending resets.
Useful? React with 👍 / 👎.
… subs Two subscribeTranscript gaps found in review: - Re-read the target's subscription after the history awaits: subscribe work runs asynchronously, so an overlapping downgrade/unsubscribe used to be answered with resets computed from the stale spec. The reset loop now uses the latest grades/filter from state.targets and bails when the target is gone or no longer graded. - Backfill roster agents admitted via the wildcard grade, not just explicitly named ones: a historical subagent seeded into the roster from session metadata had no materialized AgentTranscript, so wildcard subscribers silently never received its baseline reset. Adds regression tests for the wildcard backfill, the mid-seed downgrade, and the mid-seed unsubscribe (all three fail without the fix); makeCore now accepts persisted agent metadata for roster seeding.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f6fd1a762
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const modes: { plan?: Record<string, never>; swarm?: Record<string, never> } = {}; | ||
| if (event.planMode === true) modes.plan = {}; | ||
| if (event.swarmMode === true) modes.swarm = {}; | ||
| if (modes.plan === undefined && modes.swarm === undefined) return []; | ||
| return [{ op: 'meta.merge', meta: { modes } }]; |
There was a problem hiding this comment.
Handle mode exit events in transcript meta
When plan or swarm mode exits, the engine emits agent.status.updated with planMode: false / swarmMode: false, but this branch only emits a meta.merge for true and returns nothing for false. Once a live transcript has set meta.modes.plan or meta.modes.swarm, clients that render transcript meta will keep showing that mode as active until a full reset; emit a clearing operation for the corresponding mode instead of dropping exit events.
Useful? React with 👍 / 👎.
`agent.status.updated` with `planMode: false` / `swarmMode: false` was dropped by the transcript projector because `meta.merge` could only set mode badges, never clear them — clients kept rendering an exited mode until the next full reset. The merge wire shape now accepts `null` per mode key (set = object, clear = null, absent = keep): the reducer deletes the key and normalizes an empty `modes` away, the zod schema validates the nullable form, and the projector emits the clearing op for exit events.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0f471b428b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| type: 'turn.started', | ||
| turnId: job.turn.id, | ||
| origin, | ||
| prompt: turnPromptText(job.seed.input), |
There was a problem hiding this comment.
Do not expose hidden system prompts in transcripts
When the turn origin is a system-triggered turn such as a goal continuation or subagent run, job.seed.input is internal steering text rather than displayable user input; the existing web transcript explicitly hides system triggers (apps/kimi-web/src/composables/messagesToTurns.ts:473-479). Passing it through turn.started.prompt here makes the new live REST/WS transcript render those hidden prompts whenever those turns start, and the cold rebuild mirrors the same leak via groupTurns.ts:126. Please gate prompt population to displayable user origins, or emit the hidden-origin turn boundary without prompt text.
Useful? React with 👍 / 👎.
`turn.started.prompt` was populated from the turn input for every origin, so system-triggered turns (goal continuation, subagent run, cron) exposed their internal steering text to live transcript consumers; the cold rebuild mirrored the same leak when grouping persisted history. The loop now populates the prompt only for displayable user origins (user input, or a user-slash skill/plugin activation) via the new isDisplayablePromptOrigin gate, and the cold grouping opens hidden-origin turns promptless. Turns still open normally — only the prompt text is withheld.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d29e44153d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
|
||
| /** Dispose the live store + binding for a session (session closed / server shutdown). */ | ||
| dropSession(sessionId: string): void { | ||
| this.opsListeners.delete(sessionId); |
There was a problem hiding this comment.
Reattach transcript listeners after session reload
When a live session is closed or archived, this removes the onSessionOps listener set that SessionEventBroadcaster registered, but the broadcaster's SessionState still keeps transcriptStream populated, so ensureTranscriptStream() returns early on a later subscribe for the same session. If the session is resumed in the same daemon, the subscriber may get a fresh transcript.reset, but no mapped-op listener is re-registered and subsequent live transcript.ops are never delivered.
Useful? React with 👍 / 👎.
When the engine session closed or archived, TranscriptService dropped the live store together with its ops listener set, but the broadcaster's SessionState kept its transcriptStream — so ensureTranscriptStream returned early for a later subscribe on the resumed session, delivering a fresh reset but never the live transcript.ops. The stream is now pinned to its TranscriptStore instance and the fan-out re-registers whenever a rebuilt store shows up. Adds a regression test that drops the service entry mid-stream and asserts ops keep flowing after resubscribe (fails without the fix).
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f15354cde
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| case 'task': { | ||
| const taskId = (origin as { taskId?: unknown }).taskId; | ||
| return taskId !== undefined && typeof taskId === 'string' | ||
| ? { kind: 'task', taskId, payload: origin } | ||
| : { kind: 'other', payload: origin }; |
There was a problem hiding this comment.
Preserve background-task origins during cold rebuilds
When rebuilding cold transcripts for legacy/v1 sessions, background task notifications are persisted with origin.kind === 'background_task' (the live mapper already handles this spelling), but this branch only maps task origins. Those messages therefore fall through to { kind: 'other' } after a restart and lose their taskId, so the transcript can no longer associate the notification turn with the background task/subagent. Include background_task in the task-origin branch.
Useful? React with 👍 / 👎.
Legacy/v1 sessions persist background-task notifications with
origin.kind === 'background_task' (the live mapper already handles that
spelling), but the cold grouping only mapped 'task' — after a restart
those turns fell through to { kind: 'other' } and lost their taskId, so
the transcript could no longer associate the notification turn with its
background task. Both spellings now share the task-origin branch.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9f78f9feb5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const taskId = this.shellTasks.get(event.commandId) ?? event.taskId; | ||
| if (taskId === undefined) return []; | ||
| this.shellTasks.set(event.commandId, taskId); |
There was a problem hiding this comment.
Preserve shell failures before task startup
When a foreground ! command fails before onForegroundTaskStart runs, such as Bash validation/spawn/registerTask errors, shellCommandService publishes shell.output/shell.completed with taskId undefined. This guard drops those events, so the live transcript never gets a task ref, stderr, or terminal state for that failed command even though the command did run and appended shell output to context. Consider falling back to a synthetic task id (for example the command id) or otherwise projecting these no-taskId failures instead of returning no ops.
Useful? React with 👍 / 👎.
A foreground `!` command that failed before onForegroundTaskStart ran (Bash validation/spawn/registerTask errors) published shell.output / shell.completed with taskId undefined, and the projector's guard dropped them — the live transcript lost the stderr and the terminal state of a command that did run. Shell events now resolve their task as the id learned at shell.started, else the event's own taskId, else a synthetic per-command id (shell-<commandId>), so early failures land like any other shell task.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 266edaa07b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if (event.type === 'task.started') { | ||
| ops.push({ | ||
| op: 'taskref.upsert', | ||
| item: { kind: 'taskref', refId: `ref-${info.taskId}`, taskId: info.taskId, at: nowIso() }, | ||
| }); |
There was a problem hiding this comment.
Emit taskrefs for first-seen task terminations
When the transcript binding starts after a background process/subagent/question has already emitted task.started, the later task.terminated is the first lifecycle event this projector sees. This branch emits only task.upsert for terminations, so the task exists only in the global task map with no taskref timeline item; the chat view renders standalone tasks through taskref items, so that completed background work never appears in the transcript. Mirror the shell.completed missed-start guard by emitting the ref when this is the first task event observed.
Useful? React with 👍 / 👎.
- kap-server: remove the /api/v2 REST routes and /api/v2/ws socket (registerRpcRoutes renamed to serviceDispatcherRoutes; transport/ws/{eventMap,registerWs,wsClient,wsConnection,wsProtocol} deleted). /api/v1/debug/* is now the only RPC surface — a reflection dispatcher over the entire scoped DI registry with no whitelist — and /api/v1/ws the only WS endpoint
- klient: drop the http transport (transports/http/*, transports/ws/wsSocket.ts) and the kap-server devDependency; transports reduce to the ipc|memory subpath entries, and the dual/v2 e2e suites go with them
- kimi-inspect: target /api/v1/debug only with no fallback, replace the Service-event push channel (wsChannel/wsSocket) with on-demand fetch plus 15 s polling, and show a blocking "Debug surface unavailable" screen on connection failure
- transcript: add global attachment/interaction/todo entities (model, ops, wire schema) and project them from engine events in kap-server's coreEventMap
Related Issue
No linked issue — the problem is explained below.
Problem
Session rendering state (turns, steps, tool calls, tasks) was re-derived independently by every consumer: kap-server re-projected context memory for REST, the v1 WS broadcaster pushed raw engine events, and each client rebuilt its own timeline from those events. Every new frame kind or edge case — cold rebuilds, session reloads, subagent frame adoption, missed tool results — had to be fixed in several places at once, and the surfaces still disagreed with each other.
At the same time the RPC surface existed three times over (
/api/v2REST,/api/v2/ws, and the dev-only/api/v1/debug/*) with a channel whitelist to maintain, and the klient http transport plus the dual/v2 e2e suites existed only to serve it.What changed
1. Unified transcript layer (
@moonshot-ai/transcript)Problem: every consumer re-derived rendering state on its own, so fixes and new frame kinds had to be replicated across server and clients.
What was done:
packages/transcript: a pure-TypeScript, browser-safe transcript data layer — agent-granular L1 store, idempotent L2 operations,off/turn/block/deltaL3 subscription granularity, framework-free L4 view registry, and turn-cursor pagination. It is the sole owner of all transcript wire types.TranscriptServiceprojects core events into transcript ops, backfills history from the persisted per-agent wire records (cold sessions rebuild any agent, with 0-based turn ordinals matching the engine's), and exposesGET /api/v1/sessions/{id}/transcript(cursor pages) plus delta-onlytranscript.opsover/api/v1/wswith per-connection granularity control.ChatViewrenders turn-granularly from the transcript surface (REST pages + delta-only WS) instead of context memory, reusing the package's L2 reducer with no local re-implementation.2. Drop the /api/v2 RPC surface and the klient http transport
Problem: the RPC surface was triplicated (v2 REST, v2 WS, debug REST) with a whitelist to keep in sync, and the klient http transport plus the dual/v2 e2e suites existed only to serve it.
What was done:
/api/v2REST routes and/api/v2/wssocket (registerRpcRoutesrenamed toserviceDispatcherRoutes; the v2 ws modules deleted)./api/v1/debug/*is now the only RPC surface — a reflection dispatcher over the entire scoped DI registry with no whitelist, mounted only with--debug-endpointson a loopback bind and gated by the global bearer auth./api/v1/wsis the only WS endpoint.ipc|memorysubpath entries, and the dual/v2 e2e suites were removed with them./api/v1/debugonly (no fallback), polls react-query on a 15 s interval instead of a Service-event push channel, and shows a blocking "Debug surface unavailable" screen on connection failure.3. Supporting fixes
shell.completedon the v1 event surface; node-sdk covers it in the exhaustive event switch.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.