ds4-context-engine 0.3.3 → 0.3.5
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 +47 -6
- package/docs/ADR/059-optional-anchored-editing.md +51 -0
- package/docs/ADR/060-optional-portable-agent-tools.md +38 -0
- package/docs/ADR/061-compaction-latency.md +21 -0
- package/docs/ADR/README.md +3 -0
- package/docs/ANCHORED_EDITING.md +144 -0
- package/docs/ARCHITECTURE.md +38 -0
- package/docs/COMPACTION.md +33 -11
- package/docs/PORTABLE_AGENT_TOOLS.md +86 -0
- package/docs/releases/0.3.3.md +22 -2
- package/docs/releases/0.3.4.md +81 -0
- package/docs/releases/0.3.5.md +70 -0
- package/package.json +2 -2
- package/src/extension/adaptive-read-tool.ts +47 -0
- package/src/extension/anchored-edit-tool.ts +157 -0
- package/src/extension/anchored-edit.ts +53 -0
- package/src/extension/bash-job-tool.ts +149 -0
- package/src/extension/commands.ts +12 -0
- package/src/extension/index.ts +18 -0
- package/src/extension/post-edit-report.ts +134 -0
- package/src/extension/runtime.ts +10 -5
- package/src/pi-adapter/compaction-coordinator.ts +168 -76
- package/src/pi-adapter/compaction-workers.ts +43 -0
- package/src/pi-adapter/summary-generator.ts +5 -6
- package/src/pi-adapter/version.ts +1 -1
- package/src/tools/bash-job-manager.ts +176 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { compactionInputBudget } from "ds4-context-core/compaction/input-budget";
|
|
3
|
+
import { mapCompactionSegments } from "./compaction-workers.ts";
|
|
1
4
|
import type {
|
|
2
5
|
CompactionResult,
|
|
3
6
|
ExtensionContext,
|
|
@@ -47,6 +50,7 @@ import {
|
|
|
47
50
|
buildAggregateSummaryPrompt,
|
|
48
51
|
buildSummaryPrompt,
|
|
49
52
|
computeAggregateSourceHash,
|
|
53
|
+
computeUpdateSourceHash,
|
|
50
54
|
type SummaryValidationStatus,
|
|
51
55
|
} from "ds4-context-core/compaction/summary-contract";
|
|
52
56
|
|
|
@@ -63,6 +67,14 @@ export type CompactionPhase =
|
|
|
63
67
|
| "pi-default"
|
|
64
68
|
| "failed";
|
|
65
69
|
|
|
70
|
+
export interface CompactionTimings {
|
|
71
|
+
preparationMs: number;
|
|
72
|
+
generationMs: number;
|
|
73
|
+
aggregationMs: number;
|
|
74
|
+
persistenceMs: number;
|
|
75
|
+
totalMs: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
66
78
|
export interface CompactionDiagnostics {
|
|
67
79
|
enabled: boolean;
|
|
68
80
|
validate: boolean;
|
|
@@ -83,6 +95,14 @@ export interface CompactionDiagnostics {
|
|
|
83
95
|
segmentCount?: number;
|
|
84
96
|
aggregateCalls?: number;
|
|
85
97
|
transportRetries?: number;
|
|
98
|
+
path?: "direct-update" | "hierarchical";
|
|
99
|
+
provider?: string;
|
|
100
|
+
model?: string;
|
|
101
|
+
inputBudgetMode?: "summary" | "context";
|
|
102
|
+
directPromptTokens?: number;
|
|
103
|
+
maxConcurrentSegments?: number;
|
|
104
|
+
summaryCalls?: number;
|
|
105
|
+
timings?: CompactionTimings;
|
|
86
106
|
contextTokens?: number;
|
|
87
107
|
softLimitTokens?: number;
|
|
88
108
|
proactiveThresholdTokens?: number;
|
|
@@ -110,6 +130,7 @@ export interface SummaryGraphDiagnostics {
|
|
|
110
130
|
segmentNodes: number;
|
|
111
131
|
aggregateNodes: number;
|
|
112
132
|
branchNodes: number;
|
|
133
|
+
taskStateNodes: number;
|
|
113
134
|
maxGraphLevel: number;
|
|
114
135
|
rootSummaryIds: string[];
|
|
115
136
|
activeSummaryId?: string;
|
|
@@ -212,6 +233,7 @@ export class CompactionCoordinator {
|
|
|
212
233
|
event: SessionBeforeCompactEvent,
|
|
213
234
|
ctx: ExtensionContext,
|
|
214
235
|
): Promise<{ compaction?: CompactionResult } | undefined> {
|
|
236
|
+
const startedAt = performance.now();
|
|
215
237
|
const config = this.dependencies.config;
|
|
216
238
|
const model = this.resolveCompactionModel(ctx) ?? ctx.model;
|
|
217
239
|
if (!config.compaction.enabled || !config.enabled || !model || config.context.maxSummaryTokens <= 0) {
|
|
@@ -219,6 +241,25 @@ export class CompactionCoordinator {
|
|
|
219
241
|
}
|
|
220
242
|
|
|
221
243
|
const requestedAt = this.dependencies.now();
|
|
244
|
+
const timings: CompactionTimings = {
|
|
245
|
+
preparationMs: performance.now() - startedAt,
|
|
246
|
+
generationMs: 0,
|
|
247
|
+
aggregationMs: 0,
|
|
248
|
+
persistenceMs: 0,
|
|
249
|
+
totalMs: 0,
|
|
250
|
+
};
|
|
251
|
+
const measure = async <T>(
|
|
252
|
+
phase: Exclude<keyof CompactionTimings, "totalMs">,
|
|
253
|
+
run: () => T | Promise<T>,
|
|
254
|
+
): Promise<T> => {
|
|
255
|
+
const start = performance.now();
|
|
256
|
+
try {
|
|
257
|
+
return await run();
|
|
258
|
+
} finally {
|
|
259
|
+
timings[phase] += performance.now() - start;
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const maxConcurrentSegments = config.compaction.maxConcurrentSegments ?? 2;
|
|
222
263
|
const trigger: CompactionTrigger = this.proactiveRequested ? "proactive" : event.reason;
|
|
223
264
|
this.state = {
|
|
224
265
|
phase: "generating",
|
|
@@ -227,28 +268,43 @@ export class CompactionCoordinator {
|
|
|
227
268
|
firstKeptEntryId: event.preparation.firstKeptEntryId,
|
|
228
269
|
tokensBefore: event.preparation.tokensBefore,
|
|
229
270
|
transportRetries: 0,
|
|
271
|
+
aggregateCalls: 0,
|
|
272
|
+
summaryCalls: 0,
|
|
273
|
+
provider: model.provider,
|
|
274
|
+
model: model.id,
|
|
275
|
+
inputBudgetMode: config.compaction.inputBudget ?? "summary",
|
|
276
|
+
maxConcurrentSegments,
|
|
277
|
+
timings,
|
|
230
278
|
};
|
|
231
279
|
|
|
232
280
|
try {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
281
|
+
const { source, inputBudgetTokens, wholePlan, directPlan, segmentPlans } = await measure("preparationMs", () => {
|
|
282
|
+
if (event.signal.aborted) throw new Error("Compaction summary generation aborted");
|
|
283
|
+
this.dependencies.syncSessionIndex(ctx);
|
|
284
|
+
const source = prepareCompactionSource(event);
|
|
285
|
+
const inputBudgetTokens = this.inputBudgetTokens(model);
|
|
286
|
+
if (inputBudgetTokens <= 0) throw new Error("Active model has no safe compaction input budget");
|
|
287
|
+
const wholePlan = this.buildSegmentPlan(source, event, model.provider);
|
|
288
|
+
const update = (config.compaction.directUpdate ?? true) && source.previousSummary
|
|
289
|
+
? this.buildSegmentPlan({
|
|
290
|
+
...source,
|
|
291
|
+
segmentReadFiles: source.readFiles,
|
|
292
|
+
segmentModifiedFiles: source.modifiedFiles,
|
|
293
|
+
}, event, model.provider, source.previousSummary)
|
|
294
|
+
: undefined;
|
|
295
|
+
const directPlan = update && update.promptTokens <= inputBudgetTokens ? update : undefined;
|
|
296
|
+
this.state = {
|
|
297
|
+
...this.state,
|
|
298
|
+
path: directPlan ? "direct-update" : "hierarchical",
|
|
299
|
+
sourceEntries: source.sourceEntryIds.length,
|
|
300
|
+
inputBudgetTokens,
|
|
301
|
+
sourcePromptTokens: wholePlan.promptTokens,
|
|
302
|
+
...(update ? { directPromptTokens: update.promptTokens } : {}),
|
|
303
|
+
};
|
|
304
|
+
const segmentPlans = directPlan ? [] : this.partitionSegmentPlans(source, wholePlan, event, model.provider, inputBudgetTokens);
|
|
305
|
+
this.state.segmentCount = segmentPlans.length;
|
|
306
|
+
return { source, inputBudgetTokens, wholePlan, directPlan, segmentPlans };
|
|
307
|
+
});
|
|
252
308
|
|
|
253
309
|
const usedIds = new Set(this.graphRecords.keys());
|
|
254
310
|
if (source.previousNode) usedIds.add(source.previousNode.id);
|
|
@@ -269,53 +325,59 @@ export class CompactionCoordinator {
|
|
|
269
325
|
};
|
|
270
326
|
|
|
271
327
|
const usages: GeneratedSummary["usage"][] = [];
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
328
|
+
// Allocate in source order before concurrent requests; completion order must
|
|
329
|
+
// never determine IDs, graph edges, source order or usage accumulation.
|
|
330
|
+
const plans = directPlan ? [directPlan] : segmentPlans;
|
|
331
|
+
const ids = plans.map(() => nextId());
|
|
332
|
+
const createdNodes: EmbeddedSummaryNode[] = [];
|
|
333
|
+
let previousNode = source.previousNode;
|
|
334
|
+
if (!previousNode && source.previousSummary) {
|
|
335
|
+
previousNode = importUntrackedPreviousSummary({
|
|
336
|
+
id: nextId(),
|
|
337
|
+
content: source.previousSummary,
|
|
338
|
+
createdAt: this.dependencies.now(),
|
|
339
|
+
provider: model.provider,
|
|
340
|
+
model: model.id,
|
|
341
|
+
});
|
|
342
|
+
createdNodes.push(previousNode);
|
|
343
|
+
}
|
|
344
|
+
const results = await measure("generationMs", () => mapCompactionSegments(
|
|
345
|
+
plans, maxConcurrentSegments, event.signal, (plan, _index, signal) => this.generateSummary({
|
|
346
|
+
stage: directPlan ? "update" : "segment",
|
|
276
347
|
prompt: plan.prompt,
|
|
277
348
|
validationSource: plan.validationSource,
|
|
278
349
|
readFiles: plan.readFiles,
|
|
279
350
|
modifiedFiles: plan.modifiedFiles,
|
|
280
|
-
event,
|
|
351
|
+
event: { ...event, signal },
|
|
281
352
|
ctx,
|
|
282
353
|
model,
|
|
283
|
-
thinking:
|
|
284
|
-
})
|
|
354
|
+
thinking: config.compaction.summary?.thinking,
|
|
355
|
+
}),
|
|
356
|
+
));
|
|
357
|
+
const generatedNodes: EmbeddedSummaryNode[] = results.map((generated, index) => {
|
|
358
|
+
const plan = plans[index] as SegmentGenerationPlan;
|
|
359
|
+
const predecessor = directPlan ? previousNode : undefined;
|
|
285
360
|
usages.push(generated.usage);
|
|
286
|
-
|
|
287
|
-
id:
|
|
288
|
-
kind: "segment",
|
|
361
|
+
return {
|
|
362
|
+
id: ids[index] as string,
|
|
363
|
+
kind: predecessor ? "task-state" : "segment",
|
|
289
364
|
content: classifiedSummary(generated.content, plan.classification),
|
|
290
|
-
sourceHash: plan.source.sourceHash,
|
|
291
|
-
sourceEntryIds: [...plan.source.sourceEntryIds],
|
|
292
|
-
childSummaryIds: [],
|
|
293
|
-
graphLevel: 0,
|
|
365
|
+
sourceHash: predecessor ? computeUpdateSourceHash(source.sourceHash, predecessor) : plan.source.sourceHash,
|
|
366
|
+
sourceEntryIds: unique([...(predecessor?.sourceEntryIds ?? []), ...plan.source.sourceEntryIds]),
|
|
367
|
+
childSummaryIds: predecessor ? [predecessor.id] : [],
|
|
368
|
+
graphLevel: predecessor ? predecessor.graphLevel + 1 : 0,
|
|
294
369
|
createdAt: this.dependencies.now(),
|
|
295
370
|
validationStatus: generated.validation.status,
|
|
296
371
|
validationIssueCodes: unique(generated.validation.issues.map((issue) => issue.code)),
|
|
297
372
|
provider: model.provider,
|
|
298
373
|
model: model.id,
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
const segmentId =
|
|
302
|
-
if (!segmentId) throw new Error("Compaction
|
|
303
|
-
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
if (!previousNode && source.previousSummary) {
|
|
307
|
-
previousNode = importUntrackedPreviousSummary({
|
|
308
|
-
id: nextId(),
|
|
309
|
-
content: source.previousSummary,
|
|
310
|
-
createdAt: this.dependencies.now(),
|
|
311
|
-
provider: model.provider,
|
|
312
|
-
model: model.id,
|
|
313
|
-
});
|
|
314
|
-
createdNodes.push(previousNode);
|
|
315
|
-
}
|
|
316
|
-
createdNodes.push(...segmentNodes);
|
|
317
|
-
const roots = [...(previousNode ? [previousNode] : []), ...segmentNodes];
|
|
318
|
-
const aggregated = await this.aggregateSummaryNodes({
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
const segmentId = generatedNodes[0]?.id;
|
|
377
|
+
if (!segmentId) throw new Error("Compaction produced no summary node");
|
|
378
|
+
createdNodes.push(...generatedNodes);
|
|
379
|
+
const roots = [...(!directPlan && previousNode ? [previousNode] : []), ...generatedNodes];
|
|
380
|
+
const aggregated = await measure("aggregationMs", () => this.aggregateSummaryNodes({
|
|
319
381
|
roots,
|
|
320
382
|
event,
|
|
321
383
|
ctx,
|
|
@@ -328,29 +390,34 @@ export class CompactionCoordinator {
|
|
|
328
390
|
createdNodes,
|
|
329
391
|
usages,
|
|
330
392
|
modelObject: model,
|
|
331
|
-
});
|
|
393
|
+
}));
|
|
332
394
|
const activeNode = aggregated.activeNode;
|
|
333
395
|
const embeddedNodes = createdNodes.filter((node) => node.id !== activeNode.id);
|
|
334
396
|
|
|
335
|
-
const records =
|
|
336
|
-
|
|
337
|
-
node
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
397
|
+
const records = await measure("persistenceMs", () => {
|
|
398
|
+
if (event.signal.aborted) throw new Error("Compaction summary generation aborted");
|
|
399
|
+
const records = embeddedNodes.map((node) => createSummaryRecord({
|
|
400
|
+
sessionId: this.dependencies.sessionId,
|
|
401
|
+
node,
|
|
402
|
+
boundary,
|
|
403
|
+
segmentSummaryId: node.kind === "segment" ? node.id : segmentId,
|
|
404
|
+
lifecycleStatus: "prepared",
|
|
405
|
+
}));
|
|
406
|
+
records.push(createSummaryRecord({
|
|
407
|
+
sessionId: this.dependencies.sessionId,
|
|
408
|
+
node: activeNode,
|
|
409
|
+
boundary,
|
|
410
|
+
segmentSummaryId: segmentId,
|
|
411
|
+
lifecycleStatus: "prepared",
|
|
412
|
+
embeddedNodes,
|
|
413
|
+
}));
|
|
414
|
+
if (this.dependencies.persisted) this.dependencies.database?.summaries.saveGraph(records);
|
|
415
|
+
for (const record of records) this.graphRecords.set(record.id, record);
|
|
416
|
+
this.pendingSummaryIds = records.map((record) => record.id);
|
|
417
|
+
return records;
|
|
418
|
+
});
|
|
353
419
|
this.state = {
|
|
420
|
+
...this.state,
|
|
354
421
|
phase: "prepared",
|
|
355
422
|
trigger,
|
|
356
423
|
summaryId: activeNode.id,
|
|
@@ -409,6 +476,16 @@ export class CompactionCoordinator {
|
|
|
409
476
|
ctx.ui.notify(`DS4 compaction unavailable; using Pi default. ${message}`, "warning");
|
|
410
477
|
}
|
|
411
478
|
return undefined;
|
|
479
|
+
} finally {
|
|
480
|
+
timings.totalMs = performance.now() - startedAt;
|
|
481
|
+
this.dependencies.logger.debug("compaction.timings", {
|
|
482
|
+
path: this.state.path,
|
|
483
|
+
phase: this.state.phase,
|
|
484
|
+
provider: model.provider,
|
|
485
|
+
model: model.id,
|
|
486
|
+
summaryCalls: this.state.summaryCalls,
|
|
487
|
+
...timings,
|
|
488
|
+
});
|
|
412
489
|
}
|
|
413
490
|
}
|
|
414
491
|
|
|
@@ -433,7 +510,10 @@ export class CompactionCoordinator {
|
|
|
433
510
|
const attempt = this.state;
|
|
434
511
|
const customError = attempt.lastError;
|
|
435
512
|
this.state = {
|
|
513
|
+
...attempt,
|
|
436
514
|
phase: "pi-default",
|
|
515
|
+
summaryId: undefined,
|
|
516
|
+
validationStatus: undefined,
|
|
437
517
|
trigger: event.reason,
|
|
438
518
|
firstKeptEntryId: event.compactionEntry.firstKeptEntryId,
|
|
439
519
|
tokensBefore: event.compactionEntry.tokensBefore,
|
|
@@ -467,6 +547,7 @@ export class CompactionCoordinator {
|
|
|
467
547
|
this.pendingSummaryIds = [];
|
|
468
548
|
const metadata = details.ds4ContextEngine;
|
|
469
549
|
this.state = {
|
|
550
|
+
...this.state,
|
|
470
551
|
phase: "committed",
|
|
471
552
|
trigger: metadata.reason,
|
|
472
553
|
summaryId: metadata.summaryId,
|
|
@@ -474,7 +555,7 @@ export class CompactionCoordinator {
|
|
|
474
555
|
validationStatus: metadata.validationStatus,
|
|
475
556
|
firstKeptEntryId: effectiveEntry.firstKeptEntryId,
|
|
476
557
|
tokensBefore: effectiveEntry.tokensBefore,
|
|
477
|
-
requestedAt: metadata.generatedAt,
|
|
558
|
+
requestedAt: this.state.requestedAt ?? metadata.generatedAt,
|
|
478
559
|
completedAt: this.dependencies.now(),
|
|
479
560
|
...(this.state.inputBudgetTokens !== undefined
|
|
480
561
|
? { inputBudgetTokens: this.state.inputBudgetTokens }
|
|
@@ -660,6 +741,7 @@ export class CompactionCoordinator {
|
|
|
660
741
|
segmentNodes: records.filter((record) => record.kind === "segment").length,
|
|
661
742
|
aggregateNodes: records.filter((record) => record.kind === "aggregate").length,
|
|
662
743
|
branchNodes: records.filter((record) => record.kind === "branch").length,
|
|
744
|
+
taskStateNodes: records.filter((record) => record.kind === "task-state").length,
|
|
663
745
|
maxGraphLevel: records.reduce((maximum, record) => Math.max(maximum, record.graphLevel), 0),
|
|
664
746
|
rootSummaryIds: roots,
|
|
665
747
|
...(activeId ? { activeSummaryId: activeId } : {}),
|
|
@@ -717,7 +799,8 @@ export class CompactionCoordinator {
|
|
|
717
799
|
config.context.recentTailTokens,
|
|
718
800
|
),
|
|
719
801
|
};
|
|
720
|
-
|
|
802
|
+
const maxOutputTokens = Math.max(1, Math.min(config.context.maxSummaryTokens, model.maxTokens ?? config.context.maxSummaryTokens));
|
|
803
|
+
return compactionInputBudget(resolved.budget, maxOutputTokens, config.compaction.inputBudget ?? "summary");
|
|
721
804
|
}
|
|
722
805
|
|
|
723
806
|
private classify(text: string, provider: string): {
|
|
@@ -743,7 +826,9 @@ export class CompactionCoordinator {
|
|
|
743
826
|
source: PreparedCompactionSourceSlice,
|
|
744
827
|
event: SessionBeforeCompactEvent,
|
|
745
828
|
provider: string,
|
|
829
|
+
previousSummary?: string,
|
|
746
830
|
): SegmentGenerationPlan {
|
|
831
|
+
const previousPrivacy = previousSummary === undefined ? undefined : this.classify(previousSummary, provider);
|
|
747
832
|
const sourcePrivacy = this.classify(source.conversationText, provider);
|
|
748
833
|
const validationPrivacy = this.classify(source.sourceText, provider);
|
|
749
834
|
const instructionPrivacy = event.customInstructions
|
|
@@ -754,6 +839,7 @@ export class CompactionCoordinator {
|
|
|
754
839
|
const classification = [
|
|
755
840
|
sourcePrivacy.classification,
|
|
756
841
|
validationPrivacy.classification,
|
|
842
|
+
...(previousPrivacy ? [previousPrivacy.classification] : []),
|
|
757
843
|
...(instructionPrivacy ? [instructionPrivacy.classification] : []),
|
|
758
844
|
...readFilePrivacy.map((item) => item.classification),
|
|
759
845
|
...modifiedFilePrivacy.map((item) => item.classification),
|
|
@@ -762,6 +848,7 @@ export class CompactionCoordinator {
|
|
|
762
848
|
const modifiedFiles = modifiedFilePrivacy.map((item) => item.value);
|
|
763
849
|
const prompt = buildSummaryPrompt({
|
|
764
850
|
conversationText: sourcePrivacy.value,
|
|
851
|
+
...(previousPrivacy ? { previousSummary: previousPrivacy.value, purpose: "update" as const } : {}),
|
|
765
852
|
...(instructionPrivacy ? { customInstructions: instructionPrivacy.value } : {}),
|
|
766
853
|
readFiles,
|
|
767
854
|
modifiedFiles,
|
|
@@ -770,7 +857,7 @@ export class CompactionCoordinator {
|
|
|
770
857
|
return {
|
|
771
858
|
source,
|
|
772
859
|
prompt,
|
|
773
|
-
validationSource: validationPrivacy.value,
|
|
860
|
+
validationSource: [previousPrivacy?.value, validationPrivacy.value].filter(Boolean).join("\n\n"),
|
|
774
861
|
readFiles,
|
|
775
862
|
modifiedFiles,
|
|
776
863
|
classification,
|
|
@@ -950,6 +1037,7 @@ export class CompactionCoordinator {
|
|
|
950
1037
|
if (plan.promptTokens > input.inputBudgetTokens) {
|
|
951
1038
|
throw new Error("Compaction aggregate prompt exceeded its preflight input budget");
|
|
952
1039
|
}
|
|
1040
|
+
this.state.aggregateCalls = aggregateCalls;
|
|
953
1041
|
const generated = await this.generateSummary({
|
|
954
1042
|
stage: "aggregate",
|
|
955
1043
|
prompt: plan.prompt,
|
|
@@ -991,7 +1079,7 @@ export class CompactionCoordinator {
|
|
|
991
1079
|
}
|
|
992
1080
|
|
|
993
1081
|
private generateSummary(input: {
|
|
994
|
-
stage: "segment" | "aggregate";
|
|
1082
|
+
stage: "segment" | "aggregate" | "update";
|
|
995
1083
|
prompt: string;
|
|
996
1084
|
validationSource: string;
|
|
997
1085
|
readFiles: readonly string[];
|
|
@@ -1001,6 +1089,7 @@ export class CompactionCoordinator {
|
|
|
1001
1089
|
model: Model<Api>;
|
|
1002
1090
|
thinking?: CompactionThinkingLevel;
|
|
1003
1091
|
}): Promise<GeneratedSummary> {
|
|
1092
|
+
this.state.summaryCalls = (this.state.summaryCalls ?? 0) + 1;
|
|
1004
1093
|
return generateValidatedSummary({
|
|
1005
1094
|
...input,
|
|
1006
1095
|
validate: this.dependencies.config.compaction.validate,
|
|
@@ -1101,6 +1190,8 @@ export class CompactionCoordinator {
|
|
|
1101
1190
|
validationStatus: summary.validationStatus,
|
|
1102
1191
|
firstKeptEntryId: summary.firstKeptEntryId,
|
|
1103
1192
|
tokensBefore: summary.tokensBefore,
|
|
1193
|
+
provider: summary.provider,
|
|
1194
|
+
model: summary.model,
|
|
1104
1195
|
requestedAt: summary.createdAt,
|
|
1105
1196
|
...(summary.lifecycleStatus !== "prepared" ? { completedAt: summary.createdAt } : {}),
|
|
1106
1197
|
...(summary.lifecycleStatus === "failed" ? { lastError: "Prepared summary was not committed by Pi" } : {}),
|
|
@@ -1128,6 +1219,7 @@ export function defaultSummaryGraphDiagnostics(): SummaryGraphDiagnostics {
|
|
|
1128
1219
|
segmentNodes: 0,
|
|
1129
1220
|
aggregateNodes: 0,
|
|
1130
1221
|
branchNodes: 0,
|
|
1222
|
+
taskStateNodes: 0,
|
|
1131
1223
|
maxGraphLevel: 0,
|
|
1132
1224
|
rootSummaryIds: [],
|
|
1133
1225
|
activePathIds: [],
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered results from bounded, abort-cooperative work. A failure cancels peers
|
|
3
|
+
* and drains every started worker before returning, so fallback cannot overlap
|
|
4
|
+
* with unfinished custom requests. No new item starts after failure/abort.
|
|
5
|
+
*/
|
|
6
|
+
export async function mapCompactionSegments<T, R>(
|
|
7
|
+
items: readonly T[],
|
|
8
|
+
concurrency: number,
|
|
9
|
+
signal: AbortSignal,
|
|
10
|
+
run: (item: T, index: number, signal: AbortSignal) => Promise<R>,
|
|
11
|
+
): Promise<R[]> {
|
|
12
|
+
if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 2) {
|
|
13
|
+
throw new Error("Compaction segment concurrency must be an integer between 1 and 2");
|
|
14
|
+
}
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const abort = (): void => controller.abort();
|
|
17
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
18
|
+
if (signal.aborted) abort();
|
|
19
|
+
const results: R[] = new Array(items.length);
|
|
20
|
+
let next = 0;
|
|
21
|
+
let failure: unknown;
|
|
22
|
+
let failed = false;
|
|
23
|
+
const worker = async (): Promise<void> => {
|
|
24
|
+
while (!controller.signal.aborted) {
|
|
25
|
+
const index = next++;
|
|
26
|
+
if (index >= items.length) return;
|
|
27
|
+
try {
|
|
28
|
+
results[index] = await run(items[index] as T, index, controller.signal);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (!failed) { failure = error; failed = true; }
|
|
31
|
+
controller.abort();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
try {
|
|
36
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
|
|
37
|
+
if (failed) throw failure;
|
|
38
|
+
if (controller.signal.aborted) throw new Error("Compaction summary generation aborted");
|
|
39
|
+
return results;
|
|
40
|
+
} finally {
|
|
41
|
+
signal.removeEventListener("abort", abort);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -18,9 +18,8 @@ export const DEFAULT_COMPACTION_TRANSPORT_BASE_DELAY_MS = 2000;
|
|
|
18
18
|
export const COMPACTION_TRANSPORT_MAX_DELAY_MS = 60_000;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Transport retry policy for compaction summary requests
|
|
22
|
-
*
|
|
23
|
-
* exponential backoff, abort-aware).
|
|
21
|
+
* Transport retry policy for compaction summary requests: three total attempts
|
|
22
|
+
* (not three retries), 2000 ms base delay, exponential backoff, abort-aware.
|
|
24
23
|
*/
|
|
25
24
|
export interface CompactionTransportPolicy {
|
|
26
25
|
/** Total attempts for transport-classified failures. Default: 3. */
|
|
@@ -48,7 +47,7 @@ export function transportRetryDelayMs(baseDelayMs: number, failedAttempt: number
|
|
|
48
47
|
}
|
|
49
48
|
|
|
50
49
|
export interface CompactionTransportRetryDiagnostic {
|
|
51
|
-
stage: "segment" | "aggregate";
|
|
50
|
+
stage: "segment" | "aggregate" | "update";
|
|
52
51
|
failedAttempt: number;
|
|
53
52
|
nextAttempt: number;
|
|
54
53
|
maxAttempts: number;
|
|
@@ -56,7 +55,7 @@ export interface CompactionTransportRetryDiagnostic {
|
|
|
56
55
|
}
|
|
57
56
|
|
|
58
57
|
export interface GenerateValidatedSummaryInput {
|
|
59
|
-
stage: "segment" | "aggregate";
|
|
58
|
+
stage: "segment" | "aggregate" | "update";
|
|
60
59
|
prompt: string;
|
|
61
60
|
validationSource: string;
|
|
62
61
|
readFiles: readonly string[];
|
|
@@ -69,7 +68,7 @@ export interface GenerateValidatedSummaryInput {
|
|
|
69
68
|
model?: Model<Api>;
|
|
70
69
|
/** Reasoning level for the summary request; `off` (default) keeps the pre-existing request shape. */
|
|
71
70
|
thinking?: CompactionThinkingLevel;
|
|
72
|
-
/** Transport retry policy;
|
|
71
|
+
/** Transport-only retry policy; three total attempts by default. */
|
|
73
72
|
transport?: CompactionTransportPolicy;
|
|
74
73
|
now: () => number;
|
|
75
74
|
onTransportRetry?: (diagnostic: CompactionTransportRetryDiagnostic) => void;
|