Repository navigation
Conversation
Add task.attach / task.detach client messages and task.output events. Output offsets are UTF-8 byte positions taken from the agent task service, which now exposes onDidAppendOutput and readOutputTail. The first chunk is at most the last 4 KiB, aligned to a character boundary, and since_offset resumes from the client's local end when it is still within that tail. Attachments are per connection and are released when the connection closes.
🦋 Changeset detectedLatest commit: 27b09a7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8485e2782b
ℹ️ 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".
| void core; | ||
| const wss = new WebSocketServer({ noServer: true, handleProtocols: selectWsBearerProtocol }); | ||
| const { registry, broadcaster } = opts; | ||
| const taskOutputSource = createTaskOutputSource(core); |
There was a problem hiding this comment.
Gate task-output streaming behind an experimental flag
This installs the new task-output protocol for every kap-server instance even though the commit describes the client integration as a future change. Because this is a not-yet-public feature, declare an owning-domain flag and only pass the task-output source when that flag is enabled, defaulting it off.
AGENTS.md reference: AGENTS.md:L64-L67
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Not gating this one. task.attach / task.detach only take effect when a client sends them, and no released client does, so a default-off flag would protect no user. It would only force the desktop embedded server to opt in through an env var and require another PR to flip the default once the client adopts it. The client side lands in the follow-up code-app PR.
| const tail = await resolved.tasks | ||
| .readOutputTail(taskId, TASK_OUTPUT_TAIL_BYTES) | ||
| .catch(() => ({ offset: 0, data: new Uint8Array() })); |
There was a problem hiding this comment.
Report tail-read failures instead of returning success
When reading persisted output fails—for example because of a transient filesystem or permission error—this converts the failure into an empty tail and then sends a successful response. A completed task consequently appears to have no output, and the client receives no indication that data was dropped; detach the attachment and return an error response instead of fabricating an empty result.
AGENTS.md reference: AGENTS.md:L130-L132
Useful? React with 👍 / 👎.
| let resolved: TaskOutputSourceResult; | ||
| try { | ||
| resolved = await this.source(sessionId, taskId); | ||
| } catch { | ||
| resolved = { kind: 'session_not_found' }; |
There was a problem hiding this comment.
Preserve source errors instead of claiming the session is absent
If resuming the session or initializing its main agent throws because of an I/O, restoration, or service error, this catch rewrites the failure as session_not_found and returns 40401. Clients can then incorrectly treat an existing session as deleted; only return 40401 for an actual missing-session result and surface unexpected failures as an internal error.
Useful? React with 👍 / 👎.
| const offset = entry.outputSizeBytes; | ||
| entry.outputSizeBytes += chunkBytes; | ||
| this.appendRetainedOutput(entry, chunk, chunkBytes); | ||
| this._onDidAppendOutput.fire({ taskId: entry.taskId, offset, text: chunk }); |
There was a problem hiding this comment.
Apply the output limit before publishing chunks
When a process crosses the 16 MiB task-output limit, this event fires before outputLimitTripped is checked, and every later chunk continues to fire while asynchronous termination is in progress. A large or SIGTERM-ignoring process can therefore send arbitrarily more data through each WebSocket attachment even though those bytes are deliberately not persisted; this defeats the output cap and leaves reconnect offsets ahead of the available tail. Only publish chunks that pass the same limit check as persistence.
Useful? React with 👍 / 👎.
| session_id: z.string(), | ||
| task_id: z.string(), |
There was a problem hiding this comment.
Reject empty task attachment identifiers
Both attachment schemas accept empty session_id and task_id strings, so requests such as { session_id: "", task_id: "" } pass validation and are answered as missing resources rather than with the documented 40001 invalid-request response. The existing REST task contract requires these identifiers to be nonempty; apply the same .min(1) validation here and in the detach schema.
Useful? React with 👍 / 👎.
| sinceOffset !== undefined && sinceOffset >= tailOffset && sinceOffset <= end | ||
| ? sinceOffset | ||
| : tailOffset; | ||
| let start = requested - tailOffset; | ||
| while (start < data.byteLength && isUtf8Continuation(data[start]!)) start += 1; |
There was a problem hiding this comment.
Fall back when since_offset splits a UTF-8 character
When a reconnect supplies a since_offset that lands on a UTF-8 continuation byte, this treats it as valid and advances to the next character boundary. That silently skips bytes rather than applying the documented invalid-offset fallback; if the offset falls inside the final character, firstChunk even returns no output. Detect a non-boundary requested offset and restart from the character-aligned tail instead.
Useful? React with 👍 / 👎.
Publish appended task output only when it is kept, so nothing past the 16 MiB output cap is streamed and live offsets stay within the persisted output. Reply 50001 when the task lookup throws and 50003 when the output cannot be read, instead of reporting a missing session or an empty output. Reject empty session and task ids, and treat a since_offset inside a multi-byte character as invalid.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57dff2e134
ℹ️ 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 taskAttachSchema = z.object({ | ||
| type: z.literal('task.attach'), | ||
| request_id: z.string(), | ||
| session_id: z.string().min(1), | ||
| task_id: z.string().min(1), | ||
| since_offset: z.number().int().nonnegative().optional(), |
There was a problem hiding this comment.
Publish task-output frames in the WebSocket contract
These transport-local schemas add a new wire protocol, but protocol/ws-control.ts does not include task.attach, task.detach, response, or task.output in its message unions or wsOperations. Because createAsyncApiDocument() builds /asyncapi.json exclusively from wsOperations, external WebSocket consumers relying on the documented discovery/code-generation contract cannot discover these frames and schema-based clients may reject them; define and export the frames through the canonical protocol operations and reuse those schemas here.
AGENTS.md reference: AGENTS.md:L122-L124
Useful? React with 👍 / 👎.
Move the task output end-to-end case into transcriptContract.e2e.test.ts now that wsV1Resync.test.ts is gone, and drop the task.output coalescing that lived in the removed coalesceFrames.
Tasks leave the transcript stream and get their own WebSocket event: - Each subscribed agent's tasks are sent as `task` events: every task (ended ones included) right after a transcript reset or catch-up, then the changed task on every change. Agents at grade `off` get none, and task events do not bubble to ancestor agents. Besides the transcript task fields, an event carries `command` and `parent_tool_call_id` from the owning agent's task service when it knows the task. - The transcript no longer carries tasks on the wire: WebSocket resets and both REST `/transcript` paths answer `tasks: []`, `task.upsert` ops are split off before journaling, and task-only batches consume no seq. - `@moonshot-ai/transcript` drops `taskref` items, `TranscriptTask.outputTail` and the `task` append target; shell output only flows through `task.attach`. - `task.terminated` records no output tail anymore; older records that still carry one replay unchanged. - The AsyncAPI document lists `task`, `task.output`, `task.attach` and `task.detach`. - kimi-inspect reads task state from `task` events and keeps it across REST refreshes.
code-app: 80156e1a2fc3771a0107644a54ec0640c4a20097
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c409d9e0c8
ℹ️ 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".
| @@ -156,7 +156,7 @@ export function registerTranscriptRoutes(app: TranscriptRouteHost, deps: Transcr | |||
| agent_id: query.agent_id, | |||
| items: page.items, | |||
| has_more: page.hasMore, | |||
| tasks: [...transcript.getTasks().values()], | |||
| tasks: [], | |||
There was a problem hiding this comment.
Preserve tasks for existing transcript clients
When a server is upgraded while a REST-only or older WebSocket client is still deployed, every transcript request now returns tasks: [] even though the live store or cold snapshot contains task records. The replacement task frames only reach clients that subscribe and understand the new protocol, so existing consumers silently lose running and historical tasks; keep populating this field during the compatibility window or version the contract.
AGENTS.md reference: AGENTS.md:L111-L117
Useful? React with 👍 / 👎.
| output: (frame) => { | ||
| this.sendSubscribedFrame(frame); | ||
| }, |
There was a problem hiding this comment.
Coalesce contiguous task-output chunks before flushing
For commands that emit many small chunks, this callback queues one task.output frame per onDidAppendOutput event, while sendSubscribedFrame merely appends each frame to outbound. Consequently the advertised 16 ms batching still serializes and sends every JSON frame, and reaching maxBatchSize forces additional flushes; merge contiguous chunks for the same session/task before enqueueing or flushing them.
Useful? React with 👍 / 👎.
| onTask: (aid, task) => { | ||
| if (aid !== agentId) return; | ||
| store.applyOps([{ op: 'task.upsert', task }]); | ||
| trail.recordEvent('task', `${task.taskId} ${task.state}`, store.getState()); |
There was a problem hiding this comment.
Clear stale tasks when a transcript reset is received
When reconnect catch-up is no longer possible, the server sends a transcript.reset followed by the current full set of task frames. This handler only upserts those frames, while the reset handler ignores the snapshot and REST replacement preserves the existing task map, so a task removed by an undo while the client was disconnected remains visible indefinitely. Clear the task map when the reset arrives before applying the replayed task frames.
AGENTS.md reference: AGENTS.md:L127-L129
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: af1516a8b4
ℹ️ 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 (entry.outputLimitTripped) return; | ||
| this._onDidAppendOutput.fire({ taskId: entry.taskId, offset, text: chunk }); |
There was a problem hiding this comment.
Keep streamed offsets aligned with persisted output
When detached output persistence is active and IFileSystemStorageService.append rejects, this publishes the chunk before the write attempt while the existing write queue silently swallows the failure. Later chunks can still be appended, so live clients observe logical offsets that include the missing bytes, whereas a reconnect or fresh attach derives its tail offset from the shorter persisted file; this silently loses output and can make clients splice subsequent bytes at incorrect positions. Publish only successfully retained chunks or propagate the failed write and terminate the attachment.
AGENTS.md reference: AGENTS.md:L131-L133
Useful? React with 👍 / 👎.
| agent_id: agentId, | ||
| event_created_at: createdAt, | ||
| task_id: task.taskId, | ||
| kind: task.kind, |
There was a problem hiding this comment.
Emit question tasks with the tool kind
When AskUserQuestion runs in the background, QuestionBackgroundTask records info.kind as question, but the transcript projectors map every non-process/non-agent kind to other, and this new frame copies that value verbatim. Clients therefore receive kind: "other" instead of the documented "tool" classification—even though the existing REST task projection maps the same engine kind to tool—so clients cannot reliably group or render these long-running tool tasks. Map question tasks to tool when projecting or emitting the frame.
AGENTS.md reference: AGENTS.md:L127-L129
Useful? React with 👍 / 👎.
| <pre className="max-h-40 overflow-auto whitespace-pre-wrap text-neutral-400"> | ||
| {task.outputTail} | ||
| </pre> | ||
| ) : null} |
There was a problem hiding this comment.
Continue loading task output in kimi-inspect
For a background shell whose tool frame contains only the task id/status, this now renders nothing when frame.output is absent: the previous branch displayed task.outputTail, while the new onTask path stores task metadata but never sends task.attach or retains task.output frames. As a result, kimi-inspect users silently lose both live and final shell output in the chat view; integrate the new attachment stream before removing the old output fallback.
AGENTS.md reference: AGENTS.md:L111-L113
Useful? React with 👍 / 👎.
| onTask: (aid, task) => { | ||
| if (aid !== agentId) return; | ||
| store.applyOps([{ op: 'task.upsert', task }]); | ||
| trail.recordEvent('task', `${task.taskId} ${task.state}`, store.getState()); |
There was a problem hiding this comment.
Preserve raw task frames in the audit trail
When a task frame arrives, this records only a synthesized taskId state string rather than the received frame. The Audit panel's raw-event view therefore cannot inspect the task's kind, outcome, timestamps, usage, error, or other wire fields, so the newly added frame type violates the audit trail's guarantee to retain every WebSocket frame and its raw payload. Add a task-specific audit entry that carries the complete frame instead of reducing it to a summary.
AGENTS.md reference: apps/kimi-inspect/AGENTS.md:L44-L44
Useful? React with 👍 / 👎.
SessionTasks keeps one task list per agent for each live session and folds live engine events and wire records through the same apply, so the transcript package no longer models tasks: the task type, task.upsert / taskref.upsert ops, task append targets and snapshot tasks are gone, and the REST transcript and reset snapshots drop their tasks field. kimi-inspect keeps the latest task frame per task id.
…king lost subagents
Keep only the removal of TaskRefView: transcripts no longer contain taskref items, and the TranscriptTask type it referenced is gone, which fails type-aware lint.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: db2cacbcaf
ℹ️ 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".
| { sessionId, agentId, err: error instanceof Error ? error.message : error }, | ||
| 'transcript: task history read failed, continuing without it', | ||
| ); | ||
| return []; |
There was a problem hiding this comment.
Do not cache an empty task history after a read failure
When the session index or wire read fails transiently, this returns an empty history, allowing SessionTasks.load() to mark the agent as permanently loaded and send a successful subscription with no historical or currently running tasks. Because that SessionTasks instance never retries, desktop/web clients silently lose the task list until the live session is dropped and recreated; propagate the failure so the subscription can fail or retry rather than caching fabricated emptiness.
AGENTS.md reference: AGENTS.md:L131-L133
Useful? React with 👍 / 👎.
…32 KiB Task frames never emitted kind tool, and the larger first chunk matches the previous REST final-output window so ended-task panels stay useful after clients stop fetching output over HTTP.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3f2d3797c0
ℹ️ 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".
| | `task.attach` | flat: `{ request_id, session_id, task_id, since_offset? }` | Stream one main-agent task's output to this connection, no session subscription needed; `since_offset` resumes after output the client already has | | ||
| | `task.detach` | flat: `{ request_id, session_id, task_id }` | Stop streaming that task's output | | ||
|
|
||
| `task.attach` and `task.detach` are flat: their fields sit next to `type` instead of under `payload`, `request_id` replaces `id`, and the reply is `{ "type": "response", "request_id", "code", "msg"? }` instead of an ack. A successful `task.attach` replies with code 0, then sends the last 4 KiB of output (only the part after `since_offset` when it falls inside that window) as one `task.output` frame, followed by live output. Errors: `40001` malformed frame, `40401` session not found, `40406` task not found, `50003` output unreadable. |
There was a problem hiding this comment.
Document the actual 32 KiB attach tail
The English and Chinese API references still say that a successful task.attach sends at most the last 4 KiB and treats older since_offset values as outside that window, but TASK_OUTPUT_TAIL_BYTES is now 32 KiB. For example, an offset 20 KiB behind the end resumes from that offset and sends roughly 20 KiB rather than falling back to 4 KiB, so clients and contract tests following the published recovery semantics will make incorrect assumptions about frame size and replay behavior. Update both references to the 32 KiB window.
Useful? React with 👍 / 👎.
| }, | ||
| task: (message) => { | ||
| for (const [target, spec] of state.targets) { | ||
| if (!state.transcriptSeeded.has(target) || gradeFor(spec, message.agent_id) === 'off') continue; |
There was a problem hiding this comment.
Buffer task changes until subscription seeding completes
When a task changes after sendTasks() captures the baseline but before the awaited subscribeTranscript() returns and adds the target to transcriptSeeded, this guard silently drops the change. Unlike transcript operations, task frames have no journal or sequence cursor, so a task that finishes in this window remains shown as running until another change or a reconnect. Buffer task changes during seeding, or establish the target before taking and sending an ordered baseline.
AGENTS.md reference: AGENTS.md:L131-L133
Useful? React with 👍 / 👎.
| result_summary: rerun ? undefined : (text(event['resultSummary']) ?? prev.result_summary), | ||
| usage: rerun ? undefined : (usageOf(event['usage']) ?? prev.usage), | ||
| error: rerun ? undefined : (text(event['error']) ?? prev.error), | ||
| state_reason: text(event['reason']) ?? (rerun ? undefined : prev.state_reason), |
There was a problem hiding this comment.
Clear the suspension reason when a subagent resumes
When a suspended subagent emits subagent.started on resumption, outcome is undefined but the previous task is still running, so rerun is false and this expression preserves the old state_reason. Because toMessage() derives phase: "suspended" from the presence of that field, clients continue to show the resumed subagent as suspended until it terminates. Handle subagent.started by clearing state_reason while retaining the existing start timestamp.
Useful? React with 👍 / 👎.
…nt resume - Mark a subscriber as seeded in the same tick that sends its task baseline, so a task change landing right after the baseline is no longer dropped. - Clear state_reason when a suspended subagent resumes, so its phase returns to working. - Document the 32 KiB task.attach tail.
A successful task.attach now always yields one first task.output frame, with empty text when there is nothing to send. Clients can then tell "first chunk not received yet" apart from "the task has no output".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a43157fe2
ℹ️ 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 agent = await ensureMainAgent(session); | ||
| const tasks = agent.accessor.get(IAgentTaskService); |
There was a problem hiding this comment.
Resolve task output from the owning agent
When a subscribed subagent starts a background shell, the transcript binding emits its task frame under that subagent, but task.attach always looks in the main agent's IAgentTaskService. Attaching the emitted task ID therefore returns TASK_NOT_FOUND; because this change also removes task output from transcript snapshots, clients have no remaining way to retrieve that subagent task's output. Resolve the task through its owning agent rather than unconditionally calling ensureMainAgent.
AGENTS.md reference: AGENTS.md:L127-L129
Useful? React with 👍 / 👎.
| return; | ||
| } | ||
| if (resolved.kind === 'task_not_found') { | ||
| fail(ErrorCode.TASK_NOT_FOUND, 'task not found'); |
There was a problem hiding this comment.
Serialize attach and detach operations
When task.detach arrives while an earlier attach is still waiting for session restoration or task lookup, the detach removes the placeholder and replies successfully, but the stale attach later enters this branch and also reports code 0 despite no attachment being active. A client processing responses can therefore believe the final successful attach is streaming even though the later detach already cancelled it; serialize these operations per connection or return a cancellation/conflict response for the superseded attach.
Useful? React with 👍 / 👎.
task.attach, task.detach and task.output now carry agent_id. Subagents
can own background shells, so the server resolves the task inside that
agent instead of always looking in the main agent.
Attach and detach run through the connection's control queue, so a
detach sent while an attach is still reading the output tail is applied
after that attach replies, and replies arrive in send order.
GET /sessions/{id}/tasks/{task_id} drops with_output / output_bytes and
the output_preview / output_bytes task fields; task output is read over
task.attach only.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80e87c5f0a
ℹ️ 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".
| } | ||
|
|
||
| reply.send(okEnvelope(toWireTask(session_id, found, output), req.id)); | ||
| reply.send(okEnvelope(toWireTask(session_id, found), req.id)); |
There was a problem hiding this comment.
Preserve output in the existing REST task response
When an existing REST client requests ?with_output=true&output_bytes=..., this branch now returns a successful metadata-only task because the removed query schema lets those parameters be ignored. The endpoint previously returned output_preview and output_bytes, and task.attach is not a compatible fallback for REST-only clients, so upgrades silently discard requested output even though the changeset only declares removing tasks from transcript endpoints. Keep the existing query and response fields during the compatibility window, or version the endpoint as a breaking change.
AGENTS.md reference: AGENTS.md:L111-L117
Useful? React with 👍 / 👎.
…hes per task task.attach / task.detach become task.watch / task.unwatch. Watch and unwatch of one task still run in arrival order, but different tasks and the other control frames no longer wait on each other. task.watch takes an optional tail_bytes (32 KiB by default, at most 1 MiB) for the first chunk.
# Conflicts: # apps/kimi-inspect/src/audit/serialize.ts # apps/kimi-inspect/src/components/ChatView.tsx
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72b56454bc
ℹ️ 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".
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Send task state over the server's WebSocket as a flat `task` frame instead of inside the transcript, and add `task.attach` / `task.detach` to stream a task's output as `task.output` frames; transcript frames and the REST transcript endpoints no longer carry tasks or task references, and their `tasks` field is removed. |
There was a problem hiding this comment.
Rename the controls in the changeset
When this changeset is published, the generated changelog will tell users to send task.attach and task.detach, but the committed protocol only accepts task.watch and task.unwatch. Users following the release note will therefore receive no task output; update this sentence to use the actual wire names.
AGENTS.md reference: AGENTS.md:L89-L90
Useful? React with 👍 / 👎.
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! 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". |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 55f80cd6b5
ℹ️ 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".
| @@ -136,6 +146,7 @@ export class TranscriptService { | |||
| this.deps.logger, | |||
| (event) => this.handleLiveOps(sessionId, event), | |||
| (agentId) => this.rebuildAfterUndo(sessionId, agentId), | |||
There was a problem hiding this comment.
Rebuild the task cache after undo
When :undo removes the turn that created a shell or subagent task, this callback rebuilds only the transcript store; the new SessionTasks instance is never cleared or reloaded. The old reset path replaced snapshot.tasks, but the replacement protocol has no task-deletion frame, so existing subscribers and later subscription baselines keep the undone task until the live session is dropped. Fresh evidence in this version is that rebuildAfterUndo() ends by dispatching transcript ops while leaving entry.tasks untouched; rebuild the per-agent task state during undo reconciliation.
AGENTS.md reference: AGENTS.md:L126-L128
Useful? React with 👍 / 👎.
…ut watch Send every subscribed agent's tasks once subscribe_v2 completes instead of only after a transcript reset or catch-up. A task change that landed while a re-subscribe raised another agent's grade was dropped for agents whose grade did not change, with no seq to reveal the gap. Tasks no longer depend on whether the transcript was reset. Load the task history of an agent that joins the roster, so its earlier tasks reach subscribers even if it emits no new task event. For task.watch: skip watches still queued when the connection closes, reply 50001 and release the watch when streaming setup throws, reply success only once the first chunk is ready, and forward the unread rest of a chunk the tail read caught mid-write instead of dropping it. Read the output tail by range instead of loading the whole output file.
…/agent-state-events
Requirement or Bug
任务从 transcript 里拿出来,改成独立的 WebSocket
task帧;任务输出改为用task.watch按任务订阅推送,客户端不再轮询,REST 也不再返回任务输出。客户端配套:MoonshotAI/kimi-code-app#967。本 PR 先合,code-app 那边再把 submodule 指到合并提交。
Bug Reproduction Steps
N/A
Root Cause
N/A
Code Changes
协议形状参考 KAP 草案 的「任务」和「任务输出」两节,与草案不一致的地方以本 PR 为准,草案随后更新。改动分两块:任务状态改由 kap-server 新增的
SessionTasks维护并通过task帧发送,transcript 层不再有任务;新增task.watch/task.unwatch推送任务输出,REST 任务详情不再返回输出。源码(不含测试和文档)合计 +944 / −869 行:
sessionTasks.ts295 行和task帧的 schema,删掉 transcript 包的任务模型、coreEventMap.ts的任务投影等 820 行。taskOutputStream.ts233 行、帧 schema 和引擎侧的两个成员。一、任务状态:
SessionTasks+task帧帧的形状:
(session_id, agent_id, task_id)整条覆盖。phase只出现在运行中的 subagent 上,有state_reason时是suspended,否则是working。blocked、phase: "queued"、has_output不发送。任务状态从哪来:每个活跃会话一个
SessionTasks,按 agent 存一张任务表,表里的值就是去掉信封字段的task帧。引擎实时事件和磁盘上的 wire 记录形状相同({ type, time, ...payload }),走同一套折叠逻辑:发送时机:
off的 agent 不发。subscribe_v2完成后都发全部任务,不管这次有没有 reset 或补拉。订阅期间连接暂不接收实时变化,最后这一轮补发保证再次订阅(例如调高某个 subagent 的粒度)时,其他 agent 在这段时间里的任务变化不会丢。transcript.ops里撤销产生的reset操作不影响任务。transcript.ops批次不再产生,seq 也不再因此递增。transcript 层删掉的东西:
agent-core-v2:
task.terminated记录不再写outputTail(wire-manifest.d.ts同步删掉这个字段),旧会话里带这个字段的记录照常回放。任务状态这块没有其他引擎改动。二、任务输出:
task.watch/task.unwatch改动前 desktop 和 web 只要会话里有任务在跑,就每秒对每个运行中的任务请求一次
GET /sessions/{session_id}/tasks/{task_id}?with_output=true&output_bytes=4096,不管任务面板有没有打开。这块给 v1 WebSocket 加一条按任务订阅输出的通道,并删掉 REST 的这条输出路径:offset是text在任务完整输出中的 UTF-8 字节位置,前后两条连续。agent_id是任务所属的 agent,也就是该任务task帧上的agent_id,主 agent 是main。subagent 也能起后台 shell,服务端只在agent_id这个 agent 里找任务,传错 agent 回复 40406。response,再发首段:输出的最后至多tail_bytes字节(默认 32 KiB,最大 1 MiB);since_offset落在这段范围内且在字符边界上时,只发它之后的部分;早于这段的起点、晚于末尾或落在多字节字符中间时发整段。首段两端都对齐到字符边界,读文件时碰上写到一半的字符留给下一段;没有可发送的输出时首段text为空。之后每段新输出推一条。since_offset重新 watch。session_id/agent_id/task_id、tail_bytes超出范围)回复 40001;查找任务或建立推送时出错回复 50001,读输出失败回复 50003,失败时这次 watch 不保留;读不出request_id时以关闭码 1008 断开连接。GET /sessions/{id}/tasks/{task_id}不再读with_output/output_bytes(传了也忽略),返回的任务上没有output_preview/output_bytes;任务输出只能通过task.watch读取。引擎侧给任务服务加了两个成员:
onDidAppendOutput的offset取追加前的outputSizeBytes,与输出文件的字节位置一致;只在这段输出被保留时触发,超过输出上限后不再触发。readOutputTail返回原始字节和起点:输出已写到磁盘时按范围只读文件末尾,否则读内存里保留的部分。不复用getOutputSnapshot,因为它先解码成字符串,切到多字节字符中间会出现替换字符。watch 的顺序:
task.watch/task.unwatch不进连接的 control 队列,而是每个任务各排一队:同一个任务的请求按到达顺序执行,watch 还没回复时发来的 unwatch 在它回复之后生效;不同任务之间、以及它们和订阅等其他控制帧之间互不等待,冷会话 resume 慢也挡不住别的请求。文件
另外:
docs/{en,zh}/reference/server-api.md补上task帧、task.watch/task.unwatch/task.output,REST transcript 去掉tasks,REST 任务详情去掉输出参数和字段。task-ws-events.md写任务状态改走task帧、transcript 不再带任务;task-output-watch.md写任务输出改走task.watch、REST 任务详情不再返回输出。Behavior Changes and Affected Users
transcript.reset的snapshot.taskstask帧发送task帧);直接连 WebSocket 的外部脚本task帧transcript.ops里的task.upserttask帧GET /sessions/{id}/transcript的tasks、/transcript/ops补拉里的任务操作task.upserttasks字段删除,补拉结果里没有任务操作tasks是必填字段,缺了会解析失败;用 REST 读 transcript 任务的外部脚本GET /sessions/{id}/tasksGET /sessions/{id}/tasks/{task_id}的with_output/output_bytesoutput_preview/output_bytestask.watchtaskreftaskref;直接解析 transcript 的外部脚本TranscriptTask.outputTail与任务输出的append操作task.watch推送task.watch);外部脚本task.watchlosttask帧里是status: "ended", outcome: "lost";判定条件不变:子 agent 没在运行,且不是父 agent 当前 turn 里发起的task帧off的连接收到task.watch/task.unwatchresponserequest_id的task.watch/task.unwatchtask.terminated记录outputTailIAgentTaskService接口onDidAppendOutput、readOutputTail旧路径做的事和它们在新路径里的位置:
task.started/task.terminated→ 任务行:kind 映射(process → shell,agent → subagent,其他 → other)、状态、detached、command、parentToolCallId、model / thinkingfoldTaskInfosubagent.spawned:只收父 agent 是自己的;带 taskId 时记下子 agent → taskId,不带时按子 agent id 建行并删掉旧映射foldSubagentSpawnedsubagent.started/suspended/completed/failed/cancelled:同时更新 taskId 行和子 agent id 行;cancelled → killed;已结束的行再收到非终止事件时开始新一轮,清掉上一轮的结果、用量和错误foldSubagentRunshell.started/shell.completed:commandId → taskId,isError→ failed;shell.output不处理foldShellusageOf/text/isoTimeIAgentTaskService.list()补种运行中的 subagent 任务task.started都在记录里lostSessionTasks读完记录后按同一条件判定outputTail)task.watchwith_output/output_bytes返回输出末尾(默认 32 KiB)task.watch的首段(tail_bytes,默认 32 KiB),之后实时推送旧客户端连新服务端的情况:desktop 内嵌的 server 随安装包发布,不会出现版本错配。web 产物由同一个 server 提供:本 PR 合入之后、#4487 合入之前,main 上内置的旧 Web 产物加载会话时 REST transcript 解析失败,会话内容显示不出来,任务详情也读不到输出,所以 #4487 要紧接着合入,这段时间不要发版。删掉的几项直接替换,不保留兼容层,与 #4428、#4470 的处理方式一致。
没有变化的部分:REST
/tasks列表、任务详情的其他字段、取消和转后台接口、subscribe_v2的粒度语义、interaction帧、其他 transcript 操作和 v1 现有帧。受影响模块与测试覆盖:
kap-server/src/services/transcript:test/services/transcript.test.ts的SessionTasks用例覆盖:process 任务与它的 shell 命令合成一行;前台 shell 结束、漏掉开始事件时按事件里的 taskId 建行;swarm 成员按 agent id 跟踪挂起、失败和用量;未知 kind 归为 other、畸形记录跳过;Agent 工具发起的 subagent 按 taskId 建行并折叠生命周期;子 agent 不带 taskId 重新发起时丢掉旧映射;已结束的 subagent 被重新拉起时两行都开始新一轮、清掉终止时间;运行中重复的subagent.started不改开始时间;从task.started恢复子 agent 与 taskId 的对应;wire 记录只折叠一次、读完后报告全部任务;中断的前台 subagent 标为 lost,子 agent 仍在运行或父 agent 当前 turn 发起的除外,server 在 turn 中途崩溃、重启后父 agent 已开始新 turn 时,崩溃那一轮发起的也标为 lost;读记录期间到达的实时事件排在记录之后。SessionTasks,以及任务事件交给任务监听方而不进 journal。kap-server/src/transport/ws/v1:test/sessionEventBroadcaster.test.ts覆盖订阅后发送全部任务、每次变化发送变化的任务、reset 快照里没有tasks;再次订阅调高某个 subagent 粒度期间主 agent 的任务结束,最后的补发带上结束状态;agent 加入 roster 时读取它的任务记录;test/wsConnectionV1.test.ts覆盖回复与首段顺序、实时推送、since_offset在末尾内 / 已在末尾 / 过旧或落在字符中间时回退并对齐字符边界、没有输出时发空首段、读末尾期间到达的输出不重复、读末尾时碰上写到一半的块(首段停在完整字符处,剩余部分补发)、unwatch、watch 未回复时发来的 unwatch 在它之后生效、慢的 watch 不挡其他任务、tail_bytes限制首段、subagent 的任务按它的 agent id 推送、错误码(会话或任务不存在;空 ID、缺agent_id、tail_bytes超范围回 40001;查找出错或建立推送时出错 50001 并释放;读输出失败 50003 并释放)、1008 断开、连接关闭时释放、关闭后排队中的 watch 不再查找任务;test/transcriptContract.e2e.test.ts用真实服务端覆盖:后台 subagent 的task帧,且 transcript 推送里没有任务(S4);REST 快照和 WS reset 里都没有tasks(S6);task.watch推送运行中任务的输出、重连后带since_offset续传,subagent 的任务要带它自己的 agent id 才能 watch(S9)。kap-server/src/routes/tasks.ts:test/tasks.test.ts删掉with_output输出预览的用例。packages/transcript:test/layers.test.ts、test/store.test.ts删掉任务、taskref和任务输出追加的用例。agent-core-v2/src/agent/task:test/agent/task/output-access.test.ts覆盖多字节字符下 offset 按字节连续、已落盘输出的末尾与起点、未落盘时读内存末尾、未知任务;test/agent/task/taskManager.test.ts覆盖超过输出上限后推送的末尾不超过上限,并与已保存输出的末尾一致;taskOps.test.ts/taskService.test.ts删掉outputTail的用例。本地 kap-server、transcript、agent-core-v2 的
tsc和pnpm lint通过;kap-server、transcript 的测试全部通过(73 个文件、1571 条),agent-core-v2 的任务测试全部通过(14 个文件、241 条)。agent-core-v2 全量测试本地有 2 条超时(tool.test.ts的前台 subagent 超时用例、tower/store.test.ts的状态锁用例),把本 PR 对agent/task的改动撤回后同样超时,CI 上通过,与本 PR 无关。改名之后,在 #967 分支的 dev desktop 上验证过(内嵌 server 是本分支的
72b56454b):已结束的 bash 任务打开详情面板后通过task.watch显示完整的最终输出;用脚本按客户端的帧格式直连同一个 server,task.watch回复 0 并收到带agent_id: "main"的首段,task.unwatch回复 0,传错agent_id回复 40406,REST 任务详情带with_output时不再返回输出字段,旧的task.attach不回复。改名之前,还用本分支的 server 加 #967 分支的 web 手动验证过运行中任务的完整流程:后台 bash 任务出现在任务栏并显示命令;详情面板实时追加输出,结束后保留最终输出;停止任务后显示已取消;前台 subagent 只出现一次;刷新页面后任务状态和输出都在。再强杀 server 并重启(当时还有一个
sleep 600在跑),刷新后已结束的两个任务原样恢复,sleep 600显示为失败(服务端记为lost),没有重复行,控制台没有报错。Checklist
/approve).gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.