@cortexkit/opencode-magic-context 0.15.7 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -16
- package/dist/agents/magic-context-prompt.d.ts +2 -13
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +67 -4
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compaction.d.ts +1 -1
- package/dist/features/magic-context/compaction.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/compression-depth-storage.d.ts +1 -1
- package/dist/features/magic-context/compression-depth-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/queue.d.ts +8 -3
- package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/runner.d.ts +1 -1
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/scheduler.d.ts +1 -1
- package/dist/features/magic-context/dreamer/scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-state.d.ts +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-state.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/indexer.d.ts +1 -1
- package/dist/features/magic-context/git-commits/indexer.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/search-git-commits.d.ts +1 -1
- package/dist/features/magic-context/git-commits/search-git-commits.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commit-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commits.d.ts +1 -1
- package/dist/features/magic-context/git-commits/storage-git-commits.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/identify-key-files.d.ts +1 -1
- package/dist/features/magic-context/key-files/identify-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/read-stats.d.ts +1 -1
- package/dist/features/magic-context/key-files/read-stats.d.ts.map +1 -1
- package/dist/features/magic-context/key-files/storage-key-files.d.ts +1 -1
- package/dist/features/magic-context/key-files/storage-key-files.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-backfill.d.ts +1 -1
- package/dist/features/magic-context/memory/embedding-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-cache.d.ts +1 -1
- package/dist/features/magic-context/memory/embedding-cache.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/normalize-hash.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/promotion.d.ts +1 -1
- package/dist/features/magic-context/memory/promotion.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts +1 -1
- package/dist/features/magic-context/memory/storage-memory-embeddings.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts +1 -1
- package/dist/features/magic-context/memory/storage-memory-fts.d.ts.map +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts +1 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +1 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +1 -1
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/mock-database.d.ts +1 -1
- package/dist/features/magic-context/mock-database.d.ts.map +1 -1
- package/dist/features/magic-context/plugin-messages.d.ts +1 -1
- package/dist/features/magic-context/plugin-messages.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts +1 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/sidekick/agent.d.ts +2 -1
- package/dist/features/magic-context/sidekick/agent.d.ts.map +1 -1
- package/dist/features/magic-context/sidekick/core.d.ts +38 -0
- package/dist/features/magic-context/sidekick/core.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +20 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts +1 -1
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-notes.d.ts +1 -1
- package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
- package/dist/features/magic-context/storage-ops.d.ts +1 -1
- package/dist/features/magic-context/storage-ops.d.ts.map +1 -1
- package/dist/features/magic-context/storage-source.d.ts +1 -1
- package/dist/features/magic-context/storage-source.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +17 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +1 -1
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts +1 -1
- package/dist/features/magic-context/user-memory/review-user-memories.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-hint.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts +1 -1
- package/dist/hooks/magic-context/compaction-marker-manager.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-runner-drop-queue.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts +1 -1
- package/dist/hooks/magic-context/compartment-trigger.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-flush.d.ts +1 -1
- package/dist/hooks/magic-context/execute-flush.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-state-file.d.ts +29 -0
- package/dist/hooks/magic-context/historian-state-file.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/note-nudger.d.ts +1 -1
- package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
- package/dist/hooks/magic-context/nudge-placement-store.d.ts +1 -1
- package/dist/hooks/magic-context/nudge-placement-store.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +39 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/send-session-notification.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts +6 -5
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.js +8284 -8166
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts +1 -1
- package/dist/plugin/rpc-handlers.d.ts +4 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/shared/conflict-detector.d.ts.map +1 -1
- package/dist/shared/data-path.d.ts +22 -0
- package/dist/shared/data-path.d.ts.map +1 -1
- package/dist/shared/harness.d.ts +43 -0
- package/dist/shared/harness.d.ts.map +1 -0
- package/dist/shared/rpc-notifications.d.ts +4 -2
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/sqlite-helpers.d.ts +16 -0
- package/dist/shared/sqlite-helpers.d.ts.map +1 -0
- package/dist/shared/sqlite.d.ts +55 -0
- package/dist/shared/sqlite.d.ts.map +1 -0
- package/dist/shared/subagent-runner.d.ts +202 -0
- package/dist/shared/subagent-runner.d.ts.map +1 -0
- package/dist/shared/tag-transcript.d.ts +66 -0
- package/dist/shared/tag-transcript.d.ts.map +1 -0
- package/dist/shared/transcript-opencode.d.ts +71 -0
- package/dist/shared/transcript-opencode.d.ts.map +1 -0
- package/dist/shared/transcript.d.ts +212 -0
- package/dist/shared/transcript.d.ts.map +1 -0
- package/dist/shared/tui-config.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/types.d.ts +13 -2
- package/dist/tools/ctx-memory/types.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts +8 -2
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/types.d.ts +8 -2
- package/dist/tools/ctx-search/types.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +73 -75
- package/src/shared/conflict-detector.test.ts +44 -1
- package/src/shared/conflict-detector.ts +24 -8
- package/src/shared/data-path.test.ts +53 -1
- package/src/shared/data-path.ts +28 -0
- package/src/shared/harness.ts +61 -0
- package/src/shared/rpc-notifications.ts +11 -5
- package/src/shared/sqlite-helpers.ts +27 -0
- package/src/shared/sqlite.ts +91 -0
- package/src/shared/subagent-runner.ts +206 -0
- package/src/shared/tag-transcript.ts +541 -0
- package/src/shared/transcript-opencode.ts +259 -0
- package/src/shared/transcript.ts +226 -0
- package/src/shared/tui-config.ts +34 -8
- package/src/tui/data/context-db.ts +5 -1
- package/dist/cli/config-paths.d.ts +0 -11
- package/dist/cli/config-paths.d.ts.map +0 -1
- package/dist/cli/diagnostics.d.ts +0 -82
- package/dist/cli/diagnostics.d.ts.map +0 -1
- package/dist/cli/doctor.d.ts +0 -5
- package/dist/cli/doctor.d.ts.map +0 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/logs.d.ts +0 -22
- package/dist/cli/logs.d.ts.map +0 -1
- package/dist/cli/opencode-helpers.d.ts +0 -19
- package/dist/cli/opencode-helpers.d.ts.map +0 -1
- package/dist/cli/prompts.d.ts +0 -14
- package/dist/cli/prompts.d.ts.map +0 -1
- package/dist/cli/setup.d.ts +0 -2
- package/dist/cli/setup.d.ts.map +0 -1
- package/dist/cli.js +0 -11287
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-agnostic tagging over the Transcript interface.
|
|
3
|
+
*
|
|
4
|
+
* This is a deliberately minimal alternative to the OpenCode-specific
|
|
5
|
+
* `tag-messages.ts` that operates on `MessageLike[]`. The OpenCode flow
|
|
6
|
+
* carries 380+ lines of accumulated complexity:
|
|
7
|
+
*
|
|
8
|
+
* - source-content persistence (for cross-pass detag/restore behavior),
|
|
9
|
+
* - tool-call indexing across separate "tool" and "tool_result" parts,
|
|
10
|
+
* - reasoning-byte tracking for historian projection,
|
|
11
|
+
* - file-part stable IDs,
|
|
12
|
+
* - existing-tag resolver with content-id fallback.
|
|
13
|
+
*
|
|
14
|
+
* Most of that is OpenCode-specific (cache stability across multi-pass
|
|
15
|
+
* transforms, AI SDK part-id semantics, file part shapes). Pi's
|
|
16
|
+
* `pi.on("context", ...)` fires once per LLM call with a complete
|
|
17
|
+
* `AgentMessage[]`, so we can use a simpler tagging contract:
|
|
18
|
+
*
|
|
19
|
+
* 1. Walk the transcript in order.
|
|
20
|
+
* 2. For each tag-eligible part (text, tool_use, tool_result), assign
|
|
21
|
+
* a tag number via the shared `Tagger`.
|
|
22
|
+
* 3. Inject `§N§ ` prefix into the visible text (unless skipped).
|
|
23
|
+
* 4. Build a `TagTarget` so `applyPendingOperations` from
|
|
24
|
+
* `apply-operations.ts` can replace this part with a sentinel when
|
|
25
|
+
* a queued drop fires.
|
|
26
|
+
*
|
|
27
|
+
* Tool drops aggregate by call_id across both invocation and result
|
|
28
|
+
* occurrences (mirrors OpenCode tag-messages.ts:196-220). When a drop
|
|
29
|
+
* fires for a tool tag, BOTH the assistant `toolCall`/`tool_use` part
|
|
30
|
+
* and the user `toolResult`/`tool_result` part are mutated together so
|
|
31
|
+
* the LLM sees consistent dropped state. Without this aggregation:
|
|
32
|
+
*
|
|
33
|
+
* - Tool tag byte_size reflects only the args (~58 bytes for a `read`)
|
|
34
|
+
* because the FIRST occurrence (invocation) is tagged first and
|
|
35
|
+
* `assignTag` short-circuits the SECOND occurrence (result, ~4KB)
|
|
36
|
+
* to the same tag without updating byte_size.
|
|
37
|
+
* - Drops touch only the second occurrence (last write wins on
|
|
38
|
+
* `targets.set`), leaving the first in original form.
|
|
39
|
+
*
|
|
40
|
+
* Reuses unchanged from the OpenCode path:
|
|
41
|
+
*
|
|
42
|
+
* - `Tagger` (DB-backed counter + assignment store).
|
|
43
|
+
* - `applyPendingOperations` (operates on `Map<number, TagTarget>`).
|
|
44
|
+
* - `applyFlushedStatuses` (same).
|
|
45
|
+
* - Tag prefix primitives (`prependTag`, `stripTagPrefix`, `byteSize`).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import type { ContextDatabase } from "../features/magic-context/storage";
|
|
49
|
+
import { saveSourceContent } from "../features/magic-context/storage-source";
|
|
50
|
+
import { updateTagByteSize, updateTagInputByteSize } from "../features/magic-context/storage-tags";
|
|
51
|
+
import type { Tagger } from "../features/magic-context/tagger";
|
|
52
|
+
import {
|
|
53
|
+
byteSize,
|
|
54
|
+
prependTag,
|
|
55
|
+
stripTagPrefix,
|
|
56
|
+
} from "../hooks/magic-context/tag-content-primitives";
|
|
57
|
+
import type { TagTarget } from "../hooks/magic-context/tag-messages";
|
|
58
|
+
import type { Transcript, TranscriptPart } from "./transcript";
|
|
59
|
+
|
|
60
|
+
export interface TagTranscriptOptions {
|
|
61
|
+
/**
|
|
62
|
+
* When true, skip injecting `§N§` prefix into visible text. Tags
|
|
63
|
+
* still get assigned in the DB so historian/drops can reference
|
|
64
|
+
* them; the agent just doesn't see the markers. Used when
|
|
65
|
+
* `ctx_reduce_enabled: false` (agent has no `ctx_reduce` tool to
|
|
66
|
+
* act on the markers). Cache-safe because skip behavior is
|
|
67
|
+
* consistent across passes.
|
|
68
|
+
*/
|
|
69
|
+
skipPrefixInjection?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface TagTranscriptResult {
|
|
73
|
+
targets: Map<number, TagTarget>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Tag eligible parts of a transcript and build TagTargets for them.
|
|
78
|
+
*
|
|
79
|
+
* "Eligible" means: parts that contribute meaningfully to the LLM input
|
|
80
|
+
* and whose content can be replaced when dropped. Specifically:
|
|
81
|
+
*
|
|
82
|
+
* - text parts (user or assistant): tagged as type "message", inject
|
|
83
|
+
* prefix into the visible text, target supports setContent.
|
|
84
|
+
* - thinking parts: NOT tagged. Reasoning content has provider-
|
|
85
|
+
* specific signed-content semantics (Anthropic redacted_thinking,
|
|
86
|
+
* etc.) and replacing them mid-conversation breaks signature
|
|
87
|
+
* verification. The historian's clear-reasoning pass handles them
|
|
88
|
+
* separately if needed.
|
|
89
|
+
* - tool_use parts (assistant tool invocations): tagged as type
|
|
90
|
+
* "tool", target supports drop/truncate via the tag-content
|
|
91
|
+
* primitives.
|
|
92
|
+
* - tool_result parts (folded into user messages by the Pi adapter):
|
|
93
|
+
* tagged as type "tool", paired with the corresponding invocation
|
|
94
|
+
* for full-pair drops.
|
|
95
|
+
* - image, file, structural, unknown: skipped.
|
|
96
|
+
*
|
|
97
|
+
* The contentId we pass to the tagger uses the part's stable id when
|
|
98
|
+
* available, otherwise a synthetic locator. Pi's adapter exposes:
|
|
99
|
+
* - tool_use parts: id = ToolCall.id (from pi-ai)
|
|
100
|
+
* - tool_result parts: id = ToolResultMessage.toolCallId
|
|
101
|
+
* - text parts: id = undefined → we synthesize from message+ordinal
|
|
102
|
+
*/
|
|
103
|
+
/**
|
|
104
|
+
* Per-callId aggregation of tool occurrences across the transcript.
|
|
105
|
+
* Built up during the walk and used to:
|
|
106
|
+
* 1. Assign one tag per call_id with byte_size from the LARGEST
|
|
107
|
+
* occurrence (typically the result, ~4KB) instead of the args
|
|
108
|
+
* (~58 bytes). Without this, drop projection underestimates
|
|
109
|
+
* reclaimable bytes by ~70× per tool.
|
|
110
|
+
* 2. Build a single aggregate TagTarget that mutates BOTH the
|
|
111
|
+
* invocation and result occurrences atomically, so a queued drop
|
|
112
|
+
* replaces both halves with a sentinel instead of last-write-wins.
|
|
113
|
+
*/
|
|
114
|
+
interface ToolOccurrence {
|
|
115
|
+
message: { info: { id?: string; role: string } };
|
|
116
|
+
part: TranscriptPart;
|
|
117
|
+
kind: "tool_use" | "tool_result";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface ToolAggregate {
|
|
121
|
+
callId: string;
|
|
122
|
+
occurrences: ToolOccurrence[];
|
|
123
|
+
/** Largest byteSize seen across occurrences — used as the tag size. */
|
|
124
|
+
maxByteSize: number;
|
|
125
|
+
/** Tool name from the first occurrence we see one on. */
|
|
126
|
+
toolName: string | null;
|
|
127
|
+
/** Input byte size from the invocation occurrence (for storage projection). */
|
|
128
|
+
inputByteSize: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function tagTranscript(
|
|
132
|
+
sessionId: string,
|
|
133
|
+
transcript: Transcript,
|
|
134
|
+
tagger: Tagger,
|
|
135
|
+
db: ContextDatabase,
|
|
136
|
+
options: TagTranscriptOptions = {},
|
|
137
|
+
): TagTranscriptResult {
|
|
138
|
+
const skipPrefixInjection = options.skipPrefixInjection === true;
|
|
139
|
+
const targets = new Map<number, TagTarget>();
|
|
140
|
+
|
|
141
|
+
// Per-callId tool aggregation tracked across the single walk. Tags
|
|
142
|
+
// get allocated in transcript order (first occurrence reserves the
|
|
143
|
+
// tag number); subsequent occurrences reuse the same tag and merge
|
|
144
|
+
// their occurrence into the aggregate's TagTarget. This preserves
|
|
145
|
+
// chronological tag numbering while still aggregating drop behavior
|
|
146
|
+
// across both invocation and result.
|
|
147
|
+
const toolAggregates = new Map<string, ToolAggregate & { tagId: number }>();
|
|
148
|
+
|
|
149
|
+
db.transaction(() => {
|
|
150
|
+
for (let msgIndex = 0; msgIndex < transcript.messages.length; msgIndex += 1) {
|
|
151
|
+
const message = transcript.messages[msgIndex];
|
|
152
|
+
if (message === undefined) continue;
|
|
153
|
+
const messageId = message.info.id;
|
|
154
|
+
|
|
155
|
+
let textOrdinal = 0;
|
|
156
|
+
|
|
157
|
+
for (let partIndex = 0; partIndex < message.parts.length; partIndex += 1) {
|
|
158
|
+
const part = message.parts[partIndex];
|
|
159
|
+
if (part === undefined) continue;
|
|
160
|
+
|
|
161
|
+
if (part.kind === "text") {
|
|
162
|
+
// Synthetic message ids (Pi tail synthetic user with
|
|
163
|
+
// no id) cannot be tagged — there's no stable handle
|
|
164
|
+
// to bind a tag to across passes. Pass through
|
|
165
|
+
// untagged; this is rare (only happens for the
|
|
166
|
+
// dangling tool-result tail case in Pi).
|
|
167
|
+
if (messageId === undefined) {
|
|
168
|
+
textOrdinal += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
tagTextPart({
|
|
172
|
+
sessionId,
|
|
173
|
+
message,
|
|
174
|
+
messageId,
|
|
175
|
+
msgIndex,
|
|
176
|
+
textOrdinal,
|
|
177
|
+
part,
|
|
178
|
+
tagger,
|
|
179
|
+
db,
|
|
180
|
+
targets,
|
|
181
|
+
skipPrefixInjection,
|
|
182
|
+
});
|
|
183
|
+
textOrdinal += 1;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (part.kind === "tool_use" || part.kind === "tool_result") {
|
|
188
|
+
if (messageId === undefined) continue;
|
|
189
|
+
|
|
190
|
+
const callId = part.id;
|
|
191
|
+
const text = part.getText() ?? "";
|
|
192
|
+
const meta = part.getToolMetadata();
|
|
193
|
+
|
|
194
|
+
if (typeof callId !== "string" || callId.length === 0) {
|
|
195
|
+
// No stable callId to aggregate on. Tag independently.
|
|
196
|
+
tagToolPart({
|
|
197
|
+
sessionId,
|
|
198
|
+
message,
|
|
199
|
+
messageId,
|
|
200
|
+
msgIndex,
|
|
201
|
+
partIndex,
|
|
202
|
+
part,
|
|
203
|
+
tagger,
|
|
204
|
+
db,
|
|
205
|
+
targets,
|
|
206
|
+
skipPrefixInjection,
|
|
207
|
+
});
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const existing = toolAggregates.get(callId);
|
|
212
|
+
if (existing) {
|
|
213
|
+
// Second (or later) occurrence for this call_id.
|
|
214
|
+
// Merge into the existing aggregate, update byte_size
|
|
215
|
+
// in DB if larger, and rebuild the TagTarget so the
|
|
216
|
+
// closures over `occurrences` see all parts.
|
|
217
|
+
existing.occurrences.push({
|
|
218
|
+
message,
|
|
219
|
+
part,
|
|
220
|
+
kind: part.kind,
|
|
221
|
+
});
|
|
222
|
+
const newByteSize = byteSize(text);
|
|
223
|
+
if (newByteSize > existing.maxByteSize) {
|
|
224
|
+
existing.maxByteSize = newByteSize;
|
|
225
|
+
updateTagByteSize(db, sessionId, existing.tagId, newByteSize);
|
|
226
|
+
}
|
|
227
|
+
if (existing.toolName === null && meta.toolName) {
|
|
228
|
+
existing.toolName = meta.toolName;
|
|
229
|
+
}
|
|
230
|
+
if (
|
|
231
|
+
existing.inputByteSize === 0 &&
|
|
232
|
+
part.kind === "tool_use" &&
|
|
233
|
+
meta.inputByteSize > 0
|
|
234
|
+
) {
|
|
235
|
+
existing.inputByteSize = meta.inputByteSize;
|
|
236
|
+
updateTagInputByteSize(
|
|
237
|
+
db,
|
|
238
|
+
sessionId,
|
|
239
|
+
existing.tagId,
|
|
240
|
+
meta.inputByteSize,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
// Inject §N§ prefix into this tool_result occurrence
|
|
244
|
+
// (matches OpenCode behavior — only result gets the prefix).
|
|
245
|
+
if (!skipPrefixInjection && part.kind === "tool_result") {
|
|
246
|
+
part.setText(prependTag(existing.tagId, text));
|
|
247
|
+
}
|
|
248
|
+
// Rebuild the aggregate target so it walks the now-
|
|
249
|
+
// longer occurrences list.
|
|
250
|
+
targets.set(
|
|
251
|
+
existing.tagId,
|
|
252
|
+
buildAggregateTarget(existing.tagId, existing.occurrences),
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
// First occurrence — reserve the tag number.
|
|
256
|
+
const tagId = tagger.assignTag(
|
|
257
|
+
sessionId,
|
|
258
|
+
callId,
|
|
259
|
+
"tool",
|
|
260
|
+
byteSize(text),
|
|
261
|
+
db,
|
|
262
|
+
0,
|
|
263
|
+
meta.toolName ?? null,
|
|
264
|
+
meta.inputByteSize,
|
|
265
|
+
);
|
|
266
|
+
const aggregate = {
|
|
267
|
+
callId,
|
|
268
|
+
tagId,
|
|
269
|
+
occurrences: [
|
|
270
|
+
{
|
|
271
|
+
message,
|
|
272
|
+
part,
|
|
273
|
+
kind: part.kind,
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
maxByteSize: byteSize(text),
|
|
277
|
+
toolName: meta.toolName ?? null,
|
|
278
|
+
inputByteSize: part.kind === "tool_use" ? meta.inputByteSize : 0,
|
|
279
|
+
};
|
|
280
|
+
toolAggregates.set(callId, aggregate);
|
|
281
|
+
// Inject §N§ prefix into this occurrence's visible text
|
|
282
|
+
// when it's a tool_result. (OpenCode parity: prefix
|
|
283
|
+
// only goes on the result, not the invocation.)
|
|
284
|
+
if (!skipPrefixInjection && part.kind === "tool_result") {
|
|
285
|
+
part.setText(prependTag(tagId, text));
|
|
286
|
+
}
|
|
287
|
+
targets.set(tagId, buildAggregateTarget(tagId, aggregate.occurrences));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// thinking, image, file, structural, unknown → skip.
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
})();
|
|
294
|
+
|
|
295
|
+
return { targets };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface TagTextPartArgs {
|
|
299
|
+
sessionId: string;
|
|
300
|
+
message: { info: { id?: string; role: string } };
|
|
301
|
+
messageId: string;
|
|
302
|
+
msgIndex: number;
|
|
303
|
+
textOrdinal: number;
|
|
304
|
+
part: TranscriptPart;
|
|
305
|
+
tagger: Tagger;
|
|
306
|
+
db: ContextDatabase;
|
|
307
|
+
targets: Map<number, TagTarget>;
|
|
308
|
+
skipPrefixInjection: boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function tagTextPart(args: TagTextPartArgs): void {
|
|
312
|
+
const text = args.part.getText() ?? "";
|
|
313
|
+
const contentId = `${args.messageId}:p${args.textOrdinal}`;
|
|
314
|
+
const tagId = args.tagger.assignTag(
|
|
315
|
+
args.sessionId,
|
|
316
|
+
contentId,
|
|
317
|
+
"message",
|
|
318
|
+
byteSize(text),
|
|
319
|
+
args.db,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Persist the original (pre-tagged) source content so caveman
|
|
323
|
+
// compression and other "compress from original" heuristics have
|
|
324
|
+
// pristine text to read on later passes. saveSourceContent uses
|
|
325
|
+
// INSERT OR IGNORE — first write wins; later passes that re-tag
|
|
326
|
+
// the same (sessionId, tagId) pair from already-prefixed text won't
|
|
327
|
+
// overwrite the original. Cache-stable.
|
|
328
|
+
//
|
|
329
|
+
// We strip any existing §N§ prefix before saving in case a previous
|
|
330
|
+
// pass already injected one and the persisted source got lost
|
|
331
|
+
// (e.g. legacy session created before this code shipped). For new
|
|
332
|
+
// sessions stripTagPrefix is a no-op on the very first pass.
|
|
333
|
+
const sourceContent = stripTagPrefix(text);
|
|
334
|
+
if (sourceContent.trim().length > 0) {
|
|
335
|
+
saveSourceContent(args.db, args.sessionId, tagId, sourceContent);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!args.skipPrefixInjection) {
|
|
339
|
+
args.part.setText(prependTag(tagId, text));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
args.targets.set(tagId, buildTextTarget(args.part, args.message));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
interface TagToolPartArgs {
|
|
346
|
+
sessionId: string;
|
|
347
|
+
message: { info: { id?: string; role: string } };
|
|
348
|
+
messageId: string;
|
|
349
|
+
msgIndex: number;
|
|
350
|
+
partIndex: number;
|
|
351
|
+
part: TranscriptPart;
|
|
352
|
+
tagger: Tagger;
|
|
353
|
+
db: ContextDatabase;
|
|
354
|
+
targets: Map<number, TagTarget>;
|
|
355
|
+
skipPrefixInjection: boolean;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function tagToolPart(args: TagToolPartArgs): void {
|
|
359
|
+
// Prefer the part's stable id (tool call id from Pi/OpenCode); fall
|
|
360
|
+
// back to a synthetic locator. Tool calls and their results MAY
|
|
361
|
+
// share an id (Pi sets toolCallId on ToolResultMessage to match the
|
|
362
|
+
// originating ToolCall.id); when that happens, both tag operations
|
|
363
|
+
// resolve to the same tag number — desired behavior, since drops
|
|
364
|
+
// target the call-id pair as a unit.
|
|
365
|
+
const stableId = args.part.id;
|
|
366
|
+
const contentId = stableId ?? `${args.messageId}:t${args.partIndex}`;
|
|
367
|
+
const text = args.part.getText() ?? "";
|
|
368
|
+
const meta = args.part.getToolMetadata();
|
|
369
|
+
const tagId = args.tagger.assignTag(
|
|
370
|
+
args.sessionId,
|
|
371
|
+
contentId,
|
|
372
|
+
"tool",
|
|
373
|
+
byteSize(text),
|
|
374
|
+
args.db,
|
|
375
|
+
0,
|
|
376
|
+
meta.toolName ?? null,
|
|
377
|
+
meta.inputByteSize,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// For tool parts, the visible payload is the tool result text. We
|
|
381
|
+
// can inject the tag prefix into it for in-text references; this
|
|
382
|
+
// matches the OpenCode behavior of tagging tool outputs.
|
|
383
|
+
if (!args.skipPrefixInjection && args.part.kind === "tool_result") {
|
|
384
|
+
const tagged = prependTag(tagId, text);
|
|
385
|
+
args.part.setText(tagged);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
args.targets.set(tagId, buildToolTarget(args.part, args.message));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Build a TagTarget that walks ALL occurrences of a tool call (invocation
|
|
393
|
+
* + result) when mutating. This is the per-callId aggregate target used
|
|
394
|
+
* by `tagTranscript` so a single drop replaces both halves.
|
|
395
|
+
*
|
|
396
|
+
* The closures hold a reference to the same `occurrences` array stored
|
|
397
|
+
* on the aggregate, so when the array gets mutated (a second occurrence
|
|
398
|
+
* is pushed mid-walk), the next call to setContent/drop/truncate sees
|
|
399
|
+
* all occurrences automatically. Callers MUST rebuild the target after
|
|
400
|
+
* pushing a new occurrence so the targets map points to a fresh closure
|
|
401
|
+
* over the updated array — otherwise consumers that captured the target
|
|
402
|
+
* before the push won't see the new occurrence.
|
|
403
|
+
*
|
|
404
|
+
* Mirrors OpenCode's createToolDropTarget semantics in tool-drop-target.ts.
|
|
405
|
+
*/
|
|
406
|
+
function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): TagTarget {
|
|
407
|
+
const role = occurrences[0]?.message.info.role ?? "user";
|
|
408
|
+
const messageId = occurrences[0]?.message.info.id;
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
setContent(content: string): boolean {
|
|
412
|
+
// Walk all occurrences; mutate every one. Return true if at
|
|
413
|
+
// least one occurrence's content actually changed (used to
|
|
414
|
+
// gate sentinel-replay re-writes).
|
|
415
|
+
let changed = false;
|
|
416
|
+
for (const occ of occurrences) {
|
|
417
|
+
// Try setToolOutput first (works on tool_result-shaped parts);
|
|
418
|
+
// fall back to setText so tool_use parts also get sentinelized.
|
|
419
|
+
if (occ.part.setToolOutput(content)) {
|
|
420
|
+
changed = true;
|
|
421
|
+
} else if (occ.part.setText(content)) {
|
|
422
|
+
changed = true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return changed;
|
|
426
|
+
},
|
|
427
|
+
getContent(): string | null {
|
|
428
|
+
// Prefer the result occurrence's content (the bulky payload).
|
|
429
|
+
for (const occ of occurrences) {
|
|
430
|
+
if (occ.kind === "tool_result") {
|
|
431
|
+
return occ.part.getText() ?? null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return occurrences[0]?.part.getText() ?? null;
|
|
435
|
+
},
|
|
436
|
+
drop(): "removed" | "absent" {
|
|
437
|
+
// Replace BOTH halves with the dropped sentinel.
|
|
438
|
+
const sentinel = `[dropped \u00a7${tagId}\u00a7]`;
|
|
439
|
+
let any = false;
|
|
440
|
+
for (const occ of occurrences) {
|
|
441
|
+
if (occ.part.replaceWithSentinel(sentinel)) any = true;
|
|
442
|
+
}
|
|
443
|
+
return any ? "removed" : "absent";
|
|
444
|
+
},
|
|
445
|
+
truncate(): "truncated" | "absent" {
|
|
446
|
+
// Truncate BOTH halves. For tool_use, this typically truncates
|
|
447
|
+
// the args; for tool_result, the output. The sentinel string
|
|
448
|
+
// matches OpenCode's truncate sentinel exactly.
|
|
449
|
+
const sentinel = "[truncated]";
|
|
450
|
+
let any = false;
|
|
451
|
+
for (const occ of occurrences) {
|
|
452
|
+
if (occ.part.setToolOutput(sentinel) || occ.part.setText(sentinel)) {
|
|
453
|
+
any = true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return any ? "truncated" : "absent";
|
|
457
|
+
},
|
|
458
|
+
message: {
|
|
459
|
+
info: { id: messageId, role },
|
|
460
|
+
parts: [],
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* TagTarget for a tag-eligible text part. The shared
|
|
467
|
+
* `applyPendingOperations` flow calls `setContent` to swap in a
|
|
468
|
+
* sentinel like `[dropped §N§]` when a queued drop fires; `getContent`
|
|
469
|
+
* returns the current visible text so the truncated-preview path can
|
|
470
|
+
* compute its before/after.
|
|
471
|
+
*
|
|
472
|
+
* The `message.info.role` is used by `buildReplacementContent` in
|
|
473
|
+
* `apply-operations.ts` to differentiate user-message drops (which
|
|
474
|
+
* preserve a truncated preview) from assistant drops (full sentinel).
|
|
475
|
+
*/
|
|
476
|
+
function buildTextTarget(
|
|
477
|
+
part: TranscriptPart,
|
|
478
|
+
message: { info: { id?: string; role: string } },
|
|
479
|
+
): TagTarget {
|
|
480
|
+
return {
|
|
481
|
+
setContent(content: string): boolean {
|
|
482
|
+
return part.setText(content);
|
|
483
|
+
},
|
|
484
|
+
getContent(): string | null {
|
|
485
|
+
return part.getText() ?? null;
|
|
486
|
+
},
|
|
487
|
+
// `message` is typed as MessageLike, which has parts: unknown[].
|
|
488
|
+
// We don't carry parts here (the apply-operations flow only
|
|
489
|
+
// reads `info.role` on this field), so a minimal stub is
|
|
490
|
+
// sufficient.
|
|
491
|
+
message: {
|
|
492
|
+
info: { id: message.info.id, role: message.info.role },
|
|
493
|
+
parts: [],
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* TagTarget for a tag-eligible tool part. Tool parts get full-drop
|
|
500
|
+
* (replace with `[dropped §N§]`) or truncated-drop (replace with
|
|
501
|
+
* `[truncated]`) treatment from `applyFlushedStatuses` based on the
|
|
502
|
+
* stored `drop_mode` column. We expose both via the standard target
|
|
503
|
+
* surface; replaceWithSentinel is the canonical mutation, with
|
|
504
|
+
* truncated-drop using the "[truncated]" string.
|
|
505
|
+
*/
|
|
506
|
+
function buildToolTarget(
|
|
507
|
+
part: TranscriptPart,
|
|
508
|
+
message: { info: { id?: string; role: string } },
|
|
509
|
+
): TagTarget {
|
|
510
|
+
return {
|
|
511
|
+
setContent(content: string): boolean {
|
|
512
|
+
return part.setToolOutput(content) || part.setText(content);
|
|
513
|
+
},
|
|
514
|
+
getContent(): string | null {
|
|
515
|
+
return part.getText() ?? null;
|
|
516
|
+
},
|
|
517
|
+
drop(): "removed" | "absent" {
|
|
518
|
+
// Replace the tool part's visible content with a "[dropped]"
|
|
519
|
+
// shell. We can't physically remove the part because Pi
|
|
520
|
+
// requires tool_use ↔ tool_result pairing for the LLM call
|
|
521
|
+
// to validate; instead we shrink the content to a sentinel.
|
|
522
|
+
// For Pi the current Transcript contract treats both
|
|
523
|
+
// invocation and result parts symmetrically — both expose
|
|
524
|
+
// setText / setToolOutput.
|
|
525
|
+
const replaced = part.replaceWithSentinel(`[dropped \u00a7${part.id ?? "?"}\u00a7]`);
|
|
526
|
+
return replaced ? "removed" : "absent";
|
|
527
|
+
},
|
|
528
|
+
truncate(): "truncated" | "absent" {
|
|
529
|
+
// Truncate the tool output to a fixed sentinel string. Done
|
|
530
|
+
// via setToolOutput so the underlying tool_result content
|
|
531
|
+
// gets the truncation; falls back to setText for cases
|
|
532
|
+
// where the part type doesn't support setToolOutput.
|
|
533
|
+
const ok = part.setToolOutput("[truncated]") || part.setText("[truncated]");
|
|
534
|
+
return ok ? "truncated" : "absent";
|
|
535
|
+
},
|
|
536
|
+
message: {
|
|
537
|
+
info: { id: message.info.id, role: message.info.role },
|
|
538
|
+
parts: [],
|
|
539
|
+
},
|
|
540
|
+
};
|
|
541
|
+
}
|