chainlesschain 0.166.135 → 0.166.138

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.
@@ -4,6 +4,7 @@ import { isToolRecoveryPaused } from "./tool-recovery-result.js";
4
4
  import { hostToolOutputIdentity } from "./tool-result-preview.js";
5
5
  import { serializeRetainedContext } from "./retained-context-preview.js";
6
6
  const MAX_FILES = 12;
7
+ const MAX_UNDELIVERED_READS = 6;
7
8
 
8
9
  function keyFor(filePath, args) {
9
10
  return JSON.stringify([
@@ -94,6 +95,44 @@ export class ReadFileLoopGuard {
94
95
  retainContext(context);
95
96
  }
96
97
  }
98
+ for (const entry of this.progress.values()) {
99
+ for (const pending of entry.pendingPages) {
100
+ if (this.isPageVisible(pending.page)) {
101
+ entry.confirmedSpans = mergeSpan(
102
+ entry.confirmedSpans,
103
+ pending.page.readSpan,
104
+ );
105
+ if (pending.page.readSpan.end >= entry.confirmedCursorEnd) {
106
+ entry.confirmedCursorEnd = pending.page.readSpan.end;
107
+ entry.confirmedCursor = {
108
+ lastReadRange: pending.page.range,
109
+ nextRead: pending.page.nextRead || null,
110
+ };
111
+ }
112
+ } else {
113
+ entry.undeliveredReads++;
114
+ if (pending.reread) entry.rereads.delete(pending.reread);
115
+ if (pending.restoring) entry.contextRestoreOffered = false;
116
+ }
117
+ }
118
+ entry.pendingPages = [];
119
+ // Execution reserves ranges synchronously for parallel reads. Only
120
+ // source actually selected for the provider becomes historical coverage.
121
+ entry.spans = entry.confirmedSpans.map((span) => [...span]);
122
+ if (entry.summary) {
123
+ const through =
124
+ entry.confirmedSpans[0]?.[0] === 0 ? entry.confirmedSpans[0][1] : 0;
125
+ entry.summary.readThrough = through;
126
+ entry.summary.reachedEnd = through >= entry.total;
127
+ entry.summary.pendingDelivery = false;
128
+ if (entry.confirmedCursor)
129
+ Object.assign(entry.summary, entry.confirmedCursor);
130
+ else {
131
+ delete entry.summary.lastReadRange;
132
+ entry.summary.nextRead = null;
133
+ }
134
+ }
135
+ }
97
136
  }
98
137
 
99
138
  isPageVisible(page) {
@@ -103,8 +142,29 @@ export class ReadFileLoopGuard {
103
142
  keyFor(visible.path, {}) === keyFor(page.path, {}) &&
104
143
  visible.fileVersion === page.fileVersion &&
105
144
  typeof page.content === "string" &&
106
- page.content.length > 0 &&
107
- visible.content.includes(page.content),
145
+ (page.content.length > 0 || page.readSpan?.total === 0) &&
146
+ ((visible.readSpan &&
147
+ page.readSpan &&
148
+ visible.readSpan.total === page.readSpan.total &&
149
+ visible.readSpan.start <= page.readSpan.start &&
150
+ visible.readSpan.end >= page.readSpan.end &&
151
+ visible.readSpan.end - visible.readSpan.start >=
152
+ visible.content.length &&
153
+ visible.readSpan.end - visible.readSpan.start <=
154
+ visible.content.length + 1 &&
155
+ page.readSpan.end - page.readSpan.start >= page.content.length &&
156
+ page.readSpan.end - page.readSpan.start <=
157
+ page.content.length + 1 &&
158
+ visible.content.slice(
159
+ page.readSpan.start - visible.readSpan.start,
160
+ page.readSpan.start -
161
+ visible.readSpan.start +
162
+ page.content.length,
163
+ ) === page.content) ||
164
+ (!visible.readSpan &&
165
+ !visible.contentTruncated &&
166
+ JSON.stringify(visible.range) === JSON.stringify(page.range) &&
167
+ visible.content === page.content)),
108
168
  ) === true
109
169
  );
110
170
  }
