@cortexkit/opencode-magic-context 0.21.8 → 0.22.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 +124 -323
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/agents/permissions.d.ts +29 -14
- package/dist/agents/permissions.d.ts.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/migrate-experimental.d.ts +29 -0
- package/dist/config/migrate-experimental.d.ts.map +1 -0
- package/dist/config/schema/agent-overrides.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +95 -104
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/features/builtin-commands/commands.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-embedding.d.ts +34 -0
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-events.d.ts +50 -0
- package/dist/features/magic-context/compartment-events.d.ts.map +1 -0
- package/dist/features/magic-context/compartment-storage.d.ts +22 -0
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/queue.d.ts +13 -2
- package/dist/features/magic-context/dreamer/queue.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/runner.d.ts +11 -0
- package/dist/features/magic-context/dreamer/runner.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts +1 -1
- package/dist/features/magic-context/dreamer/task-prompts.d.ts.map +1 -1
- package/dist/features/magic-context/git-commits/git-log-reader.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/project-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/memory/constants.d.ts +4 -0
- package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-identity.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-openai.d.ts +6 -0
- package/dist/features/magic-context/memory/embedding-openai.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-probe.d.ts +5 -0
- package/dist/features/magic-context/memory/embedding-probe.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding.d.ts.map +1 -1
- package/dist/features/magic-context/memory/index.d.ts +1 -1
- package/dist/features/magic-context/memory/index.d.ts.map +1 -1
- package/dist/features/magic-context/memory/memory-migration.d.ts +133 -0
- package/dist/features/magic-context/memory/memory-migration.d.ts.map +1 -0
- package/dist/features/magic-context/memory/project-identity.d.ts +38 -7
- package/dist/features/magic-context/memory/project-identity.d.ts.map +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 +15 -1
- package/dist/features/magic-context/memory/storage-memory.d.ts.map +1 -1
- package/dist/features/magic-context/memory/types.d.ts +3 -1
- package/dist/features/magic-context/memory/types.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +7 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/project-docs-hash.d.ts +6 -0
- package/dist/features/magic-context/project-docs-hash.d.ts.map +1 -0
- package/dist/features/magic-context/project-identity.d.ts +2 -0
- package/dist/features/magic-context/project-identity.d.ts.map +1 -0
- package/dist/features/magic-context/storage-db.d.ts +51 -7
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-historian-runs.d.ts +73 -0
- package/dist/features/magic-context/storage-historian-runs.d.ts.map +1 -0
- package/dist/features/magic-context/storage-identity-rekey-map.d.ts +11 -0
- package/dist/features/magic-context/storage-identity-rekey-map.d.ts.map +1 -0
- package/dist/features/magic-context/storage-m0-mutation-log.d.ts +22 -0
- package/dist/features/magic-context/storage-m0-mutation-log.d.ts.map +1 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts +25 -0
- package/dist/features/magic-context/storage-memory-mutation-log.d.ts.map +1 -0
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +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 +44 -0
- package/dist/features/magic-context/storage-meta-shared.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -0
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-project-state.d.ts +19 -0
- package/dist/features/magic-context/storage-project-state.d.ts.map +1 -0
- package/dist/features/magic-context/storage-subagent-invocations.d.ts +9 -0
- package/dist/features/magic-context/storage-subagent-invocations.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts +21 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage-v22-backfill-failures.d.ts +24 -0
- package/dist/features/magic-context/storage-v22-backfill-failures.d.ts.map +1 -0
- package/dist/features/magic-context/storage.d.ts +12 -3
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
- package/dist/features/magic-context/tagger.d.ts +15 -1
- package/dist/features/magic-context/tagger.d.ts.map +1 -1
- package/dist/features/magic-context/types.d.ts +21 -0
- package/dist/features/magic-context/types.d.ts.map +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.map +1 -1
- package/dist/features/magic-context/v22-deferred-backfill.d.ts +46 -0
- package/dist/features/magic-context/v22-deferred-backfill.d.ts.map +1 -0
- package/dist/features/magic-context/work-metrics.d.ts +66 -0
- package/dist/features/magic-context/work-metrics.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/cache.d.ts.map +1 -1
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/cache-busting-signals.d.ts +9 -0
- package/dist/hooks/magic-context/cache-busting-signals.d.ts.map +1 -1
- package/dist/hooks/magic-context/command-handler.d.ts +13 -1
- package/dist/hooks/magic-context/command-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-parser.d.ts +25 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-prompt.d.ts +27 -16
- package/dist/hooks/magic-context/compartment-prompt.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts +6 -2
- package/dist/hooks/magic-context/compartment-runner-mapping.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts +9 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +67 -4
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/decay-curve.d.ts +78 -0
- package/dist/hooks/magic-context/decay-curve.d.ts.map +1 -0
- package/dist/hooks/magic-context/decay-render.d.ts +67 -0
- package/dist/hooks/magic-context/decay-render.d.ts.map +1 -0
- package/dist/hooks/magic-context/event-handler.d.ts +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts +17 -0
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts +2 -0
- package/dist/hooks/magic-context/historian-prompt.generated.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts +3 -0
- package/dist/hooks/magic-context/hook-handlers.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +9 -21
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +126 -0
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/key-files-block.d.ts.map +1 -1
- package/dist/hooks/magic-context/live-session-state.d.ts +9 -0
- package/dist/hooks/magic-context/live-session-state.d.ts.map +1 -1
- package/dist/hooks/magic-context/m0-token-breakdown.d.ts +35 -0
- package/dist/hooks/magic-context/m0-token-breakdown.d.ts.map +1 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts +9 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +7 -0
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts +104 -0
- package/dist/hooks/magic-context/recomp-orchestrator.d.ts.map +1 -0
- package/dist/hooks/magic-context/reference-retrieval.d.ts +61 -0
- package/dist/hooks/magic-context/reference-retrieval.d.ts.map +1 -0
- package/dist/hooks/magic-context/reference-seeds.generated.d.ts +8 -0
- package/dist/hooks/magic-context/reference-seeds.generated.d.ts.map +1 -0
- package/dist/hooks/magic-context/send-session-notification.d.ts +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 +5 -6
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts +6 -0
- package/dist/hooks/magic-context/tokenizer-calibration.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts +0 -7
- package/dist/hooks/magic-context/transform-compartment-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +18 -0
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +9 -7
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/upgrade-reminder.d.ts +73 -0
- package/dist/hooks/magic-context/upgrade-reminder.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9435 -4001
- package/dist/plugin/conflict-warning-hook.d.ts +13 -0
- package/dist/plugin/conflict-warning-hook.d.ts.map +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/event.d.ts +10 -0
- package/dist/plugin/event.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/plugin/tool-registry.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +17 -1
- package/dist/shared/announcement.d.ts.map +1 -1
- package/dist/shared/models-dev-cache.d.ts.map +1 -1
- package/dist/shared/rpc-client.d.ts +1 -0
- package/dist/shared/rpc-client.d.ts.map +1 -1
- package/dist/shared/rpc-notifications.d.ts +27 -5
- package/dist/shared/rpc-notifications.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts +1 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +30 -2
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +9 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-helpers.d.ts +7 -7
- package/dist/shared/sqlite.d.ts +23 -14
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts +10 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/tools/ctx-expand/tools.d.ts +5 -1
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tui/data/context-db.d.ts +16 -1
- package/dist/tui/data/context-db.d.ts.map +1 -1
- package/package.json +5 -7
- package/src/shared/announcement.test.ts +23 -7
- package/src/shared/announcement.ts +30 -8
- package/src/shared/conflict-detector.test.ts +15 -2
- package/src/shared/conflict-fixer.test.ts +5 -1
- package/src/shared/models-dev-cache.test.ts +72 -4
- package/src/shared/models-dev-cache.ts +47 -8
- package/src/shared/opencode-compaction-detector.test.ts +10 -2
- package/src/shared/rpc-client.test.ts +54 -3
- package/src/shared/rpc-client.ts +19 -9
- package/src/shared/rpc-notifications.test.ts +54 -1
- package/src/shared/rpc-notifications.ts +82 -13
- package/src/shared/rpc-server.ts +33 -4
- package/src/shared/rpc-types.ts +30 -2
- package/src/shared/rpc-utils.ts +10 -0
- package/src/shared/sqlite-helpers.ts +9 -9
- package/src/shared/sqlite.ts +99 -80
- package/src/shared/tag-transcript.test.ts +280 -0
- package/src/shared/tag-transcript.ts +162 -33
- package/src/tui/data/context-db.ts +75 -11
- package/src/tui/index.tsx +223 -32
- package/src/tui/slots/sidebar-content.tsx +366 -34
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts +0 -87
- package/dist/hooks/magic-context/compartment-runner-compressor.d.ts.map +0 -1
- package/dist/shared/native-binding.d.ts +0 -87
- package/dist/shared/native-binding.d.ts.map +0 -1
- package/src/shared/native-binding.ts +0 -311
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import type { ContextDatabase } from "../features/magic-context/storage";
|
|
5
|
+
import type { Tagger } from "../features/magic-context/tagger";
|
|
6
|
+
import { tagTranscript } from "./tag-transcript";
|
|
7
|
+
import type { Transcript, TranscriptPart, TranscriptPartKind } from "./transcript";
|
|
8
|
+
|
|
9
|
+
class FakeTagger implements Tagger {
|
|
10
|
+
private nextTag = 1;
|
|
11
|
+
private toolTags = new Map<string, number>();
|
|
12
|
+
readonly owners: string[] = [];
|
|
13
|
+
readonly byteSizes = new Map<number, number>();
|
|
14
|
+
|
|
15
|
+
assignTag(): number {
|
|
16
|
+
return this.nextTag++;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getTag(): number | undefined {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
assignToolTag(
|
|
24
|
+
_sessionId: string,
|
|
25
|
+
callId: string,
|
|
26
|
+
ownerMsgId: string,
|
|
27
|
+
byteSize: number,
|
|
28
|
+
): number {
|
|
29
|
+
const key = `${ownerMsgId}\0${callId}`;
|
|
30
|
+
const existing = this.toolTags.get(key);
|
|
31
|
+
if (existing !== undefined) return existing;
|
|
32
|
+
const tag = this.nextTag++;
|
|
33
|
+
this.toolTags.set(key, tag);
|
|
34
|
+
this.byteSizes.set(tag, byteSize);
|
|
35
|
+
this.owners.push(ownerMsgId);
|
|
36
|
+
return tag;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getToolTag(_sessionId: string, callId: string, ownerMsgId: string): number | undefined {
|
|
40
|
+
return this.toolTags.get(`${ownerMsgId}\0${callId}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
bindTag(): void {}
|
|
44
|
+
|
|
45
|
+
bindToolTag(_sessionId: string, callId: string, ownerMsgId: string, tagNumber: number): void {
|
|
46
|
+
this.toolTags.set(`${ownerMsgId}\0${callId}`, tagNumber);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getAssignments(): ReadonlyMap<string, number> {
|
|
50
|
+
return this.toolTags;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
resetCounter(): void {
|
|
54
|
+
this.nextTag = 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getCounter(): number {
|
|
58
|
+
return this.nextTag - 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
initFromDb(): void {}
|
|
62
|
+
|
|
63
|
+
cleanup(): void {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
class TestPart implements TranscriptPart {
|
|
67
|
+
constructor(
|
|
68
|
+
readonly kind: TranscriptPartKind,
|
|
69
|
+
readonly id: string | undefined,
|
|
70
|
+
private text: string,
|
|
71
|
+
private readonly toolName = "read",
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
getText(): string | undefined {
|
|
75
|
+
return this.text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
setText(newText: string): boolean {
|
|
79
|
+
if (this.text === newText) return false;
|
|
80
|
+
this.text = newText;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setToolOutput(newText: string): boolean {
|
|
85
|
+
return this.setText(newText);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getToolMetadata(): { toolName: string | undefined; inputByteSize: number } {
|
|
89
|
+
return {
|
|
90
|
+
toolName: this.toolName,
|
|
91
|
+
inputByteSize: this.kind === "tool_use" ? this.text.length : 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
replaceWithSentinel(sentinelText: string): boolean {
|
|
96
|
+
return this.setText(sentinelText);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class ThrowingToolOutputPart extends TestPart {
|
|
101
|
+
setToolOutput(): boolean {
|
|
102
|
+
throw new Error("setToolOutput on assistant part");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class NonTextToolResultPart extends TestPart {
|
|
107
|
+
constructor(
|
|
108
|
+
id: string,
|
|
109
|
+
readonly content: unknown,
|
|
110
|
+
) {
|
|
111
|
+
super("tool_result", id, "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getText(): string | undefined {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setText(): boolean {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class FakeDb {
|
|
124
|
+
readonly byteSizeUpdates: Array<{ byteSize: number; sessionId: string; tagNumber: number }> =
|
|
125
|
+
[];
|
|
126
|
+
|
|
127
|
+
prepare(sql: string): { run: (...args: unknown[]) => void } {
|
|
128
|
+
return {
|
|
129
|
+
run: (...args: unknown[]) => {
|
|
130
|
+
if (!sql.startsWith("UPDATE tags SET byte_size =")) return;
|
|
131
|
+
const [byteSize, sessionId, tagNumber] = args;
|
|
132
|
+
if (
|
|
133
|
+
typeof byteSize === "number" &&
|
|
134
|
+
typeof sessionId === "string" &&
|
|
135
|
+
typeof tagNumber === "number"
|
|
136
|
+
) {
|
|
137
|
+
this.byteSizeUpdates.push({ byteSize, sessionId, tagNumber });
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
describe("tagTranscript tool aggregation", () => {
|
|
145
|
+
it("keeps repeated callIds in separate owner-scoped aggregate targets", () => {
|
|
146
|
+
const tagger = new FakeTagger();
|
|
147
|
+
const firstUse = new TestPart("tool_use", "read:32", '{"file":"long-a"}');
|
|
148
|
+
const firstResult = new TestPart("tool_result", "read:32", "r1");
|
|
149
|
+
const secondUse = new TestPart("tool_use", "read:32", '{"file":"long-b"}');
|
|
150
|
+
const secondResult = new TestPart("tool_result", "read:32", "r2");
|
|
151
|
+
const transcript: Transcript = {
|
|
152
|
+
harness: "pi",
|
|
153
|
+
messages: [
|
|
154
|
+
{ info: { id: "assistant-1", role: "assistant" }, parts: [firstUse] },
|
|
155
|
+
{ info: { id: "user-1", role: "user" }, parts: [firstResult] },
|
|
156
|
+
{ info: { id: "assistant-2", role: "assistant" }, parts: [secondUse] },
|
|
157
|
+
{ info: { id: "user-2", role: "user" }, parts: [secondResult] },
|
|
158
|
+
],
|
|
159
|
+
commit() {},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const { targets } = tagTranscript("session-1", transcript, tagger, {} as ContextDatabase);
|
|
163
|
+
|
|
164
|
+
const firstTag = tagger.getToolTag("session-1", "read:32", "assistant-1");
|
|
165
|
+
const secondTag = tagger.getToolTag("session-1", "read:32", "assistant-2");
|
|
166
|
+
expect(firstTag).toBeDefined();
|
|
167
|
+
expect(secondTag).toBeDefined();
|
|
168
|
+
expect(firstTag).not.toBe(secondTag);
|
|
169
|
+
expect(tagger.owners).toEqual(["assistant-1", "assistant-2"]);
|
|
170
|
+
expect(targets.size).toBe(2);
|
|
171
|
+
|
|
172
|
+
expect(targets.get(firstTag ?? -1)?.drop()).toBe("removed");
|
|
173
|
+
expect(firstUse.getText()).toBe(`[dropped §${firstTag}§]`);
|
|
174
|
+
expect(firstResult.getText()).toBe(`[dropped §${firstTag}§]`);
|
|
175
|
+
expect(secondUse.getText()).toBe('{"file":"long-b"}');
|
|
176
|
+
expect(secondResult.getText()).toContain("r2");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("truncates assistant tool_use parts via text fallback when setToolOutput asserts", () => {
|
|
180
|
+
const tagger = new FakeTagger();
|
|
181
|
+
const toolUse = new ThrowingToolOutputPart("tool_use", "read:99", '{"file":"long-a"}');
|
|
182
|
+
const transcript: Transcript = {
|
|
183
|
+
harness: "pi",
|
|
184
|
+
messages: [{ info: { id: "assistant-1", role: "assistant" }, parts: [toolUse] }],
|
|
185
|
+
commit() {},
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const { targets } = tagTranscript("session-1", transcript, tagger, {} as ContextDatabase);
|
|
189
|
+
const tag = tagger.getToolTag("session-1", "read:99", "assistant-1");
|
|
190
|
+
|
|
191
|
+
let result: "truncated" | "absent" | undefined;
|
|
192
|
+
expect(() => {
|
|
193
|
+
result = targets.get(tag ?? -1)?.truncate?.();
|
|
194
|
+
}).not.toThrow();
|
|
195
|
+
expect(result).toBe("truncated");
|
|
196
|
+
expect(toolUse.getText()).toBe("[truncated]");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("drops every contiguous folded tool_result block for the paired callId", () => {
|
|
200
|
+
const tagger = new FakeTagger();
|
|
201
|
+
const toolUse = new TestPart("tool_use", "read:multi", '{"file":"long-a"}');
|
|
202
|
+
const firstResult = new TestPart("tool_result", "read:multi", "r1");
|
|
203
|
+
const secondResult = new TestPart("tool_result", "read:multi", "r2");
|
|
204
|
+
const transcript: Transcript = {
|
|
205
|
+
harness: "pi",
|
|
206
|
+
messages: [
|
|
207
|
+
{ info: { id: "assistant-1", role: "assistant" }, parts: [toolUse] },
|
|
208
|
+
{ info: { id: "user-1", role: "user" }, parts: [firstResult, secondResult] },
|
|
209
|
+
],
|
|
210
|
+
commit() {},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const { targets } = tagTranscript("session-1", transcript, tagger, {} as ContextDatabase);
|
|
214
|
+
const tag = tagger.getToolTag("session-1", "read:multi", "assistant-1");
|
|
215
|
+
|
|
216
|
+
expect(targets.size).toBe(1);
|
|
217
|
+
expect(targets.get(tag ?? -1)?.drop()).toBe("removed");
|
|
218
|
+
expect(toolUse.getText()).toBe(`[dropped §${tag}§]`);
|
|
219
|
+
expect(firstResult.getText()).toBe(`[dropped §${tag}§]`);
|
|
220
|
+
expect(secondResult.getText()).toBe(`[dropped §${tag}§]`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("pairs a reused callId result with the nearest previous unresolved owner", () => {
|
|
224
|
+
const tagger = new FakeTagger();
|
|
225
|
+
const olderUse = new TestPart("tool_use", "read:reused", '{"file":"older"}');
|
|
226
|
+
const nearestUse = new TestPart("tool_use", "read:reused", '{"file":"nearest"}');
|
|
227
|
+
const result = new TestPart("tool_result", "read:reused", "nearest result");
|
|
228
|
+
const transcript: Transcript = {
|
|
229
|
+
harness: "pi",
|
|
230
|
+
messages: [
|
|
231
|
+
{ info: { id: "assistant-old", role: "assistant" }, parts: [olderUse] },
|
|
232
|
+
{ info: { id: "assistant-near", role: "assistant" }, parts: [nearestUse] },
|
|
233
|
+
{ info: { id: "user-result", role: "user" }, parts: [result] },
|
|
234
|
+
],
|
|
235
|
+
commit() {},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const { targets } = tagTranscript("session-1", transcript, tagger, {} as ContextDatabase);
|
|
239
|
+
|
|
240
|
+
const olderTag = tagger.getToolTag("session-1", "read:reused", "assistant-old");
|
|
241
|
+
const nearestTag = tagger.getToolTag("session-1", "read:reused", "assistant-near");
|
|
242
|
+
expect(olderTag).toBeDefined();
|
|
243
|
+
expect(nearestTag).toBeDefined();
|
|
244
|
+
expect(olderTag).not.toBe(nearestTag);
|
|
245
|
+
|
|
246
|
+
expect(targets.get(nearestTag ?? -1)?.drop()).toBe("removed");
|
|
247
|
+
expect(olderUse.getText()).toBe('{"file":"older"}');
|
|
248
|
+
expect(nearestUse.getText()).toBe(`[dropped §${nearestTag}§]`);
|
|
249
|
+
expect(result.getText()).toBe(`[dropped §${nearestTag}§]`);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("accounts non-text tool_result content when ranking tool output byte size", () => {
|
|
253
|
+
const tagger = new FakeTagger();
|
|
254
|
+
const db = new FakeDb();
|
|
255
|
+
const toolUse = new TestPart("tool_use", "read:image", "{}");
|
|
256
|
+
const caption = new TestPart("tool_result", "read:image", "c");
|
|
257
|
+
const image = new NonTextToolResultPart("read:image", {
|
|
258
|
+
type: "image",
|
|
259
|
+
data: "x".repeat(512),
|
|
260
|
+
mediaType: "image/png",
|
|
261
|
+
});
|
|
262
|
+
const transcript: Transcript = {
|
|
263
|
+
harness: "pi",
|
|
264
|
+
messages: [
|
|
265
|
+
{ info: { id: "assistant-1", role: "assistant" }, parts: [toolUse] },
|
|
266
|
+
{ info: { id: "user-1", role: "user" }, parts: [caption, image] },
|
|
267
|
+
],
|
|
268
|
+
commit() {},
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
tagTranscript("session-1", transcript, tagger, db as unknown as ContextDatabase);
|
|
272
|
+
|
|
273
|
+
const tag = tagger.getToolTag("session-1", "read:image", "assistant-1");
|
|
274
|
+
expect(tag).toBeDefined();
|
|
275
|
+
expect(tagger.byteSizes.get(tag ?? -1)).toBe(2);
|
|
276
|
+
expect(db.byteSizeUpdates).toHaveLength(1);
|
|
277
|
+
expect(db.byteSizeUpdates[0]?.tagNumber).toBe(tag);
|
|
278
|
+
expect(db.byteSizeUpdates[0]?.byteSize).toBeGreaterThan(512);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
import type { ContextDatabase } from "../features/magic-context/storage";
|
|
49
49
|
import { saveSourceContent } from "../features/magic-context/storage-source";
|
|
50
50
|
import { updateTagByteSize, updateTagInputByteSize } from "../features/magic-context/storage-tags";
|
|
51
|
-
import type
|
|
51
|
+
import { makeToolCompositeKey, type Tagger } from "../features/magic-context/tagger";
|
|
52
52
|
import {
|
|
53
53
|
byteSize,
|
|
54
54
|
prependTag,
|
|
@@ -67,6 +67,15 @@ export interface TagTranscriptOptions {
|
|
|
67
67
|
* consistent across passes.
|
|
68
68
|
*/
|
|
69
69
|
skipPrefixInjection?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Pi-only: map of messageId → raw-message fingerprint. When a NEW message
|
|
72
|
+
* text tag is created, its fingerprint is persisted on the tag row so a
|
|
73
|
+
* later pass can adopt the fallback-id tag onto the real SessionEntry id
|
|
74
|
+
* (keeping tag_number/§N§ stable). OpenCode omits this → tags store NULL
|
|
75
|
+
* → adoption never fires. Keyed by the bare messageId (not the `:pN`
|
|
76
|
+
* contentId) since all parts of a message share one fingerprint.
|
|
77
|
+
*/
|
|
78
|
+
entryFingerprintByMessageId?: ReadonlyMap<string, string>;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
export interface TagTranscriptResult {
|
|
@@ -138,13 +147,13 @@ export function tagTranscript(
|
|
|
138
147
|
const skipPrefixInjection = options.skipPrefixInjection === true;
|
|
139
148
|
const targets = new Map<number, TagTarget>();
|
|
140
149
|
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// chronological tag numbering while still aggregating drop behavior
|
|
146
|
-
// across both invocation and result.
|
|
150
|
+
// Tool aggregation is keyed by the same owner+callId identity used by
|
|
151
|
+
// assignToolTag. OpenCode/Pi callId counters can repeat across turns, so
|
|
152
|
+
// a bare callId key can merge distinct invocations and replay drops/status
|
|
153
|
+
// changes against the wrong tool pair.
|
|
147
154
|
const toolAggregates = new Map<string, ToolAggregate & { tagId: number }>();
|
|
155
|
+
const openToolAggregateKeysByCallId = new Map<string, string[]>();
|
|
156
|
+
let activeToolResultRun: { callId: string; aggregateKey: string } | undefined;
|
|
148
157
|
|
|
149
158
|
// v3.3.1 Layer C (plan v3.3.1 Finding #16): the previous outer
|
|
150
159
|
// db.transaction() wrapper rolled back EVERY tag insert + savedSource
|
|
@@ -156,6 +165,7 @@ export function tagTranscript(
|
|
|
156
165
|
for (let msgIndex = 0; msgIndex < transcript.messages.length; msgIndex += 1) {
|
|
157
166
|
const message = transcript.messages[msgIndex];
|
|
158
167
|
if (message === undefined) continue;
|
|
168
|
+
activeToolResultRun = undefined;
|
|
159
169
|
const messageId = message.info.id;
|
|
160
170
|
|
|
161
171
|
let textOrdinal = 0;
|
|
@@ -165,6 +175,10 @@ export function tagTranscript(
|
|
|
165
175
|
const part = parts[partIndex];
|
|
166
176
|
if (part === undefined) continue;
|
|
167
177
|
|
|
178
|
+
if (part.kind !== "tool_result") {
|
|
179
|
+
activeToolResultRun = undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
if (part.kind === "text") {
|
|
169
183
|
// Synthetic message ids (Pi tail synthetic user with
|
|
170
184
|
// no id) cannot be tagged — there's no stable handle
|
|
@@ -186,19 +200,25 @@ export function tagTranscript(
|
|
|
186
200
|
db,
|
|
187
201
|
targets,
|
|
188
202
|
skipPrefixInjection,
|
|
203
|
+
entryFingerprint: options.entryFingerprintByMessageId?.get(messageId) ?? null,
|
|
189
204
|
});
|
|
190
205
|
textOrdinal += 1;
|
|
191
206
|
continue;
|
|
192
207
|
}
|
|
193
208
|
|
|
194
209
|
if (part.kind === "tool_use" || part.kind === "tool_result") {
|
|
195
|
-
if (messageId === undefined)
|
|
210
|
+
if (messageId === undefined) {
|
|
211
|
+
activeToolResultRun = undefined;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
196
214
|
|
|
197
215
|
const callId = part.id;
|
|
198
216
|
const text = part.getText() ?? "";
|
|
217
|
+
const toolByteSize = getToolPartByteSize(part, text);
|
|
199
218
|
const meta = part.getToolMetadata();
|
|
200
219
|
|
|
201
220
|
if (typeof callId !== "string" || callId.length === 0) {
|
|
221
|
+
activeToolResultRun = undefined;
|
|
202
222
|
// No stable callId to aggregate on. Tag independently.
|
|
203
223
|
tagToolPart({
|
|
204
224
|
sessionId,
|
|
@@ -215,21 +235,35 @@ export function tagTranscript(
|
|
|
215
235
|
continue;
|
|
216
236
|
}
|
|
217
237
|
|
|
218
|
-
const
|
|
238
|
+
const pendingKeys = openToolAggregateKeysByCallId.get(callId) ?? [];
|
|
239
|
+
let existingKey: string | undefined;
|
|
240
|
+
if (part.kind === "tool_result") {
|
|
241
|
+
if (
|
|
242
|
+
activeToolResultRun !== undefined &&
|
|
243
|
+
activeToolResultRun.callId === callId
|
|
244
|
+
) {
|
|
245
|
+
existingKey = activeToolResultRun.aggregateKey;
|
|
246
|
+
} else {
|
|
247
|
+
existingKey = findLastUnresolvedToolAggregateKey(
|
|
248
|
+
pendingKeys,
|
|
249
|
+
toolAggregates,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const aggregateKey: string = existingKey ?? makeToolCompositeKey(messageId, callId);
|
|
254
|
+
const existing = toolAggregates.get(aggregateKey);
|
|
219
255
|
if (existing) {
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// closures over `occurrences` see all parts.
|
|
256
|
+
// Later occurrence for this owner+callId pair. Merge into the
|
|
257
|
+
// aggregate, update byte accounting if larger, and rebuild the
|
|
258
|
+
// TagTarget so drops mutate both invocation and result.
|
|
224
259
|
existing.occurrences.push({
|
|
225
260
|
message,
|
|
226
261
|
part,
|
|
227
262
|
kind: part.kind,
|
|
228
263
|
});
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
existing.
|
|
232
|
-
updateTagByteSize(db, sessionId, existing.tagId, newByteSize);
|
|
264
|
+
if (toolByteSize > existing.maxByteSize) {
|
|
265
|
+
existing.maxByteSize = toolByteSize;
|
|
266
|
+
updateTagByteSize(db, sessionId, existing.tagId, toolByteSize);
|
|
233
267
|
}
|
|
234
268
|
if (existing.toolName === null && meta.toolName) {
|
|
235
269
|
existing.toolName = meta.toolName;
|
|
@@ -253,18 +287,23 @@ export function tagTranscript(
|
|
|
253
287
|
existing.tagId,
|
|
254
288
|
buildAggregateTarget(existing.tagId, existing.occurrences),
|
|
255
289
|
);
|
|
290
|
+
if (part.kind === "tool_result") {
|
|
291
|
+
markToolAggregateResolved(
|
|
292
|
+
callId,
|
|
293
|
+
aggregateKey,
|
|
294
|
+
openToolAggregateKeysByCallId,
|
|
295
|
+
);
|
|
296
|
+
activeToolResultRun = { callId, aggregateKey };
|
|
297
|
+
}
|
|
256
298
|
} else {
|
|
257
|
-
// First occurrence
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// Owner stays stable across passes because Pi
|
|
261
|
-
// re-emits the full transcript each time and
|
|
262
|
-
// message ids are durable.
|
|
299
|
+
// First occurrence for this owner+callId identity — reserve
|
|
300
|
+
// the tag number. Owner stays stable across passes because
|
|
301
|
+
// transcript message ids are durable.
|
|
263
302
|
const tagId = tagger.assignToolTag(
|
|
264
303
|
sessionId,
|
|
265
304
|
callId,
|
|
266
305
|
messageId,
|
|
267
|
-
|
|
306
|
+
toolByteSize,
|
|
268
307
|
db,
|
|
269
308
|
0,
|
|
270
309
|
meta.toolName ?? null,
|
|
@@ -280,11 +319,14 @@ export function tagTranscript(
|
|
|
280
319
|
kind: part.kind,
|
|
281
320
|
},
|
|
282
321
|
],
|
|
283
|
-
maxByteSize:
|
|
322
|
+
maxByteSize: toolByteSize,
|
|
284
323
|
toolName: meta.toolName ?? null,
|
|
285
324
|
inputByteSize: part.kind === "tool_use" ? meta.inputByteSize : 0,
|
|
286
325
|
};
|
|
287
|
-
toolAggregates.set(
|
|
326
|
+
toolAggregates.set(aggregateKey, aggregate);
|
|
327
|
+
if (part.kind === "tool_use") {
|
|
328
|
+
openToolAggregateKeysByCallId.set(callId, [...pendingKeys, aggregateKey]);
|
|
329
|
+
}
|
|
288
330
|
// Inject §N§ prefix into this occurrence's visible text
|
|
289
331
|
// when it's a tool_result. (OpenCode parity: prefix
|
|
290
332
|
// only goes on the result, not the invocation.)
|
|
@@ -292,6 +334,14 @@ export function tagTranscript(
|
|
|
292
334
|
part.setText(prependTag(tagId, text));
|
|
293
335
|
}
|
|
294
336
|
targets.set(tagId, buildAggregateTarget(tagId, aggregate.occurrences));
|
|
337
|
+
if (part.kind === "tool_result") {
|
|
338
|
+
markToolAggregateResolved(
|
|
339
|
+
callId,
|
|
340
|
+
aggregateKey,
|
|
341
|
+
openToolAggregateKeysByCallId,
|
|
342
|
+
);
|
|
343
|
+
activeToolResultRun = { callId, aggregateKey };
|
|
344
|
+
}
|
|
295
345
|
}
|
|
296
346
|
}
|
|
297
347
|
// thinking, image, file, structural, unknown → skip.
|
|
@@ -301,6 +351,69 @@ export function tagTranscript(
|
|
|
301
351
|
return { targets };
|
|
302
352
|
}
|
|
303
353
|
|
|
354
|
+
function findLastUnresolvedToolAggregateKey(
|
|
355
|
+
pendingKeys: string[],
|
|
356
|
+
toolAggregates: Map<string, ToolAggregate & { tagId: number }>,
|
|
357
|
+
): string | undefined {
|
|
358
|
+
for (let i = pendingKeys.length - 1; i >= 0; i -= 1) {
|
|
359
|
+
const key = pendingKeys[i];
|
|
360
|
+
if (key === undefined) continue;
|
|
361
|
+
const aggregate = toolAggregates.get(key);
|
|
362
|
+
if (aggregate === undefined) continue;
|
|
363
|
+
if (!aggregate.occurrences.some((occ) => occ.kind === "tool_result")) {
|
|
364
|
+
return key;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function markToolAggregateResolved(
|
|
371
|
+
callId: string,
|
|
372
|
+
aggregateKey: string,
|
|
373
|
+
openToolAggregateKeysByCallId: Map<string, string[]>,
|
|
374
|
+
): void {
|
|
375
|
+
const pendingKeys = openToolAggregateKeysByCallId.get(callId);
|
|
376
|
+
if (pendingKeys === undefined) return;
|
|
377
|
+
const nextPendingKeys = pendingKeys.filter((key) => key !== aggregateKey);
|
|
378
|
+
if (nextPendingKeys.length === 0) {
|
|
379
|
+
openToolAggregateKeysByCallId.delete(callId);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
openToolAggregateKeysByCallId.set(callId, nextPendingKeys);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getToolPartByteSize(part: TranscriptPart, text: string): number {
|
|
386
|
+
const textByteSize = byteSize(text);
|
|
387
|
+
if (textByteSize > 0 || part.kind !== "tool_result") return textByteSize;
|
|
388
|
+
return getNonTextToolResultByteSize(part);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getNonTextToolResultByteSize(part: TranscriptPart): number {
|
|
392
|
+
const record = isRecord(part) ? part : undefined;
|
|
393
|
+
const content =
|
|
394
|
+
record?.content ??
|
|
395
|
+
record?.rawContent ??
|
|
396
|
+
record?.rawPart ??
|
|
397
|
+
record?.part ??
|
|
398
|
+
record?.data ??
|
|
399
|
+
record?.image ??
|
|
400
|
+
record?.source;
|
|
401
|
+
const serialized = safeJsonStringify(content ?? part);
|
|
402
|
+
return serialized === undefined ? 0 : byteSize(serialized);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function safeJsonStringify(value: unknown): string | undefined {
|
|
406
|
+
try {
|
|
407
|
+
return JSON.stringify(value);
|
|
408
|
+
} catch {
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
414
|
+
return typeof value === "object" && value !== null;
|
|
415
|
+
}
|
|
416
|
+
|
|
304
417
|
interface TagTextPartArgs {
|
|
305
418
|
sessionId: string;
|
|
306
419
|
message: { info: { id?: string; role: string } };
|
|
@@ -312,6 +425,7 @@ interface TagTextPartArgs {
|
|
|
312
425
|
db: ContextDatabase;
|
|
313
426
|
targets: Map<number, TagTarget>;
|
|
314
427
|
skipPrefixInjection: boolean;
|
|
428
|
+
entryFingerprint: string | null;
|
|
315
429
|
}
|
|
316
430
|
|
|
317
431
|
function tagTextPart(args: TagTextPartArgs): void {
|
|
@@ -323,6 +437,10 @@ function tagTextPart(args: TagTextPartArgs): void {
|
|
|
323
437
|
"message",
|
|
324
438
|
byteSize(text),
|
|
325
439
|
args.db,
|
|
440
|
+
0,
|
|
441
|
+
null,
|
|
442
|
+
0,
|
|
443
|
+
args.entryFingerprint,
|
|
326
444
|
);
|
|
327
445
|
|
|
328
446
|
// Persist the original (pre-tagged) source content so caveman
|
|
@@ -371,6 +489,7 @@ function tagToolPart(args: TagToolPartArgs): void {
|
|
|
371
489
|
const stableId = args.part.id;
|
|
372
490
|
const contentId = stableId ?? `${args.messageId}:t${args.partIndex}`;
|
|
373
491
|
const text = args.part.getText() ?? "";
|
|
492
|
+
const toolByteSize = getToolPartByteSize(args.part, text);
|
|
374
493
|
const meta = args.part.getToolMetadata();
|
|
375
494
|
// v3.3.1 Layer C: synthetic ownership for the no-callId Pi
|
|
376
495
|
// fallback. Owner == callId == contentId. The composite key
|
|
@@ -382,7 +501,7 @@ function tagToolPart(args: TagToolPartArgs): void {
|
|
|
382
501
|
args.sessionId,
|
|
383
502
|
contentId,
|
|
384
503
|
contentId,
|
|
385
|
-
|
|
504
|
+
toolByteSize,
|
|
386
505
|
args.db,
|
|
387
506
|
0,
|
|
388
507
|
meta.toolName ?? null,
|
|
@@ -400,6 +519,18 @@ function tagToolPart(args: TagToolPartArgs): void {
|
|
|
400
519
|
args.targets.set(tagId, buildToolTarget(args.part, args.message));
|
|
401
520
|
}
|
|
402
521
|
|
|
522
|
+
function setToolContentOrText(part: TranscriptPart, content: string): boolean {
|
|
523
|
+
try {
|
|
524
|
+
if (part.setToolOutput(content)) return true;
|
|
525
|
+
} catch {
|
|
526
|
+
// Pi assistant tool_use parts deliberately assert if callers try
|
|
527
|
+
// to write a nonexistent output slot. Truncated-mode drops still
|
|
528
|
+
// need to shrink the invocation, so fall back to visible text/args
|
|
529
|
+
// replacement while preserving the adapter-level invariant.
|
|
530
|
+
}
|
|
531
|
+
return part.setText(content);
|
|
532
|
+
}
|
|
533
|
+
|
|
403
534
|
/**
|
|
404
535
|
* Build a TagTarget that walks ALL occurrences of a tool call (invocation
|
|
405
536
|
* + result) when mutating. This is the per-callId aggregate target used
|
|
@@ -428,9 +559,7 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
|
|
|
428
559
|
for (const occ of occurrences) {
|
|
429
560
|
// Try setToolOutput first (works on tool_result-shaped parts);
|
|
430
561
|
// fall back to setText so tool_use parts also get sentinelized.
|
|
431
|
-
if (occ.part
|
|
432
|
-
changed = true;
|
|
433
|
-
} else if (occ.part.setText(content)) {
|
|
562
|
+
if (setToolContentOrText(occ.part, content)) {
|
|
434
563
|
changed = true;
|
|
435
564
|
}
|
|
436
565
|
}
|
|
@@ -461,7 +590,7 @@ function buildAggregateTarget(tagId: number, occurrences: ToolOccurrence[]): Tag
|
|
|
461
590
|
const sentinel = "[truncated]";
|
|
462
591
|
let any = false;
|
|
463
592
|
for (const occ of occurrences) {
|
|
464
|
-
if (occ.part
|
|
593
|
+
if (setToolContentOrText(occ.part, sentinel)) {
|
|
465
594
|
any = true;
|
|
466
595
|
}
|
|
467
596
|
}
|
|
@@ -521,7 +650,7 @@ function buildToolTarget(
|
|
|
521
650
|
): TagTarget {
|
|
522
651
|
return {
|
|
523
652
|
setContent(content: string): boolean {
|
|
524
|
-
return part
|
|
653
|
+
return setToolContentOrText(part, content);
|
|
525
654
|
},
|
|
526
655
|
getContent(): string | null {
|
|
527
656
|
return part.getText() ?? null;
|
|
@@ -542,7 +671,7 @@ function buildToolTarget(
|
|
|
542
671
|
// via setToolOutput so the underlying tool_result content
|
|
543
672
|
// gets the truncation; falls back to setText for cases
|
|
544
673
|
// where the part type doesn't support setToolOutput.
|
|
545
|
-
const ok = part
|
|
674
|
+
const ok = setToolContentOrText(part, "[truncated]");
|
|
546
675
|
return ok ? "truncated" : "absent";
|
|
547
676
|
},
|
|
548
677
|
message: {
|