pi-ui-extend 1.0.39 → 1.0.41

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.
Files changed (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -0,0 +1,566 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, fstatSync, openSync, readSync } from "node:fs";
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+
5
+ import type {
6
+ CompressionBlock,
7
+ DcpLastNudge,
8
+ DcpNudgeAnchor,
9
+ DcpState,
10
+ } from "./state.js";
11
+
12
+ export const DCP_JOURNAL_CUSTOM_TYPE = "dcp-journal";
13
+ export const DCP_JOURNAL_SCHEMA_VERSION = 1;
14
+
15
+ const MAX_JOURNAL_OPERATION_BYTES = 1_000_000;
16
+ const JOURNAL_DURABLE_TAIL_BYTES = MAX_JOURNAL_OPERATION_BYTES + 128 * 1024;
17
+ const MAX_ASSIGNMENTS_PER_OPERATION = 20_000;
18
+ const MAX_BLOCKS_PER_OPERATION = 1_000;
19
+ const MAX_PRUNES_PER_OPERATION = 20_000;
20
+
21
+ export interface DcpJournalInitOperation {
22
+ schemaVersion: 1;
23
+ kind: "init";
24
+ operationId: string;
25
+ previousOperationId: null;
26
+ createdAt: number;
27
+ }
28
+
29
+ export interface DcpJournalPrunedTool {
30
+ toolCallId: string;
31
+ reason: string;
32
+ tokenEstimate: number;
33
+ }
34
+
35
+ export interface DcpJournalBlockState {
36
+ id: number;
37
+ active: boolean;
38
+ deactivatedReason?: string;
39
+ }
40
+
41
+ export interface DcpJournalDeltaOperation {
42
+ schemaVersion: 1;
43
+ kind: "delta";
44
+ operationId: string;
45
+ previousOperationId: string;
46
+ createdAt: number;
47
+ messageAssignments?: Array<[stableId: string, visibleId: string]>;
48
+ nextMessageId?: number;
49
+ blocks?: CompressionBlock[];
50
+ blockStates?: DcpJournalBlockState[];
51
+ prunedTools?: DcpJournalPrunedTool[];
52
+ manualMode?: boolean;
53
+ nudgeAnchors?: DcpNudgeAnchor[];
54
+ nextNudgeAnchorId?: number;
55
+ lastNudge?: DcpLastNudge | null;
56
+ }
57
+
58
+ export type DcpJournalOperation = DcpJournalInitOperation | DcpJournalDeltaOperation;
59
+
60
+ export interface DcpJournalReplayResult {
61
+ initialized: boolean;
62
+ lastOperationId?: string;
63
+ operationCount: number;
64
+ }
65
+
66
+ export interface DcpJournalPublicationOptions {
67
+ beforePublish?: () => void;
68
+ onPublished?: () => void;
69
+ }
70
+
71
+ export interface DcpJournalMirror {
72
+ lastOperationId: string;
73
+ messageAssignments: Map<string, string>;
74
+ blocks: Map<number, string>;
75
+ blockImmutable: Map<number, string>;
76
+ blockStates: Map<number, string>;
77
+ inactiveBlocks: Set<number>;
78
+ prunedTools: Set<string>;
79
+ manualMode: boolean;
80
+ nudgeState: string;
81
+ }
82
+
83
+ export class DcpJournalError extends Error {
84
+ constructor(message: string) {
85
+ super(message);
86
+ this.name = "DcpJournalError";
87
+ }
88
+ }
89
+
90
+ function isRecord(value: unknown): value is Record<string, unknown> {
91
+ return typeof value === "object" && value !== null && !Array.isArray(value);
92
+ }
93
+
94
+ function finiteNonNegativeInteger(value: unknown): value is number {
95
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
96
+ }
97
+
98
+ function stableJson(value: unknown): string {
99
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
100
+ if (value && typeof value === "object") {
101
+ const record = value as Record<string, unknown>;
102
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
103
+ }
104
+ return JSON.stringify(value) ?? "null";
105
+ }
106
+
107
+ function assertBounded(operation: DcpJournalOperation): void {
108
+ const bytes = Buffer.byteLength(JSON.stringify(operation), "utf8");
109
+ if (bytes > MAX_JOURNAL_OPERATION_BYTES) {
110
+ throw new DcpJournalError(`DCP journal operation exceeds ${MAX_JOURNAL_OPERATION_BYTES} bytes`);
111
+ }
112
+ }
113
+
114
+ function validateExactBlock(value: unknown): asserts value is CompressionBlock {
115
+ if (!isRecord(value)) throw new DcpJournalError("DCP journal block must be an object");
116
+ if (!finiteNonNegativeInteger(value.id) || value.id < 1) throw new DcpJournalError("DCP journal block id is invalid");
117
+ if (value.version !== 2) throw new DcpJournalError(`DCP journal block b${value.id} is not v2 exact`);
118
+ if (value.replacementMode !== "range" && value.replacementMode !== "message-body") {
119
+ throw new DcpJournalError(`DCP journal block b${value.id} has invalid replacement mode`);
120
+ }
121
+ for (const key of ["topic", "summary", "startMessageId", "endMessageId"] as const) {
122
+ if (typeof value[key] !== "string" || value[key].length === 0) {
123
+ throw new DcpJournalError(`DCP journal block b${value.id} is missing ${key}`);
124
+ }
125
+ }
126
+ for (const key of ["startTimestamp", "endTimestamp", "anchorTimestamp", "summaryTokenEstimate", "createdAt"] as const) {
127
+ if (typeof value[key] !== "number" || !Number.isFinite(value[key])) {
128
+ throw new DcpJournalError(`DCP journal block b${value.id} has invalid ${key}`);
129
+ }
130
+ }
131
+ for (const key of ["sourceMembers", "mutationMembers"] as const) {
132
+ const members = value[key];
133
+ if (!Array.isArray(members) || members.length === 0) {
134
+ throw new DcpJournalError(`DCP journal block b${value.id} is missing exact ${key}`);
135
+ }
136
+ const seen = new Set<string>();
137
+ for (const member of members) {
138
+ if (!isRecord(member) || typeof member.stableId !== "string" || member.stableId.length === 0 || typeof member.hash !== "string" || member.hash.length === 0) {
139
+ throw new DcpJournalError(`DCP journal block b${value.id} has malformed ${key}`);
140
+ }
141
+ if (seen.has(member.stableId)) throw new DcpJournalError(`DCP journal block b${value.id} repeats ${member.stableId}`);
142
+ seen.add(member.stableId);
143
+ }
144
+ }
145
+ if (!Array.isArray(value.protectedFragments)) {
146
+ throw new DcpJournalError(`DCP journal block b${value.id} is missing protectedFragments`);
147
+ }
148
+ for (const fragment of value.protectedFragments) {
149
+ if (!isRecord(fragment)
150
+ || (fragment.kind !== "user" && fragment.kind !== "prompt" && fragment.kind !== "tool")
151
+ || typeof fragment.origin !== "string" || fragment.origin.length === 0
152
+ || typeof fragment.hash !== "string" || !/^[a-f0-9]{64}$/i.test(fragment.hash)
153
+ || typeof fragment.text !== "string") {
154
+ throw new DcpJournalError(`DCP journal block b${value.id} has malformed protectedFragments`);
155
+ }
156
+ }
157
+ }
158
+
159
+ function validateAnchor(value: unknown): value is DcpNudgeAnchor {
160
+ if (!isRecord(value)) return false;
161
+ return finiteNonNegativeInteger(value.id)
162
+ && typeof value.type === "string"
163
+ && typeof value.anchorRole === "string"
164
+ && typeof value.turnIndex === "number"
165
+ && typeof value.createdAt === "number"
166
+ && typeof value.updatedAt === "number"
167
+ && typeof value.anchorTimestamp === "number"
168
+ && (value.anchorStableId === undefined || typeof value.anchorStableId === "string")
169
+ && (value.renderedReminder === undefined || typeof value.renderedReminder === "string");
170
+ }
171
+
172
+ export function validateDcpJournalOperation(value: unknown): DcpJournalOperation {
173
+ if (!isRecord(value)) throw new DcpJournalError("DCP journal operation must be an object");
174
+ if (value.schemaVersion !== DCP_JOURNAL_SCHEMA_VERSION) throw new DcpJournalError("Unsupported DCP journal schema version");
175
+ if (typeof value.operationId !== "string" || value.operationId.length < 8 || value.operationId.length > 200) {
176
+ throw new DcpJournalError("DCP journal operationId is invalid");
177
+ }
178
+ if (typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) throw new DcpJournalError("DCP journal createdAt is invalid");
179
+ if (value.kind === "init") {
180
+ if (value.previousOperationId !== null) throw new DcpJournalError("DCP journal init must start a chain");
181
+ const operation = value as unknown as DcpJournalInitOperation;
182
+ assertBounded(operation);
183
+ return operation;
184
+ }
185
+ if (value.kind !== "delta") throw new DcpJournalError("Unsupported DCP journal operation kind");
186
+ if (typeof value.previousOperationId !== "string" || value.previousOperationId.length === 0) {
187
+ throw new DcpJournalError("DCP journal delta is missing previousOperationId");
188
+ }
189
+ if (value.messageAssignments !== undefined) {
190
+ if (!Array.isArray(value.messageAssignments) || value.messageAssignments.length > MAX_ASSIGNMENTS_PER_OPERATION) {
191
+ throw new DcpJournalError("DCP journal message assignments are invalid");
192
+ }
193
+ for (const assignment of value.messageAssignments) {
194
+ if (!Array.isArray(assignment) || assignment.length !== 2 || typeof assignment[0] !== "string" || !assignment[0]
195
+ || typeof assignment[1] !== "string" || !/^m\d+$/.test(assignment[1])) {
196
+ throw new DcpJournalError("DCP journal message assignment is malformed");
197
+ }
198
+ }
199
+ }
200
+ if (value.nextMessageId !== undefined && (!finiteNonNegativeInteger(value.nextMessageId) || value.nextMessageId < 1)) {
201
+ throw new DcpJournalError("DCP journal nextMessageId is invalid");
202
+ }
203
+ if (value.blocks !== undefined) {
204
+ if (!Array.isArray(value.blocks) || value.blocks.length > MAX_BLOCKS_PER_OPERATION) throw new DcpJournalError("DCP journal blocks are invalid");
205
+ for (const block of value.blocks) validateExactBlock(block);
206
+ }
207
+ if (value.blockStates !== undefined) {
208
+ if (!Array.isArray(value.blockStates) || value.blockStates.length > MAX_BLOCKS_PER_OPERATION) throw new DcpJournalError("DCP journal block states are invalid");
209
+ for (const state of value.blockStates) {
210
+ if (!isRecord(state) || !finiteNonNegativeInteger(state.id) || state.id < 1 || typeof state.active !== "boolean"
211
+ || (state.deactivatedReason !== undefined && typeof state.deactivatedReason !== "string")) {
212
+ throw new DcpJournalError("DCP journal block state is malformed");
213
+ }
214
+ }
215
+ }
216
+ if (value.prunedTools !== undefined) {
217
+ if (!Array.isArray(value.prunedTools) || value.prunedTools.length > MAX_PRUNES_PER_OPERATION) throw new DcpJournalError("DCP journal pruned tools are invalid");
218
+ for (const item of value.prunedTools) {
219
+ if (!isRecord(item) || typeof item.toolCallId !== "string" || !item.toolCallId || typeof item.reason !== "string"
220
+ || typeof item.tokenEstimate !== "number" || !Number.isFinite(item.tokenEstimate) || item.tokenEstimate < 0) {
221
+ throw new DcpJournalError("DCP journal pruned tool is malformed");
222
+ }
223
+ }
224
+ }
225
+ if (value.manualMode !== undefined && typeof value.manualMode !== "boolean") throw new DcpJournalError("DCP journal manualMode is invalid");
226
+ if (value.nudgeAnchors !== undefined && (!Array.isArray(value.nudgeAnchors) || !value.nudgeAnchors.every(validateAnchor))) {
227
+ throw new DcpJournalError("DCP journal nudge anchors are invalid");
228
+ }
229
+ if (value.nextNudgeAnchorId !== undefined && (!finiteNonNegativeInteger(value.nextNudgeAnchorId) || value.nextNudgeAnchorId < 1)) {
230
+ throw new DcpJournalError("DCP journal nextNudgeAnchorId is invalid");
231
+ }
232
+ if (value.lastNudge !== undefined && value.lastNudge !== null && !isRecord(value.lastNudge)) {
233
+ throw new DcpJournalError("DCP journal lastNudge is invalid");
234
+ }
235
+ const operation = value as unknown as DcpJournalDeltaOperation;
236
+ assertBounded(operation);
237
+ return operation;
238
+ }
239
+
240
+ function blockStateKey(block: CompressionBlock): string {
241
+ return stableJson({ active: block.active, deactivatedReason: block.deactivatedReason });
242
+ }
243
+
244
+ function immutableBlockKey(block: CompressionBlock): string {
245
+ const { active: _active, deactivatedReason: _deactivatedReason, ...immutable } = block;
246
+ return stableJson(immutable);
247
+ }
248
+
249
+ function nudgeStateKey(state: Pick<DcpState, "nudgeAnchors" | "nextNudgeAnchorId" | "lastNudge">): string {
250
+ return stableJson({
251
+ nudgeAnchors: state.nudgeAnchors,
252
+ nextNudgeAnchorId: state.nextNudgeAnchorId,
253
+ lastNudge: state.lastNudge ?? null,
254
+ });
255
+ }
256
+
257
+ export function createDcpJournalMirror(state: DcpState, lastOperationId: string): DcpJournalMirror {
258
+ return {
259
+ lastOperationId,
260
+ messageAssignments: new Map(state.messageIdsByStableId),
261
+ blocks: new Map(state.compressionBlocks.map((block) => [block.id, stableJson(block)])),
262
+ blockImmutable: new Map(state.compressionBlocks.map((block) => [block.id, immutableBlockKey(block)])),
263
+ blockStates: new Map(state.compressionBlocks.map((block) => [block.id, blockStateKey(block)])),
264
+ inactiveBlocks: new Set(state.compressionBlocks.filter((block) => !block.active).map((block) => block.id)),
265
+ prunedTools: new Set(state.prunedToolIds),
266
+ manualMode: state.manualMode,
267
+ nudgeState: nudgeStateKey(state),
268
+ };
269
+ }
270
+
271
+ export function createDcpJournalInit(operationId: string = randomUUID(), createdAt = Date.now()): DcpJournalInitOperation {
272
+ return {
273
+ schemaVersion: DCP_JOURNAL_SCHEMA_VERSION,
274
+ kind: "init",
275
+ operationId,
276
+ previousOperationId: null,
277
+ createdAt,
278
+ };
279
+ }
280
+
281
+ export function buildDcpJournalDelta(
282
+ state: DcpState,
283
+ mirror: DcpJournalMirror,
284
+ operationId: string = randomUUID(),
285
+ createdAt = Date.now(),
286
+ ): DcpJournalDeltaOperation | undefined {
287
+ for (const [stableId, visibleId] of mirror.messageAssignments) {
288
+ const current = state.messageIdsByStableId.get(stableId);
289
+ if (current === undefined) throw new DcpJournalError(`Published message assignment ${stableId} was removed`);
290
+ if (current !== visibleId) throw new DcpJournalError(`Published message assignment ${stableId} was reassigned`);
291
+ }
292
+ const currentBlocksById = new Map(state.compressionBlocks.map((block) => [block.id, block]));
293
+ for (const blockId of mirror.blocks.keys()) {
294
+ if (!currentBlocksById.has(blockId)) throw new DcpJournalError(`Published DCP block b${blockId} was removed`);
295
+ }
296
+ for (const toolCallId of mirror.prunedTools) {
297
+ if (!state.prunedToolIds.has(toolCallId)) throw new DcpJournalError(`Published pruned tool ${toolCallId} was restored`);
298
+ }
299
+ for (const blockId of mirror.inactiveBlocks) {
300
+ if (currentBlocksById.get(blockId)?.active) throw new DcpJournalError(`Retired DCP block b${blockId} was reactivated`);
301
+ }
302
+
303
+ const messageAssignments = [...state.messageIdsByStableId.entries()].filter(([stableId, visibleId]) => mirror.messageAssignments.get(stableId) !== visibleId);
304
+ const blocks = state.compressionBlocks.filter((block) => mirror.blocks.get(block.id) !== stableJson(block));
305
+ const newBlockIds = new Set(blocks.filter((block) => !mirror.blocks.has(block.id)).map((block) => block.id));
306
+ const newBlocks = blocks.filter((block) => newBlockIds.has(block.id));
307
+ const changedExistingBlocks = blocks.filter((block) => mirror.blocks.has(block.id));
308
+ const blockStates = changedExistingBlocks.map((block) => ({
309
+ id: block.id,
310
+ active: block.active,
311
+ ...(block.deactivatedReason ? { deactivatedReason: block.deactivatedReason } : {}),
312
+ }));
313
+ // Existing blocks are immutable except for active/deactivation state. If any
314
+ // other field changed, fail rather than silently turning an update into a new
315
+ // representation of an already provider-visible summary.
316
+ for (const block of changedExistingBlocks) {
317
+ if (mirror.blockImmutable.get(block.id) !== immutableBlockKey(block)) {
318
+ throw new DcpJournalError(`DCP block b${block.id} changed after publication`);
319
+ }
320
+ }
321
+ const prunedTools: DcpJournalPrunedTool[] = [];
322
+ for (const toolCallId of state.prunedToolIds) {
323
+ if (mirror.prunedTools.has(toolCallId)) continue;
324
+ prunedTools.push({
325
+ toolCallId,
326
+ reason: state.prunedToolReasons.get(toolCallId) ?? "pruned",
327
+ tokenEstimate: Math.max(0, Math.round(state.toolCalls.get(toolCallId)?.tokenEstimate ?? 0)),
328
+ });
329
+ }
330
+ const nudgeChanged = nudgeStateKey(state) !== mirror.nudgeState;
331
+ const manualMode = state.manualMode !== mirror.manualMode ? state.manualMode : undefined;
332
+ if (messageAssignments.length === 0 && blocks.length === 0 && blockStates.length === 0 && prunedTools.length === 0 && manualMode === undefined && !nudgeChanged) {
333
+ return undefined;
334
+ }
335
+ const operation: DcpJournalDeltaOperation = {
336
+ schemaVersion: DCP_JOURNAL_SCHEMA_VERSION,
337
+ kind: "delta",
338
+ operationId,
339
+ previousOperationId: mirror.lastOperationId,
340
+ createdAt,
341
+ ...(messageAssignments.length > 0 ? { messageAssignments, nextMessageId: state.nextMessageId } : {}),
342
+ ...(newBlocks.length > 0 ? { blocks: newBlocks } : {}),
343
+ ...(blockStates.length > 0 ? { blockStates } : {}),
344
+ ...(prunedTools.length > 0 ? { prunedTools } : {}),
345
+ ...(manualMode === undefined ? {} : { manualMode }),
346
+ ...(nudgeChanged ? {
347
+ nudgeAnchors: structuredClone(state.nudgeAnchors),
348
+ nextNudgeAnchorId: state.nextNudgeAnchorId,
349
+ lastNudge: state.lastNudge ? structuredClone(state.lastNudge) : null,
350
+ } : {}),
351
+ };
352
+ return validateDcpJournalOperation(operation) as DcpJournalDeltaOperation;
353
+ }
354
+
355
+ function applyDelta(state: DcpState, delta: DcpJournalDeltaOperation): void {
356
+ if (delta.messageAssignments) {
357
+ const usedIds = new Set(state.messageIdsByStableId.values());
358
+ for (const [stableId, visibleId] of delta.messageAssignments) {
359
+ const existing = state.messageIdsByStableId.get(stableId);
360
+ if (existing && existing !== visibleId) throw new DcpJournalError(`Stable message ${stableId} was reassigned`);
361
+ if (!existing && usedIds.has(visibleId)) throw new DcpJournalError(`Visible message ID ${visibleId} was reused`);
362
+ state.messageIdsByStableId.set(stableId, visibleId);
363
+ usedIds.add(visibleId);
364
+ }
365
+ }
366
+ if (delta.nextMessageId !== undefined) state.nextMessageId = Math.max(state.nextMessageId, delta.nextMessageId);
367
+ if (delta.blocks) {
368
+ for (const raw of delta.blocks) {
369
+ validateExactBlock(raw);
370
+ const existing = state.compressionBlocks.find((block) => block.id === raw.id);
371
+ if (existing) {
372
+ if (stableJson(existing) !== stableJson(raw)) throw new DcpJournalError(`Compression block b${raw.id} conflicts with an earlier operation`);
373
+ continue;
374
+ }
375
+ state.compressionBlocks.push(structuredClone(raw));
376
+ state.nextBlockId = Math.max(state.nextBlockId, raw.id + 1);
377
+ }
378
+ }
379
+ if (delta.blockStates) {
380
+ for (const update of delta.blockStates) {
381
+ const block = state.compressionBlocks.find((candidate) => candidate.id === update.id);
382
+ if (!block) throw new DcpJournalError(`Block state refers to unknown b${update.id}`);
383
+ if (!block.active && update.active) throw new DcpJournalError(`Retired DCP block b${update.id} cannot be reactivated`);
384
+ block.active = update.active;
385
+ block.deactivatedReason = update.deactivatedReason;
386
+ }
387
+ }
388
+ if (delta.prunedTools) {
389
+ for (const item of delta.prunedTools) {
390
+ if (state.prunedToolIds.has(item.toolCallId)) continue;
391
+ state.prunedToolIds.add(item.toolCallId);
392
+ state.prunedToolReasons.set(item.toolCallId, item.reason);
393
+ state.accountedPrunedToolIds.add(item.toolCallId);
394
+ state.totalPruneCount += 1;
395
+ state.tokensSaved += Math.max(0, Math.round(item.tokenEstimate));
396
+ }
397
+ }
398
+ if (delta.manualMode !== undefined) state.manualMode = delta.manualMode;
399
+ if (delta.nudgeAnchors !== undefined) state.nudgeAnchors = structuredClone(delta.nudgeAnchors);
400
+ if (delta.nextNudgeAnchorId !== undefined) state.nextNudgeAnchorId = delta.nextNudgeAnchorId;
401
+ if (delta.lastNudge !== undefined) state.lastNudge = delta.lastNudge === null ? undefined : structuredClone(delta.lastNudge);
402
+ }
403
+
404
+ export function replayDcpJournal(entries: readonly unknown[], state: DcpState): DcpJournalReplayResult {
405
+ const operations: DcpJournalOperation[] = [];
406
+ for (const entry of entries) {
407
+ if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== DCP_JOURNAL_CUSTOM_TYPE) continue;
408
+ operations.push(validateDcpJournalOperation(entry.data));
409
+ }
410
+ if (operations.length === 0) return { initialized: false, operationCount: 0 };
411
+ const first = operations[0]!;
412
+ if (first.kind !== "init") throw new DcpJournalError("DCP journal does not begin with init");
413
+ let previous = first.operationId;
414
+ const seen = new Map<string, string>([[first.operationId, stableJson(first)]]);
415
+ for (let index = 1; index < operations.length; index += 1) {
416
+ const operation = operations[index]!;
417
+ const fingerprint = stableJson(operation);
418
+ const existing = seen.get(operation.operationId);
419
+ if (existing !== undefined) {
420
+ if (existing !== fingerprint) throw new DcpJournalError(`DCP journal operation ${operation.operationId} conflicts with an earlier operation`);
421
+ continue;
422
+ }
423
+ if (operation.kind !== "delta") throw new DcpJournalError("DCP journal contains a second init operation");
424
+ if (operation.previousOperationId !== previous) throw new DcpJournalError(`DCP journal predecessor mismatch at ${operation.operationId}`);
425
+ applyDelta(state, operation);
426
+ seen.set(operation.operationId, fingerprint);
427
+ previous = operation.operationId;
428
+ }
429
+ return { initialized: true, lastOperationId: previous, operationCount: operations.length };
430
+ }
431
+
432
+ function journalBranch(ctx: ExtensionContext): unknown[] {
433
+ try {
434
+ const branch = ctx.sessionManager.getBranch();
435
+ return Array.isArray(branch) ? branch : [];
436
+ } catch {
437
+ return [];
438
+ }
439
+ }
440
+
441
+ export async function readDcpJournalBranch(ctx: ExtensionContext): Promise<unknown[]> {
442
+ const manager = ctx.sessionManager as any;
443
+ if (typeof manager.readFullBranchEntries === "function") {
444
+ const entries = await manager.readFullBranchEntries();
445
+ return Array.isArray(entries) ? entries : [];
446
+ }
447
+ return journalBranch(ctx);
448
+ }
449
+
450
+ function restoreLeafAfterAppendFailure(ctx: ExtensionContext, previousLeafId: string | null | undefined): void {
451
+ const manager = ctx.sessionManager as any;
452
+ try {
453
+ if (previousLeafId) manager.branch?.(previousLeafId);
454
+ else manager.resetLeaf?.();
455
+ } catch {
456
+ // The append error remains authoritative. A reload is required before DCP
457
+ // can safely publish another operation.
458
+ }
459
+ }
460
+
461
+ function branchContainsJournalOperation(ctx: ExtensionContext, operationId: string): boolean {
462
+ return journalBranch(ctx).some((entry: any) =>
463
+ entry?.type === "custom"
464
+ && entry.customType === DCP_JOURNAL_CUSTOM_TYPE
465
+ && entry.data?.operationId === operationId,
466
+ );
467
+ }
468
+
469
+ /**
470
+ * `ExtensionAPI.appendEntry()` writes through SessionManager and then emits an
471
+ * `entry_appended` event. A listener can therefore throw after the JSONL append
472
+ * already succeeded. On the error path only, inspect the bounded durable tail
473
+ * so a confirmed commit is not reported as an uncommitted mutation. This is a
474
+ * read-only reconciliation check, not a second persistence backend.
475
+ */
476
+ function durableJournalOperationPresent(ctx: ExtensionContext, operationId: string): boolean {
477
+ const manager = ctx.sessionManager as any;
478
+ let sessionFile: unknown;
479
+ try {
480
+ sessionFile = manager.getSessionFile?.();
481
+ } catch {
482
+ return false;
483
+ }
484
+ if (typeof sessionFile !== "string" || sessionFile.length === 0) return false;
485
+
486
+ let fd: number | undefined;
487
+ try {
488
+ fd = openSync(sessionFile, "r");
489
+ const size = fstatSync(fd).size;
490
+ if (size <= 0) return false;
491
+ const length = Math.min(size, JOURNAL_DURABLE_TAIL_BYTES);
492
+ const buffer = Buffer.allocUnsafe(length);
493
+ const bytes = readSync(fd, buffer, 0, length, size - length);
494
+ const lines = buffer.subarray(0, bytes).toString("utf8").split("\n");
495
+ // The first line may begin in the middle of a large preceding JSON record;
496
+ // malformed/partial lines are ignored and never count as a durable commit.
497
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
498
+ const line = lines[index]!.trim();
499
+ if (!line) continue;
500
+ try {
501
+ const entry = JSON.parse(line);
502
+ if (
503
+ entry?.type === "custom"
504
+ && entry.customType === DCP_JOURNAL_CUSTOM_TYPE
505
+ && entry.data?.operationId === operationId
506
+ ) return true;
507
+ } catch {
508
+ // Keep scanning complete earlier lines in the bounded tail.
509
+ }
510
+ }
511
+ } catch {
512
+ return false;
513
+ } finally {
514
+ if (fd !== undefined) {
515
+ try { closeSync(fd); } catch { /* best-effort reconciliation read */ }
516
+ }
517
+ }
518
+ return false;
519
+ }
520
+
521
+ export function appendDcpJournalOperation(
522
+ pi: ExtensionAPI,
523
+ ctx: ExtensionContext,
524
+ operation: DcpJournalOperation,
525
+ ): void {
526
+ validateDcpJournalOperation(operation);
527
+ const manager = ctx.sessionManager as any;
528
+ const previousLeafId = typeof manager.getLeafId === "function" ? manager.getLeafId() : undefined;
529
+ try {
530
+ pi.appendEntry(DCP_JOURNAL_CUSTOM_TYPE, operation);
531
+ } catch (error) {
532
+ const durable = durableJournalOperationPresent(ctx, operation.operationId);
533
+ if (durable && branchContainsJournalOperation(ctx, operation.operationId)) {
534
+ // The session write committed and only post-append notification failed.
535
+ // Treat it as committed so callers do not retry the mutation.
536
+ return;
537
+ }
538
+ if (durable) {
539
+ throw new DcpJournalError(
540
+ "DCP journal append is durable but the active in-memory branch no longer agrees; reload the session before continuing",
541
+ );
542
+ }
543
+ restoreLeafAfterAppendFailure(ctx, previousLeafId);
544
+ throw new DcpJournalError(`Failed to append DCP journal operation: ${error instanceof Error ? error.message : String(error)}`);
545
+ }
546
+ if (!branchContainsJournalOperation(ctx, operation.operationId)) {
547
+ restoreLeafAfterAppendFailure(ctx, previousLeafId);
548
+ throw new DcpJournalError("DCP journal append did not become part of the active session branch");
549
+ }
550
+ }
551
+
552
+
553
+ export function appendDcpJournalDelta(
554
+ pi: ExtensionAPI,
555
+ ctx: ExtensionContext,
556
+ state: DcpState,
557
+ mirror: DcpJournalMirror,
558
+ publication: DcpJournalPublicationOptions = {},
559
+ ): DcpJournalMirror {
560
+ const delta = buildDcpJournalDelta(state, mirror);
561
+ if (!delta) return mirror;
562
+ publication.beforePublish?.();
563
+ appendDcpJournalOperation(pi, ctx, delta);
564
+ publication.onPublished?.();
565
+ return createDcpJournalMirror(state, delta.operationId);
566
+ }