@@ -166,6 +226,11 @@ export class ReadFileLoopGuard {
166
226
  entry = {
167
227
  fileVersion,
168
228
  spans: [],
229
+ confirmedSpans: [],
230
+ pendingPages: [],
231
+ undeliveredReads: 0,
232
+ confirmedCursor: null,
233
+ confirmedCursorEnd: 0,
169
234
  rereads: new Set(),
170
235
  summary: null,
171
236
  excerpts: [],
@@ -184,7 +249,21 @@ export class ReadFileLoopGuard {
184
249
  readProgress: { newContent: false },
185
250
  };
186
251
  }
252
+ if (entry.undeliveredReads >= MAX_UNDELIVERED_READS) {
253
+ return {
254
+ path: filePath,
255
+ fileVersion,
256
+ code: "CC_READ_SOURCE_NOT_DELIVERED",
257
+ error:
258
+ "Repeated file reads did not deliver source content to the model. Report this limitation or use a focused search; do not claim the file was read.",
259
+ contentOmitted: true,
260
+ reachedEnd: false,
261
+ readRecovery: { action: "source-unavailable" },
262
+ };
263
+ }
187
264
  let span = page.readSpan;
265
+ let reread;
266
+ let restoringPage = false;
188
267
  const covered = entry.spans.some(
189
268
  ([start, end]) => start <= span.start && end >= span.end,
190
269
  );
@@ -221,6 +300,7 @@ export class ReadFileLoopGuard {
221
300
  span = page.readSpan;
222
301
  targeted = true;
223
302
  entry.contextRestoreOffered = true;
303
+ restoringPage = true;
224
304
  }
225
305
  }
226
306
  if (covered || (coveredPrefix && !targeted)) {
@@ -240,6 +320,7 @@ export class ReadFileLoopGuard {
240
320
  entry.rereads.size < 3
241
321
  ) {
242
322
  entry.rereads.add(fingerprint);
323
+ reread = fingerprint;
243
324
  page.readRecovery = {
244
325
  action: restoring ? "context-restore" : "targeted-review",
245
326
  };
@@ -257,6 +338,19 @@ export class ReadFileLoopGuard {
257
338
  };
258
339
  newContent = true;
259
340
  } else {
341
+ const confirmedEnd =
342
+ entry.confirmedSpans[0]?.[0] === 0 ? entry.confirmedSpans[0][1] : 0;
343
+ if (this.visiblePages !== null && confirmedEnd < span.total) {
344
+ return {
345
+ path: filePath,
346
+ fileVersion,
347
+ contentOmitted: true,
348
+ reachedEnd: false,
349
+ readProgress: { newContent: false },
350
+ readRecovery: { action: "pending-delivery" },
351
+ hint: "This range has a read pending in the current tool batch. Historical source coverage is recorded only after content reaches the model.",
352
+ };
353
+ }
260
354
  return {
261
355
  path: filePath,
262
356
  fileVersion,
@@ -283,6 +377,25 @@ export class ReadFileLoopGuard {
283
377
  }
284
378
  }
285
379
  entry.spans = mergeSpan(entry.spans, page.readSpan);
380
+ entry.total = page.readSpan.total;
381
+ if (this.visiblePages === null) {
382
+ // Direct tool consumers without provider selection keep synchronous
383
+ // semantics. Agent runs observe the initial request before any reads.
384
+ entry.confirmedSpans = entry.spans.map((span) => [...span]);
385
+ } else {
386
+ entry.pendingPages.push({
387
+ page: {
388
+ path: page.path,
389
+ fileVersion: page.fileVersion,
390
+ content: page.content,
391
+ readSpan: { ...page.readSpan },
392
+ ...(page.range ? { range: { ...page.range } } : {}),
393
+ ...(page.nextRead ? { nextRead: { ...page.nextRead } } : {}),
394
+ },
395
+ reread,
396
+ restoring: restoringPage,
397
+ });
398
+ }
286
399
  entry.lastReadRequest = this.visibleRequest;
287
400
  // Coverage alone cannot restore source code after compaction. Keep bounded
288
401
  // observed pages for every file type, not only large Markdown outlines.
@@ -302,10 +415,12 @@ export class ReadFileLoopGuard {
302
415
  ].slice(-2);
303
416
  }
304
417
  const allRead =
305
- entry.spans[0]?.[0] === 0 && entry.spans[0][1] >= page.readSpan.total;
418
+ entry.confirmedSpans[0]?.[0] === 0 &&
419
+ entry.confirmedSpans[0][1] >= page.readSpan.total;
306
420
  // A read of the tail alone must never imply coverage of earlier sections.
307
421
  const previousEnd = entry.summary?.readThrough || 0;
308
- const updateCursor = page.readSpan.end >= previousEnd;
422
+ const updateCursor =
423
+ this.visiblePages === null && page.readSpan.end >= previousEnd;
309
424
  entry.summary = {
310
425
  ...entry.summary,
311
426
  path: filePath,
@@ -317,8 +432,16 @@ export class ReadFileLoopGuard {
317
432
  }
318
433
  : {}),
319
434
  reachedEnd: allRead,
435
+ ...(this.visiblePages !== null ? { pendingDelivery: true } : {}),
320
436
  ...(page.outline ? { outline: page.outline } : {}),
321
437
  };
