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.
@@ -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
- this.dependencies.syncSessionIndex(ctx);
234
- const source = prepareCompactionSource(event);
235
- const inputBudgetTokens = this.inputBudgetTokens(model);
236
- if (inputBudgetTokens <= 0) throw new Error("Active model has no safe compaction input budget");
237
- const wholePlan = this.buildSegmentPlan(source, event, model.provider);
238
- this.state = {
239
- ...this.state,
240
- sourceEntries: source.sourceEntryIds.length,
241
- inputBudgetTokens,
242
- sourcePromptTokens: wholePlan.promptTokens,
243
- };
244
- const segmentPlans = this.partitionSegmentPlans(
245
- source,
246
- wholePlan,
247
- event,
248
- model.provider,
249
- inputBudgetTokens,
250
- );
251
- this.state.segmentCount = segmentPlans.length;
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
- const segmentNodes: EmbeddedSummaryNode[] = [];
273
- for (const plan of segmentPlans) {
274
- const generated = await this.generateSummary({
275
- stage: "segment",
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: this.dependencies.config.compaction.summary?.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
- segmentNodes.push({
287
- id: nextId(),
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 = segmentNodes[0]?.id;
302
- if (!segmentId) throw new Error("Compaction fan-out produced no segment summary node");
303
-
304
- const createdNodes: EmbeddedSummaryNode[] = [];
305
- let previousNode = source.previousNode;
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 = embeddedNodes.map((node) => createSummaryRecord({
336
- sessionId: this.dependencies.sessionId,
337
- node,
338
- boundary,
339
- segmentSummaryId: node.kind === "segment" ? node.id : segmentId,
340
- lifecycleStatus: "prepared",
341
- }));
342
- records.push(createSummaryRecord({
343
- sessionId: this.dependencies.sessionId,
344
- node: activeNode,
345
- boundary,
346
- segmentSummaryId: segmentId,
347
- lifecycleStatus: "prepared",
348
- embeddedNodes,
349
- }));
350
- if (this.dependencies.persisted) this.dependencies.database?.summaries.saveGraph(records);
351
- for (const record of records) this.graphRecords.set(record.id, record);
352
- this.pendingSummaryIds = records.map((record) => record.id);
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
- return Math.floor(resolved.budget.activeInputBudget);
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. Defaults mirror Pi's
22
- * assistant retry settings (`retry.maxRetries` 3, `retry.baseDelayMs` 2000,
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; defaults mirror Pi's assistant retry settings. */
71
+ /** Transport-only retry policy; three total attempts by default. */
73
72
  transport?: CompactionTransportPolicy;
74
73
  now: () => number;
75
74
  onTransportRetry?: (diagnostic: CompactionTransportRetryDiagnostic) => void;
@@ -1,4 +1,4 @@
1
- export const EXTENSION_VERSION = "0.3.3";
1
+ export const EXTENSION_VERSION = "0.3.5";
2
2
  export const SUPPORTED_PI_VERSION = "0.84.3";
3
3
  export const OBSERVER_PLANNER_VERSION = "observer-model-aware-v1";
4
4
  export const PLANNER_VERSION = "managed-learned-ranking-v1";