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 docs/design/ctrl-o-detail-expand/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ claude code 的机制是"**存储层保留完整、显示层按 `verbose` 截断
> - **新增** core helper `packages/core/src/utils/generateContentResponseUtilities.ts` 的 `getToolResponseDisplayText(parts)`(读 `functionResponse.response.output` + 遍历 nested `functionResponse.parts` 媒体占位、空/缺失返回 `undefined`;**不二次截断**;规则见 §4.9 改动点 1);
> - **改** `packages/cli/src/ui/types.ts`:`IndividualToolCallDisplay` 加 `detailedDisplay?: string`(派生、不持久化);
> - **改** `packages/cli/src/ui/hooks/useReactToolScheduler.ts`(live 提取,`success` 分支派生 `detailedDisplay`)、`packages/cli/src/ui/utils/resumeHistoryUtils.ts`(resume 提取,`tool_result` 分支从 `responseParts ?? message.parts` 派生)、`packages/cli/src/ui/components/messages/ToolMessage.tsx` + `ToolGroupMessage.tsx`(渲染拆分:`ToolGroupMessage` 下传 `fullDetail`,`ToolMessage` 仅 `fullDetail && isCollapsibleTool && detailedDisplay` 切数据源);
> - **不改** `packages/cli/src/acp-integration/session/HistoryReplayer.ts` / `emitters/ToolCallEmitter.ts`——ACP `content[]` 已含完整 `output`(见上表),TUI transcript 不经此路,无需改动;
> - **不改** `packages/cli/src/acp-integration/session/history-replayer.ts` / `emitters/tool-call-emitter.ts`——ACP `content[]` 已含完整 `output`(见上表),TUI transcript 不经此路,无需改动;
> - **不改** 持久化 schema(`serializeToolResponse` / `chatRecordingService` / ACP 协议字段)——完整明细已天然存于 `responseParts`,新字段为派生值。

### C. 新增
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Client 点击时打开 URL;Daemon 不读取、不验证、不预渲染该 URL
- `packages/core/src/tools/tool-names.ts`
- `packages/core/src/tools/artifact/artifact-tool.ts`
- `packages/cli/src/acp-integration/session/Session.ts`
- `packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts`
- `packages/cli/src/acp-integration/session/emitters/tool-call-emitter.ts`

现状:

Expand Down Expand Up @@ -1005,7 +1005,7 @@ Phase A 先接入 `ToolResult.artifacts` 和 `ArtifactTool`;`record_artifact`

- `packages/cli/src/acp-integration/session/types.ts`
- `ToolCallResultParams.artifacts?`
- `packages/cli/src/acp-integration/session/emitters/ToolCallEmitter.ts`
- `packages/cli/src/acp-integration/session/emitters/tool-call-emitter.ts`
- `_meta.artifacts = params.artifacts`
- `packages/cli/src/acp-integration/session/Session.ts`
- 工具成功后收集 `toolResult.artifacts`。
Expand Down Expand Up @@ -1338,7 +1338,7 @@ cd packages/core && npx vitest run src/tools/artifact/artifact-tool.test.ts
命令:

```bash
cd packages/cli && npx vitest run src/acp-integration/session/emitters/ToolCallEmitter.test.ts
cd packages/cli && npx vitest run src/acp-integration/session/emitters/tool-call-emitter.test.ts
cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts
```

Expand Down
47 changes: 47 additions & 0 deletions docs/design/untrusted-persisted-transcript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Untrusted persisted transcript reader

## Context

The daemon already exposes persisted-only session and group catalogs for registered untrusted secondary workspaces. Transcript paging remains unavailable because the legacy `GET /session/:id/transcript` route resolves a session owner through live bridge state and delegates replay to an ACP child. That path can start a process, load workspace settings and project-defined capabilities, and create or repair a persisted cursor signing key.

This design adds a separate workspace-qualified REST contract that reads only an active persisted transcript. The legacy route and its ACP-backed behavior remain unchanged.

## Contract

The daemon advertises the unconditional `workspace_persisted_transcript` capability and serves:

`GET /workspaces/:workspace/session/:id/transcript?cursor=<opaque>&limit=<1..500>`

The workspace selector resolves an exact registered workspace id first, then a URL-decoded portable absolute cwd. Unknown or unregistered selectors fail closed with `workspace_mismatch`. Trusted primary and secondary workspaces and untrusted secondary workspaces may read. An untrusted primary remains rejected to preserve the existing plural-route safe-mode boundary.

Only active persisted JSONL is read. Archived sessions return `session_archived`; active/archive conflicts return `session_conflict`; missing sessions return 404. The response reuses `DaemonSessionTranscriptPage` and contains id-less `session_update` frames produced by the existing visible history projection. Raw JSONL and hidden system records are never returned.

## Security boundary

The route must not call a bridge method, start or preheat ACP, load settings, parse agents or skills, discover tools, run external commands, create a persisted cursor key, or write a route-specific daemon/debug log. Existing daemon-wide HTTP access logging and telemetry remain outside this route-specific no-write guarantee.

Direct persisted transcript reads run with debug session logging suppressed. The archive coordinator holds a shared session lock across location checks, index construction, record reads, session-id validation and replay. Every returned record must carry the requested session id; a mismatch fails closed as a snapshot conflict.

The direct replay context contains only a session id, an update sink, optional message rewriting, and optional cumulative usage. Tool display metadata falls back to persisted tool name and description when no full `Config` is present. No project-controlled tool registry is consulted.

## Cursor lifecycle

The core reader accepts an explicit cursor codec. Existing callers continue to use the file-backed codec. The daemon creates one random master key per process, derives a separate 32-byte HMAC key from workspace id and canonical cwd, and caches an in-memory codec per registered runtime. Cursors from the new route therefore expire on daemon restart and cannot be replayed across workspaces.

The cursor freezes file identity, byte size, active leaf, replay position and replay state. Appends after page one do not change the snapshot. Delete, archive, truncate, replacement, leaf change, or session-id mismatch returns `transcript_snapshot_unavailable`.

## Failure and race handling

For the first page, the route checks active/archive location but still attempts the active reader when location is unknown so malformed active storage is not silently reported as missing. For cursor pages it validates only the frozen active snapshot. On `ENOENT`, a cursor page returns snapshot unavailable; a first page rechecks location to detect an archive race before returning missing.

Replay is page-transactional at the protocol boundary: updates emitted before a replay conversion failure are returned with `partial: true`, a generic replay error and no next cursor. Pending tool calls and cumulative usage are carried in the signed cursor only after successful replay.

## Compatibility and limits

The old singular route, persisted cursor key and ACP error mapping remain intact. The new route keeps the existing default page size of 100, maximum of 500, 256 MiB snapshot cap, 32-entry/64 MiB index cache and five-minute cache lifetime. The first index scan remains linear in the frozen snapshot size.

The TypeScript SDK exposes the method on `WorkspaceDaemonClient`. It forces native REST transport and has no ACP route mapping. Older daemons can be detected through the new capability and continue returning 404 for the route.

## Deferred work

Archived transcript reading, reverse/tail pagination, live follow, automatic full-history loading, worker-thread indexing, cross-restart cursors, and the Web Shell viewer are separate follow-ups.
Loading
Loading