438
+ if (updateCursor) {
439
+ entry.confirmedCursorEnd = page.readSpan.end;
440
+ entry.confirmedCursor = {
441
+ lastReadRange: page.range,
442
+ nextRead: page.nextRead || null,
443
+ };
444
+ }
322
445
  this.progress.delete(key);
323
446
  this.progress.set(key, entry);
324
447
  // Discard pathological fragmented coverage instead of inventing coverage.
@@ -336,6 +459,13 @@ export class ReadFileLoopGuard {
336
459
  // as another duplicate would immediately schedule another "one-turn"
337
460
  // pause and prevent read_file from ever becoming available again.
338
461
  if (isToolRecoveryPaused(result)) return;
462
+ if (
463
+ tool === "read_file" &&
464
+ result?.code === "CC_READ_SOURCE_NOT_DELIVERED"
465
+ ) {
466
+ this.repeatedBatches = Math.max(this.repeatedBatches, 6);
467
+ return;
468
+ }
339
469
  if (
340
470
  !this.batch ||
341
471
  result?.error ||
@@ -83,3 +83,13 @@ export function bareModeRequested(opts = {}, env = process.env) {
83
83
  const raw = env.CC_BARE;
84
84
  return raw != null && /^(1|true|yes|on)$/i.test(String(raw).trim());
85
85
  }
86
+
87
+ /** Apply independent saved restrictions without enabling the rest of bare mode. */
88
+ export function applyCustomizationOptions(options, env = process.env) {
89
+ if (options.plugins === false) env.CC_PLUGINS = "0";
90
+ if (options.mcpAutoConnect === false) {
91
+ options.mcp = false;
92
+ for (const field of ["ide", "pdh", "jetbrains"])
93
+ if (options[field] === undefined) options[field] = false;
94
+ }
95
+ }
@@ -108,6 +108,42 @@ export function createSessionHistoryOrigins() {
108
108
  },
109
109
  branchPrefix(messages, head, count) {
110
110
  if (!context) return null;
111
+ // A branch may retain the complete verified context. A rewind must remove
112
+ // something, so its strict-prefix admission cannot decide this case.
113
+ if (messages.length === context.length) {
114
+ if (
115
+ !/^[a-f0-9]{64}$/u.test(head) ||
116
+ !Number.isSafeInteger(count) ||
117
+ count < 0 ||
118
+ context.some(
119
+ (entry) =>
120
+ historyBearing(entry) &&
121
+ (!entry.origin ||
122
+ !Number.isSafeInteger(entry.origin.first) ||
123
+ !Number.isSafeInteger(entry.origin.last) ||
124
+ entry.origin.first < 0 ||
125
+ entry.origin.last < entry.origin.first ||
126
+ entry.origin.last >= count),
127
+ )
128
+ )
129
+ return null;
130
+ try {
131
+ if (
132
+ fingerprint(context.map((entry) => entry.message)) !==
133
+ fingerprint(messages)
134
+ )
135
+ return null;
136
+ } catch {
137
+ return null;
138
+ }
139
+ return {
140
+ cutoff: count,
141
+ entries: context.map((entry) => ({
142
+ message: entry.message,
143
+ origin: entry.origin ? { ...entry.origin } : null,
144
+ })),
145
+ };
146
+ }
111
147
  const cutoff = api.rewind(
112
148
  {
113
149
  prevHash: head,
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  createEvidenceJsonTextBoundary,
3
3
  AGENT_CODE_TEXT_RESULT_TOOLS,
4
+ EVIDENCE_JSON_POLICY,
4
5
  } from "./evolution/evidence-json-text.js";
5
6
  import { createHash } from "node:crypto";
6
7
 
@@ -33,7 +34,11 @@ const PREVIEW_HINT =
33
34
 
34
35
  /** Check only host-created excerpts; original uncut tool text still reaches ingress. */
35
36
  function completeExcerpt(text, tool) {
36
- const scan = createEvidenceJsonTextBoundary((message) => new Error(message));
37
+ const scan = createEvidenceJsonTextBoundary((message) => new Error(message), {
38
+ ...EVIDENCE_JSON_POLICY,
39
+ sourceLiteralPolicy: "explicit-source-containers-v1",
40
+ maxSourceTokens: 32768,
41
+ });
37
42
  const visit = (value, layer) => {
38
43
  if (typeof value === "string") return project(value, layer);
39
44
  if (Array.isArray(value)) return value.map((child) => visit(child, layer));
@@ -48,6 +53,7 @@ function completeExcerpt(text, tool) {
48
53
  scan(value, {
49
54
  layer,
50
55
  codeText: AGENT_CODE_TEXT_RESULT_TOOLS.includes(tool),
56
+ sourceText: layer === 0,
51
57
  visit,
52
58
  plain: (plain) => plain,
53
59
  equal: (left, right) => JSON.stringify(left) === JSON.stringify(right),
@@ -235,7 +241,10 @@ export function safeHostToolResultForModel(tool, result) {
235
241
  const text = result[field];
236
242
  if (typeof text !== "string" || completeExcerpt(text, tool)) continue;
237
243
  if (safe === result) safe = { ...result };
238
- safe[field] = omitted(text.length);
244
+ safe[field] =
245
+ tool === "read_file"
246
+ ? `Text omitted (${text.length} chars); this excerpt could not be safely projected. Read a complete surrounding statement or container; expand the range or read the full file within existing size limits when necessary. Omitted content has not been inspected.`
247
+ : omitted(text.length);
239
248
  safe[`${field}Omitted`] = true;
240
249
  safe[`${field}OmittedChars`] = text.length;
241
250
  }
@@ -3544,6 +3544,7 @@ async function startAgentReplInWorkspaceOwned(
3544
3544
  });
3545
3545
  if (feature("PROMPT_COMPRESSOR") || contextMemoryCutover.canonical) {
3546
3546
  _compressor = new PromptCompressor({
3547
+ contextMemoryModelWindowTokens: options.contextMemoryModelWindowTokens,
3547
3548
  model,
3548
3549
  provider,
3549
3550
  llmQuery: async (prompt) => {
@@ -9852,6 +9853,7 @@ async function startAgentReplInWorkspaceOwned(
9852
9853
  usageEvents,
9853
9854
  thinking: reasoning,
9854
9855
  } = await agentLoop(messages, {
9856
+ contextMemoryModelWindowTokens: options.contextMemoryModelWindowTokens,
9855
9857
  ...captureSkillRuntimeDependencies(options),
9856
9858
  ...liveOpts,
9857
9859
  waitForOutput: _replOutputFlow.wait,
@@ -1,4 +1,7 @@
1
- import { projectMessagesWithTaskBinding } from "./message-roles.js";
1
+ import {
2
+ projectMessagesWithTaskBinding,
3
+ getTaskCompactionBindings,
4
+ } from "./message-roles.js";
2
5
  import {
3
6
  providerFetch,
4
7
  requireUnscopedToolExecution,
@@ -12375,7 +12378,10 @@ export async function chatWithTools(rawMessages, options) {
12375
12378
  ),
12376
12379
  {
12377
12380
  signal,
12378
- retries: options.workflowEffectId ? 0 : undefined,
12381
+ retries:
12382
+ options.workflowEffectId || options.disableTransportRetry === true
12383
+ ? 0
12384
+ : undefined,
12379
12385
  strictRetryObserver: options.strictUsageTelemetry === true,
12380
12386
  ...(typeof options.onStreamRetry === "function"
12381
12387
  ? {
@@ -12527,7 +12533,10 @@ export async function chatWithTools(rawMessages, options) {
12527
12533
  ),
12528
12534
  {
12529
12535
  signal,
12530
- retries: options.workflowEffectId ? 0 : undefined,
12536
+ retries:
12537
+ options.workflowEffectId || options.disableTransportRetry === true
12538
+ ? 0
12539
+ : undefined,
12531
12540
  strictRetryObserver: options.strictUsageTelemetry === true,
12532
12541
  ...(typeof options.onStreamRetry === "function"
12533
12542
  ? {
@@ -12655,7 +12664,10 @@ export async function chatWithTools(rawMessages, options) {
12655
12664
  ),
12656
12665
  {
12657
12666
  signal,
12658
- retries: options.workflowEffectId ? 0 : undefined,
12667
+ retries:
12668
+ options.workflowEffectId || options.disableTransportRetry === true
12669
+ ? 0
12670
+ : undefined,
12659
12671
  strictRetryObserver: options.strictUsageTelemetry === true,
12660
12672
  ...(typeof options.onStreamRetry === "function"
12661
12673
  ? {
@@ -12720,7 +12732,10 @@ export async function chatWithTools(rawMessages, options) {
12720
12732
  ),
12721
12733
  {
12722
12734
  signal,
12723
- retries: options.workflowEffectId ? 0 : undefined,
12735
+ retries:
12736
+ options.workflowEffectId || options.disableTransportRetry === true
12737
+ ? 0
12738
+ : undefined,
12724
12739
  strictRetryObserver: options.strictUsageTelemetry === true,
12725
12740
  ...(typeof options.onStreamRetry === "function"
12726
12741
  ? {
@@ -12768,7 +12783,10 @@ export async function chatWithTools(rawMessages, options) {
12768
12783
  ),
12769
12784
  {
12770
12785
  signal,
12771
- retries: options.workflowEffectId ? 0 : undefined,
12786
+ retries:
12787
+ options.workflowEffectId || options.disableTransportRetry === true
12788
+ ? 0
12789
+ : undefined,
12772
12790
  strictRetryObserver: options.strictUsageTelemetry === true,
12773
12791
  ...(typeof options.onStreamRetry === "function"
12774
12792
  ? {
@@ -14607,6 +14625,9 @@ async function _getAutoCompactor(options) {
14607
14625
  onToken: undefined,
14608
14626
  onStall: undefined,
14609
14627
  onStreamRetry: undefined,
14628
+ // Summary spend is settled once at the compactor boundary.
14629
+ // A second transport attempt would bypass that accounting.
14630
+ disableTransportRetry: true,
14610
14631
  providerRequestId:
14611
14632
  requestBinding?.providerRequestId || undefined,
14612
14633
  maxOutputTokens,
@@ -14642,6 +14663,7 @@ async function _getAutoCompactor(options) {
14642
14663
  return compactLiveMessagesCanonical(messages, {
14643
14664
  compressor: compatibilitySummarizer,
14644
14665
  compressOptions,
14666
+ requiredUserBindings: compressOptions.requiredUserBindings,
14645
14667
  sessionId: options.sessionId,
14646
14668
  operationId: `auto-${randomUUID()}`,
14647
14669
  provider: options.provider || "ollama",
@@ -14979,6 +15001,11 @@ export async function* agentLoop(messages, options) {
14979
15001
  ).length === 1,
14980
15002
  }
14981
15003
  : null;
15004
+ const taskCompactionBindings = getTaskCompactionBindings(
15005
+ options.projectProviderMessages,
15006
+ messages,
15007
+ currentUserTaskBinding,
15008
+ );
14982
15009
  if (options.serviceTier != null) {
14983
15010
  resolveModelServiceTier(
14984
15011
  resolveModelCapabilityProfile(options),
@@ -15901,6 +15928,7 @@ export async function* agentLoop(messages, options) {
15901
15928
  }
15902
15929
  : await compactor.compress(workingMessages, {
15903
15930
  preserveToolPairs: true,
15931
+ requiredUserBindings: taskCompactionBindings,
15904
15932
  ...pinOpts,
15905
15933
  });
15906
15934
  if (
@@ -15936,6 +15964,9 @@ export async function* agentLoop(messages, options) {
15936
15964
  reason:
15937
15965
  stats.summaryUsageUnknownReason ||
15938
15966
  "provider_transport_outcome_unknown",
15967
+ ...(stats.summaryProviderError
15968
+ ? { providerError: stats.summaryProviderError }
15969
+ : {}),
15939
15970
  };
15940
15971
  }
15941
15972
  }
@@ -16042,6 +16073,9 @@ export async function* agentLoop(messages, options) {
16042
16073
  ? compactionUsageCall.model
16043
16074
  : stats.summaryModel || options.model || null,
16044
16075
  reason: unknownReason,
16076
+ ...(stats.summaryProviderError
16077
+ ? { providerError: stats.summaryProviderError }
16078
+ : {}),
16045
16079
  code:
16046
16080
  unknownReason === "provider_usage_not_reported"
16047
16081
  ? "provider_usage_missing"
@@ -2942,6 +2942,7 @@ async function runAgentHeadlessInWorkspace(
2942
2942
  });
2943
2943
  };
2944
2944
  const loopOptions = {
2945
+ contextMemoryModelWindowTokens: options.contextMemoryModelWindowTokens,
2945
2946
  ...(evolutionIngress === null ? {} : { evolutionIngress }),
2946
2947
  model,
2947
2948
  provider,
@@ -964,6 +964,7 @@ async function runTurn(
964
964
  let endReason = "complete";
965
965
  let stopForCost = false;
966
966
  let stopForCompactionUsageUnknown = false;
967
+ let compactionUsageUnknown = null;
967
968
  let responseCompleted = false;
968
969
  // P0-2: the side-effect op currently in flight (at most one — the loop runs
969
970
  // tools serially). Non-null only between a dangerous tool's tool-executing
@@ -1223,6 +1224,7 @@ async function runTurn(
1223
1224
  }
1224
1225
  break;
1225
1226
  case "compaction-usage-unknown":
1227
+ compactionUsageUnknown = compactionUsageUnknownDiagnostic(event);
1226
1228
  if (event.ledgerPersisted !== true) {
1227
1229
  persistUsageEvent?.({
1228
1230
  ...event,
@@ -1282,7 +1284,52 @@ async function runTurn(
1282
1284
  ) {
1283
1285
  sideEffects.persist();
1284
1286
  }
1285
- return { finalText, endReason, usage, toolCalls };
1287
+ return { finalText, endReason, usage, toolCalls, compactionUsageUnknown };
1288
+ }
1289
+
1290
+ function compactionUsageUnknownDiagnostic(event) {
1291
+ const reason =
1292
+ event?.reason === "provider_usage_not_reported"
1293
+ ? "provider_usage_not_reported"
1294
+ : "provider_transport_outcome_unknown";
1295
+ const providerError = event?.providerError;
1296
+ const providerHints = {
1297
+ authentication:
1298
+ "Provider authentication failed; check the API credentials.",
1299
+ access_denied: "Provider access was denied; check model permissions.",
1300
+ rate_limit: "The provider rate limit was reached; wait before resuming.",
1301
+ server_error:
1302
+ "The provider returned a server error; check service availability.",
1303
+ http_error:
1304
+ "The provider rejected the compaction request; check the model endpoint.",
1305
+ network: "The model connection failed; check network and proxy settings.",
1306
+ invalid_response:
1307
+ "The provider returned an invalid response; check API compatibility.",
1308
+ unknown: "The provider request failed.",
1309
+ };
1310
+ const category = providerError?.category;
1311
+ const projected = Object.hasOwn(providerHints, category || "")
1312
+ ? {
1313
+ category,
1314
+ ...(Number.isInteger(providerError.httpStatus) &&
1315
+ providerError.httpStatus >= 400 &&
1316
+ providerError.httpStatus <= 599
1317
+ ? { httpStatus: providerError.httpStatus }
1318
+ : {}),
1319
+ }
1320
+ : null;
1321
+ return {
1322
+ code: "CC_COMPACTION_USAGE_UNKNOWN",
1323
+ usage_unknown_reason: reason,
1324
+ ...(projected ? { provider_error: projected } : {}),
1325
+ error:
1326
+ (reason === "provider_usage_not_reported"
1327
+ ? "Compaction provider did not report valid token usage"
1328
+ : "Compaction request failed before provider usage could be confirmed") +
1329
+ "; the session stopped without retrying the request. " +
1330
+ (projected ? providerHints[category] + " " : "") +
1331
+ "Review the saved session and provider usage, check the model connection, then resume explicitly.",
1332
+ };
1286
1333
  }
1287
1334
 
1288
1335
  /**
@@ -3821,6 +3868,31 @@ async function runAgentHeadlessStreamInWorkspace(
3821
3868
  { code: "CC_SESSION_TOOL_ROUND_SETTLEMENT_HEAD_UNKNOWN" },
3822
3869
  );
3823
3870
  }
3871
+ // Only a new, acknowledged tool_round has item positions to associate
3872
+ // with live IDE output. A no-op settlement returns an existing head only.
3873
+ if (appended.event?.type === "tool_round") {
3874
+ const items = appended.event.data.messages.map((message, itemIndex) => ({
3875
+ itemIndex,
3876
+ role: message.role,
3877
+ toolCallId:
3878
+ message.role === "assistant"
3879
+ ? message.tool_calls[0].id
3880
+ : message.tool_call_id,
3881
+ }));
3882
+ for (let offset = 0; offset < items.length; offset += 100) {
3883
+ emit({
3884
+ type: "system",
3885
+ subtype: "tool_round_persisted",
3886
+ session_id: sessionId,
3887
+ transcript_refs: {
3888
+ schema: "chainlesschain.session-tool-round-references/v1",
3889
+ sessionId,
3890
+ eventId: appended.hash,
3891
+ items: items.slice(offset, offset + 100),
3892
+ },
3893
+ });
3894
+ }
3895
+ }
3824
3896
  return appended;
3825
3897
  };
3826
3898
  const settleToolRoundsForHost = (phase, clientMessageId = null) => {
@@ -3873,6 +3945,7 @@ async function runAgentHeadlessStreamInWorkspace(
3873
3945
  return settleToolRoundsForHost(phase);
3874
3946
  };
3875
3947
  const loopOptionsBase = {
3948
+ contextMemoryModelWindowTokens: options.contextMemoryModelWindowTokens,
3876
3949
  ...(evolutionIngress === null ? {} : { evolutionIngress }),
3877
3950
  reviewChanges: options.reviewChanges === true,
3878
3951
  model,
@@ -4851,9 +4924,11 @@ async function runAgentHeadlessStreamInWorkspace(
4851
4924
  type: "result",
4852
4925
  subtype: "error_compaction_usage_unknown",
4853
4926
  is_error: true,
4854
- code: "CC_COMPACTION_USAGE_UNKNOWN",
4855
- error:
4856
- "Compaction provider usage is unknown; the paid compaction is not retried",
4927
+ ...compactionUsageUnknownDiagnostic(
4928
+ compatibilityResult?.usageUnknownEvent || {
4929
+ reason: "provider_transport_outcome_unknown",
4930
+ },
4931
+ ),
4857
4932
  session_id: sessionId,
4858
4933
  });
4859
4934
  sawError = true;
@@ -4905,9 +4980,7 @@ async function runAgentHeadlessStreamInWorkspace(
4905
4980
  type: "result",
4906
4981
  subtype: "error_compaction_usage_unknown",
4907
4982
  is_error: true,
4908
- code: "CC_COMPACTION_USAGE_UNKNOWN",
4909
- error:
4910
- "Compaction provider usage is unknown; the paid compaction is not retried",
4983
+ ...compactionUsageUnknownDiagnostic(usageUnknownEvent),
4911
4984
  session_id: sessionId,
4912
4985
  ...(costBudget ? { budget_state: "unverifiable" } : {}),
4913
4986
  });
@@ -5908,9 +5981,8 @@ async function runAgentHeadlessStreamInWorkspace(
5908
5981
  type: "result",
5909
5982
  subtype: "error_compaction_usage_unknown",
5910
5983
  is_error: true,
5911
- code: "CC_COMPACTION_USAGE_UNKNOWN",
5912
- error:
5913
- "Compaction provider usage is unknown; the paid compaction is not retried",
5984
+ ...(outcome.compactionUsageUnknown ||
5985
+ compactionUsageUnknownDiagnostic(null)),
5914
5986
  session_id: sessionId,
5915
5987
  turn: turns,
5916
5988
  usage: outcome.usage,
@@ -166,6 +166,34 @@ export function createResumeRoleProjection(messages) {
166
166
  return projection;
167
167
  }
168
168
 
169
+ /** Capture the original user sources needed by an owned resume merge. */
170
+ export function getTaskCompactionBindings(projection, input, taskBinding) {
171
+ if (!taskBinding) return [];
172
+ const owned = RESUME_ROLE_PROJECTIONS.get(projection);
173
+ const capture = (binding) => ({
174
+ ...binding,
175
+ matchCount: input.filter((message) =>
176
+ isDeepStrictEqual(message, binding.message),
177
+ ).length,
178
+ });
179
+ if (!owned) return [capture(taskBinding)];
180
+ const sourceIndex = input.indexOf(taskBinding.original);
181
+ const group = owned(input).sources.find((indices) =>
182
+ indices.includes(sourceIndex),
183
+ );
184
+ return (group || [sourceIndex]).map((index) => {
185
+ if (index === sourceIndex) return capture(taskBinding);
186
+ const original = input[index];
187
+ return capture({
188
+ original,
189
+ message: JSON.parse(canonicalJson(original)),
190
+ allowContentRelocation:
191
+ input.filter((message) => isDeepStrictEqual(message, original))
192
+ .length === 1,
193
+ });
194
+ });
195
+ }
196
+
169
197
  /**
170
198
  * Preserve a bound task through this module's exact, captured resume merges.
171
199
  * Arbitrary callbacks, wrappers and forged properties never receive lineage.
@@ -23,6 +23,7 @@ export function resolveAgentPolicy({
23
23
 
24
24
  return {
25
25
  model: connection.model || defaults.model || "qwen2:7b",
26
+ contextMemoryModelWindowTokens: overrides.contextMemoryModelWindowTokens,
26
27
  visionModel: overrides.visionModel,
27
28
  provider: connection.provider,
28
29
  baseUrl: connection.baseUrl,