chainlesschain 0.166.136 → 0.166.139

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,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,
@@ -74,7 +77,12 @@ import {
74
77
  CODING_TASK_QUALITY_GUIDANCE,
75
78
  CODING_CHANGE_REVIEW_PROMPT,
76
79
  } from "../lib/coding-task-guidance.js";
77
- import { CodingChangeReviewContext } from "../lib/coding-change-review-context.js";
80
+ import {
81
+ CodingChangeReviewContext,
82
+ changeReviewBudgetNotice,
83
+ changeReviewBudgetInsufficientError,
84
+ canReserveChangeReview,
85
+ } from "../lib/coding-change-review-context.js";
78
86
  import { RemoteReadLoopGuard } from "../lib/remote-read-loop-guard.js";
79
87
  import {
80
88
  isPrActionRequest,
@@ -12375,7 +12383,10 @@ export async function chatWithTools(rawMessages, options) {
12375
12383
  ),
12376
12384
  {
12377
12385
  signal,
12378
- retries: options.workflowEffectId ? 0 : undefined,
12386
+ retries:
12387
+ options.workflowEffectId || options.disableTransportRetry === true
12388
+ ? 0
12389
+ : undefined,
12379
12390
  strictRetryObserver: options.strictUsageTelemetry === true,
12380
12391
  ...(typeof options.onStreamRetry === "function"
12381
12392
  ? {
@@ -12527,7 +12538,10 @@ export async function chatWithTools(rawMessages, options) {
12527
12538
  ),
12528
12539
  {
12529
12540
  signal,
12530
- retries: options.workflowEffectId ? 0 : undefined,
12541
+ retries:
12542
+ options.workflowEffectId || options.disableTransportRetry === true
12543
+ ? 0
12544
+ : undefined,
12531
12545
  strictRetryObserver: options.strictUsageTelemetry === true,
12532
12546
  ...(typeof options.onStreamRetry === "function"
12533
12547
  ? {
@@ -12655,7 +12669,10 @@ export async function chatWithTools(rawMessages, options) {
12655
12669
  ),
12656
12670
  {
12657
12671
  signal,
12658
- retries: options.workflowEffectId ? 0 : undefined,
12672
+ retries:
12673
+ options.workflowEffectId || options.disableTransportRetry === true
12674
+ ? 0
12675
+ : undefined,
12659
12676
  strictRetryObserver: options.strictUsageTelemetry === true,
12660
12677
  ...(typeof options.onStreamRetry === "function"
12661
12678
  ? {
@@ -12720,7 +12737,10 @@ export async function chatWithTools(rawMessages, options) {
12720
12737
  ),
12721
12738
  {
12722
12739
  signal,
12723
- retries: options.workflowEffectId ? 0 : undefined,
12740
+ retries:
12741
+ options.workflowEffectId || options.disableTransportRetry === true
12742
+ ? 0
12743
+ : undefined,
12724
12744
  strictRetryObserver: options.strictUsageTelemetry === true,
12725
12745
  ...(typeof options.onStreamRetry === "function"
12726
12746
  ? {
@@ -12768,7 +12788,10 @@ export async function chatWithTools(rawMessages, options) {
12768
12788
  ),
12769
12789
  {
12770
12790
  signal,
12771
- retries: options.workflowEffectId ? 0 : undefined,
12791
+ retries:
12792
+ options.workflowEffectId || options.disableTransportRetry === true
12793
+ ? 0
12794
+ : undefined,
12772
12795
  strictRetryObserver: options.strictUsageTelemetry === true,
12773
12796
  ...(typeof options.onStreamRetry === "function"
12774
12797
  ? {
@@ -14607,6 +14630,9 @@ async function _getAutoCompactor(options) {
14607
14630
  onToken: undefined,
14608
14631
  onStall: undefined,
14609
14632
  onStreamRetry: undefined,
14633
+ // Summary spend is settled once at the compactor boundary.
14634
+ // A second transport attempt would bypass that accounting.
14635
+ disableTransportRetry: true,
14610
14636
  providerRequestId:
14611
14637
  requestBinding?.providerRequestId || undefined,
14612
14638
  maxOutputTokens,
@@ -14642,6 +14668,7 @@ async function _getAutoCompactor(options) {
14642
14668
  return compactLiveMessagesCanonical(messages, {
14643
14669
  compressor: compatibilitySummarizer,
14644
14670
  compressOptions,
14671
+ requiredUserBindings: compressOptions.requiredUserBindings,
14645
14672
  sessionId: options.sessionId,
14646
14673
  operationId: `auto-${randomUUID()}`,
14647
14674
  provider: options.provider || "ollama",
@@ -14979,6 +15006,11 @@ export async function* agentLoop(messages, options) {
14979
15006
  ).length === 1,
14980
15007
  }
14981
15008
  : null;
15009
+ const taskCompactionBindings = getTaskCompactionBindings(
15010
+ options.projectProviderMessages,
15011
+ messages,
15012
+ currentUserTaskBinding,
15013
+ );
14982
15014
  if (options.serviceTier != null) {
14983
15015
  resolveModelServiceTier(
14984
15016
  resolveModelCapabilityProfile(options),
@@ -15581,6 +15613,37 @@ export async function* agentLoop(messages, options) {
15581
15613
  toolContext.readFileLoopGuard = readFileLoopGuard;
15582
15614
  let lastTaskProgressIntervention = null;
15583
15615
 
15616
+ // Both an author completion and the reserved review window enter the same
15617
+ // phase. Neither path manufactures an author response or adds request budget.
15618
+ const beginChangeReview = (draft = null) => {
15619
+ throwIfAborted(signal);
15620
+ if (budget.remaining() < 2)
15621
+ throw changeReviewBudgetInsufficientError(budget.remaining());
15622
+ if (independentReviewSetupError) throw independentReviewSetupError;
15623
+ changeReviewState.requested = true;
15624
+ const reviewPrompt = markRuntimeOnlySessionMessage({
15625
+ role: "user",
15626
+ content: CODING_CHANGE_REVIEW_PROMPT,
15627
+ });
15628
+ if (draft)
15629
+ appendMessages(
15630
+ markRuntimeOnlySessionMessage({
15631
+ role: "assistant",
15632
+ content: draft.content || "",
15633
+ }),
15634
+ );
15635
+ appendMessages(reviewPrompt);
15636
+ if (independentReviewContext) {
15637
+ independentReviewContext.activate(reviewPrompt);
15638
+ changeReviewState.independentReviewPending = true;
15639
+ readFileLoopGuard = new ReadFileLoopGuard();
15640
+ remoteReadLoopGuard = new RemoteReadLoopGuard();
15641
+ taskProgressTracker = new TaskProgressTracker();
15642
+ toolContext.readFileLoopGuard = readFileLoopGuard;
15643
+ lastTaskProgressIntervention = null;
15644
+ }
15645
+ };
15646
+
15584
15647
  while (budget.hasRemaining()) {
15585
15648
  if (loopMcpCatalog) {
15586
15649
  currentMcpSnapshot = readMcpToolCatalog(loopMcpCatalog);
@@ -15627,6 +15690,24 @@ export async function* agentLoop(messages, options) {
15627
15690
  return;
15628
15691
  }
15629
15692
  readFileLoopGuard.finishBatch();
15693
+ if (
15694
+ canReserveChangeReview({
15695
+ enabled: options.reviewChanges,
15696
+ changed: hasFileContentChanges,
15697
+ requested: changeReviewState.requested,
15698
+ usageKnown: changeReviewState.usageKnown,
15699
+ pendingMutations: pendingSubtreeMutations.size,
15700
+ pendingChildren: backgroundSubAgents.size,
15701
+ remaining: budget.remaining(),
15702
+ limit: budget.limit,
15703
+ })
15704
+ ) {
15705
+ beginChangeReview();
15706
+ yield {
15707
+ type: "iteration-warning",
15708
+ message: "Reviewing changed files against the task before finishing.",
15709
+ };
15710
+ }
15630
15711
  if (remoteReadLoopGuard.stalled && !remoteReadLoopGuard.synthesisRequired) {
15631
15712
  await _awaitBackgroundUsageSettlement(
15632
15713
  backgroundSubAgents,
@@ -15901,6 +15982,7 @@ export async function* agentLoop(messages, options) {
15901
15982
  }
15902
15983
  : await compactor.compress(workingMessages, {
15903
15984
  preserveToolPairs: true,
15985
+ requiredUserBindings: taskCompactionBindings,
15904
15986
  ...pinOpts,
15905
15987
  });
15906
15988
  if (
@@ -15936,6 +16018,9 @@ export async function* agentLoop(messages, options) {
15936
16018
  reason:
15937
16019
  stats.summaryUsageUnknownReason ||
15938
16020
  "provider_transport_outcome_unknown",
16021
+ ...(stats.summaryProviderError
16022
+ ? { providerError: stats.summaryProviderError }
16023
+ : {}),
15939
16024
  };
15940
16025
  }
15941
16026
  }
@@ -16042,6 +16127,9 @@ export async function* agentLoop(messages, options) {
16042
16127
  ? compactionUsageCall.model
16043
16128
  : stats.summaryModel || options.model || null,
16044
16129
  reason: unknownReason,
16130
+ ...(stats.summaryProviderError
16131
+ ? { providerError: stats.summaryProviderError }
16132
+ : {}),
16045
16133
  code:
16046
16134
  unknownReason === "provider_usage_not_reported"
16047
16135
  ? "provider_usage_missing"
@@ -16551,7 +16639,10 @@ export async function* agentLoop(messages, options) {
16551
16639
  `Execution budget: this request plus ${furtherRequests} further model requests. ` +
16552
16640
  "For requested changes, reserve requests for implementation, focused validation, and repair or an accurate unfinished-work report. " +
16553
16641
  "Reading or an existing passing baseline does not deliver requested changes. " +
16554
- "For research or monitoring, use the remaining requests for evidence and the requested answer; do not create files just to show activity.",
16642
+ "For research or monitoring, use the remaining requests for evidence and the requested answer; do not create files just to show activity." +
16643
+ (options.reviewChanges === true
16644
+ ? changeReviewBudgetNotice(budget, changeReviewState.requested)
16645
+ : ""),
16555
16646
  },
16556
16647
  ];
16557
16648
  contextMemoryTrustedSystemIndexes.push(callMessages.length - 1);
@@ -16954,7 +17045,7 @@ export async function* agentLoop(messages, options) {
16954
17045
  }
16955
17046
  throwIfAborted(signal);
16956
17047
  if (
16957
- independentReviewContext?.active &&
17048
+ options.reviewChanges === true &&
16958
17049
  changeReviewState.usageKnown === false
16959
17050
  ) {
16960
17051
  const error = new Error(
@@ -17213,42 +17304,14 @@ export async function* agentLoop(messages, options) {
17213
17304
  };
17214
17305
  return;
17215
17306
  }
17216
- // Opt-in review uses the same loop, tools, usage settlement and budget.
17217
- // Never emit a provisional completion or turn this self-check into an
17218
- // acceptance verdict. Failed/deferred writes do not arm the review.
17307
+ // Author completion is provisional until the independent review phase.
17219
17308
  if (
17220
17309
  options.reviewChanges === true &&
17221
17310
  hasFileContentChanges &&
17222
17311
  !changeReviewState.requested &&
17223
- changeReviewState.usageKnown !== false &&
17224
- budget.hasRemaining()
17312
+ changeReviewState.usageKnown !== false
17225
17313
  ) {
17226
- throwIfAborted(signal);
17227
- if (independentReviewSetupError) throw independentReviewSetupError;
17228
- changeReviewState.requested = true;
17229
- const reviewPrompt = markRuntimeOnlySessionMessage({
17230
- role: "user",
17231
- content: CODING_CHANGE_REVIEW_PROMPT,
17232
- });
17233
- appendMessages(
17234
- markRuntimeOnlySessionMessage({
17235
- role: "assistant",
17236
- content: msg.content || "",
17237
- }),
17238
- reviewPrompt,
17239
- );
17240
- if (independentReviewContext) {
17241
- independentReviewContext.activate(reviewPrompt);
17242
- changeReviewState.independentReviewPending = true;
17243
- // These are phase-local discovery hints, not permission or spend
17244
- // authorities. Fresh inspection must not inherit author assertions,
17245
- // old visible file pages, or a pause caused by author exploration.
17246
- readFileLoopGuard = new ReadFileLoopGuard();
17247
- remoteReadLoopGuard = new RemoteReadLoopGuard();
17248
- taskProgressTracker = new TaskProgressTracker();
17249
- toolContext.readFileLoopGuard = readFileLoopGuard;
17250
- lastTaskProgressIntervention = null;
17251
- }
17314
+ beginChangeReview(msg);
17252
17315
  yield {
17253
17316
  type: "iteration-warning",
17254
17317
  message: "Reviewing changed files against the task before finishing.",
@@ -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,