chainlesschain 0.166.131 → 0.166.132

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 CHANGED
@@ -2,11 +2,21 @@
2
2
 
3
3
  Command-line interface for installing, configuring, and managing [ChainlessChain](https://www.chainlesschain.com), including the coding agent, governed tools, sessions, memory, and IDE integrations.
4
4
 
5
- ## Source candidate 0.166.131 (unreleased)
5
+ ## Source candidate 0.166.132 (unreleased, 2026-10-30)
6
6
 
7
- Main now prepares CLI **0.166.131** with Kernel **0.1.11**, Open VSX **0.37.169** and JetBrains **0.4.186** source candidates. It contains the later independent Goal, Memory, MCP/Skills, temporary-worktree and Terra changes; the verified public installation below remains `.130`. Publish and verify the changed kernel first, then require complete Linux/Windows/macOS CLI CI and Strict Sandbox at the exact new source, OIDC publication and public readback before releasing IDEs. No candidate release tag or publication is created by this preparation; 34/45 acceptance and the original unresolved dispatch remain unchanged.
7
+ CLI **0.166.132** keeps Kernel **0.1.11** and pairs with Open VSX **0.37.170** / JetBrains **0.4.187** source candidates. The candidate CLI stops tools from running on a truncated response and can replan once when complete usage has been recorded and budget remains. Recovery keeps the original task available even in a small context window. Filtered responses, explicitly incomplete terminal responses, and missing task context now stop with a clear failure. Resuming a session preserves task identity when the built-in adapter combines message roles; custom callbacks and unverified metadata cannot claim that identity. After asynchronous recall, the CLI rechecks the task and its position before continuing. The readContext continuation now moves the same complete user message by its exact index, preserving metadata and avoiding duplicate task identity. The original four independent-process restart cases passed locally, and 16 host-continuation tests passed; the latest full combined validation passed 456 tests across 18 files and 10 real CLI tests across 3 files, with no skips (5 output-limit, 1 resume and 4 compaction-memory-restart). Source 7f8ef64b4bd8b429f97d2e121c440143ed7ddf3b was superseded before publication after CLI CI found four failures in each Ubuntu and Windows E2E shard 4 job (147 passed each). Its 452+6 local preparation and successful Strict Sandbox (6/6) and Kernel (3/3) checks remain historical; remaining CLI and IDE jobs were canceled, and no release tag was created. The earlier 417+5 result also remains historical. A new exact-source release matrix is required. This candidate is not published, and the original broker evaluation remains unresolved. Require complete exact-source Linux/Windows/macOS CLI CI and Strict Sandbox, OIDC publication and public readback before releasing the paired IDEs. The verified public versions and installation below remain **0.166.131 / 0.37.169 / 0.4.186**; PC/mobile remain independent **v5.0.3.143**.
8
8
 
9
- ## CLI 0.166.130 — current public release (2026-10-29)
9
+ ## CLI 0.166.131 — current public release (2026-10-29)
10
+
11
+ Install: `npm i -g chainlesschain@0.166.131 --registry https://registry.npmjs.org`. Kernel **0.1.11** was published first. Restart the chat host, then run `cc --version` and `cc doctor`. Public Open VSX **0.37.169** / JetBrains **0.4.186** both recommend CLI **0.166.131**; their downloaded packages match the official CI payloads. Microsoft Marketplace is deferred.
12
+
13
+ This release includes explicit independent Goal `verify:` checks, Memory directory identity and cancellation fixes, MCP subtask and late-connection isolation, Skills download cancellation, rule refresh around compaction, temporary-worktree lifetime fixes and updated Terra capability/billing contracts. Exact-source three-OS CLI / Strict / Kernel checks passed. [OIDC publication](https://github.com/chainlesschain/chainlesschain/actions/runs/40862685540) and [independent public readback](https://github.com/chainlesschain/chainlesschain/actions/runs/40866417524) verified immutable package bytes and signed provenance. [Publication evidence](../../docs/research/cli/evidence/release131-public/publication.json).
14
+
15
+ Cumulative accepted tasks remain **34/45**, with **11** unfinished. Full Memory SLO, semantic quality and both IDE journeys remain separate acceptance work; the current native generation driver does not automatically use `verify:`. **NOT_PAID_READY / NOT_ADMITTED / HOLD** remain. PC/mobile retain their independent **v5.0.3.143** release. Older notes below retain their historical scope.
16
+
17
+ [.131 upgrade guide](https://docs.chainlesschain.com/chainlesschain/cli-ide-update-2026-10-29-131.html) · [Runtime design](https://design.chainlesschain.com/release-runtime-update-2026-10-29-131.html).
18
+
19
+ ## CLI 0.166.130 — previous public release (2026-10-29)
10
20
 
11
21
  Install the verified public package with `npm i -g chainlesschain@0.166.130 --registry https://registry.npmjs.org`. Public Open VSX **0.37.168** and approved JetBrains **0.4.185** both recommend `.130`; restart the chat host and run `cc --version` and `cc doctor`. Microsoft Marketplace remains unpublished; use Open VSX or its versioned VSIX.
12
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chainlesschain",
3
- "version": "0.166.131",
3
+ "version": "0.166.132",
4
4
  "description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,5 +6,5 @@
6
6
  "imageDigest": "sha256:6395a47a5bd9b038303e94a5e93754258c8480dab64635460f973eb93dc6f4d8",
7
7
  "bytes": 778808,
8
8
  "linkage": "static",
9
- "commit": "c9d0bab231850697df38f2d476c2dd974cbcb6c0"
9
+ "commit": "772fcdbda85bcf91df95bfced5b476dcf4c74614"
10
10
  }
@@ -6,5 +6,5 @@
6
6
  "imageDigest": "sha256:f162e55ad3f3f72f8a52e9d7f12b5b8db44506c2ca66a14d7d62d5cd4f12498e",
7
7
  "bytes": 934160,
8
8
  "linkage": "static",
9
- "commit": "c9d0bab231850697df38f2d476c2dd974cbcb6c0"
9
+ "commit": "772fcdbda85bcf91df95bfced5b476dcf4c74614"
10
10
  }
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "source": "CHANGELOG.md",
3
3
  "releases": [
4
+ {
5
+ "productVersion": null,
6
+ "cliVersion": "0.166.132",
7
+ "cliVersions": ["0.166.132"],
8
+ "date": "2026-10-30",
9
+ "title": "",
10
+ "sections": [
11
+ {
12
+ "heading": "Fixed - cc CLI 0.166.132: output-limit integrity and bounded recovery (candidate, 2026-10-30)",
13
+ "body": "- Preserve OpenAI-compatible chat-completions termination evidence in JSON and streaming responses. A response ending with `length` cannot execute any of its tool calls or become a successful final answer, even when its arguments happen to be valid JSON.\n- Settle complete physical-call usage before permitting at most one fresh planning iteration within the existing budget. Discard the truncated assistant/tool group; missing usage, incomplete streams, exhausted budgets and repeated truncation remain failures. Cancellation and final-synthesis limits still apply.\n- Reject `content_filter` and explicitly incomplete or ambiguous provider termination before executing tools or reporting completion. Filtered or ambiguous responses do not trigger a recovery request; conflicting SSE frames cannot clear an earlier filter.\n- Bind the current real user task across ordinary tool rounds, recovery, thinking-only turns and review, including canonical planning and recall. Resolve the owned task after provider projection, reject lost or ambiguous mappings, and retain the existing hermetic review contract. Recovery hints remain temporary system suffixes and do not replace the task.\n- Preserve legitimate resumed user-message merges through source mappings owned by the built-in role projector, while rejecting forged mapping fields and arbitrary rewrites. Recheck the task after asynchronous recall so changed content or moved messages cannot acquire a stale binding; keep authenticated history unchanged.\n- Move the existing user message after retained read context without duplicating it or discarding metadata. This preserves task identity after canonical compaction and independent CLI restart, retains historical user messages, and does not invent a fallback task when no user is present.\n- Add protocol and real canonical CLI process regressions for recovery, repeated truncation, missing usage, filtered output and ambiguous termination. Separately install three assisted pagination/history regression deliveries; their independent checks do not change the original 34/45 automatic task result.\n- Repair the VS Code README's current-public and unpublished-candidate sections against the existing Marketplace overview contract, retaining versioned artifacts and historical releases.\n- Pair the candidate with Open VSX 0.37.170 / JetBrains 0.4.187 after CLI publication is verified; Context/Memory Kernel remains 0.1.11. Exact-source full multi-OS gates, OIDC publication and public readback are required. Microsoft Marketplace remains deferred.\n- This change does not bypass the diagnostic broker's HTTP403 rejection or guarantee that a model will produce smaller writes. Real-model task retests follow verified publication of the new versions."
14
+ }
15
+ ]
16
+ },
4
17
  {
5
18
  "productVersion": null,
6
19
  "cliVersion": "0.166.131",
@@ -1,3 +1,4 @@
1
+ import { isDeepStrictEqual } from "node:util";
1
2
  import {
2
3
  canonicalDigest,
3
4
  canonicalJson,
@@ -205,6 +206,17 @@ export function messagesToContextItems(messagesInput, options = {}) {
205
206
  ? [...options.allowedSinks]
206
207
  : DEFAULT_ALLOWED_SINKS;
207
208
  const items = [];
209
+ const requiredUser = options.requiredUserMessage;
210
+ if (
211
+ requiredUser != null &&
212
+ (!Number.isSafeInteger(requiredUser.index) ||
213
+ requiredUser.index < 0 ||
214
+ requiredUser.index >= messages.length ||
215
+ requiredUser.message?.role !== "user" ||
216
+ !isDeepStrictEqual(messages[requiredUser.index], requiredUser.message))
217
+ ) {
218
+ throw new TypeError("Invalid current-user task binding");
219
+ }
208
220
  const trustedSystemIndexes = new Set(
209
221
  Array.isArray(options.trustedSystemIndexes)
210
222
  ? options.trustedSystemIndexes.filter(Number.isSafeInteger)
@@ -289,11 +301,20 @@ export function messagesToContextItems(messagesInput, options = {}) {
289
301
  const lastIndex = items.findLastIndex(
290
302
  (item) => item.kind !== "system-policy",
291
303
  );
292
- const last = items[lastIndex];
293
- if (last) {
294
- const payload = JSON.parse(loadBundleContent(last, sessionId));
295
- if (payload.messages.at(-1)?.role === "user") {
296
- const protectedItem = { ...last };
304
+ for (let index = 0; index < items.length; index++) {
305
+ const item = items[index];
306
+ if (
307
+ index !== lastIndex &&
308
+ item.sourceRef.eventSequence !== requiredUser?.index
309
+ )
310
+ continue;
311
+ const payload = JSON.parse(loadBundleContent(item, sessionId));
312
+ if (
313
+ payload.messages.at(-1)?.role === "user" &&
314
+ (index === lastIndex ||
315
+ item.sourceRef.eventSequence === requiredUser?.index)
316
+ ) {
317
+ const protectedItem = { ...item };
297
318
  delete protectedItem.digest;
298
319
  protectedItem.kind = "task-state";
299
320
  protectedItem.priority = 900_000;
@@ -301,7 +322,7 @@ export function messagesToContextItems(messagesInput, options = {}) {
301
322
  taskState: "waiting",
302
323
  requiredForRecovery: true,
303
324
  };
304
- items[lastIndex] = normalizeContextItem(protectedItem);
325
+ items[index] = normalizeContextItem(protectedItem);
305
326
  }
306
327
  }
307
328
  return items;
@@ -1,3 +1,4 @@
1
+ import { isDeepStrictEqual } from "node:util";
1
2
  import {
2
3
  canonicalDigest,
3
4
  canonicalJson,
@@ -203,6 +204,40 @@ export async function prepareCanonicalProviderContext(
203
204
  };
204
205
  }
205
206
 
207
+ // Resolve against the final provider projection, never a pre-projection index.
208
+ // Exact role/content/metadata matching prevents a transformed tool result or
209
+ // an ambiguous duplicate from taking the original user's task-state binding.
210
+ const taskBinding = options.contextMemoryRequiredUserBinding;
211
+ const requiredUserMessage = taskBinding?.message;
212
+ function resolveRequiredUserIndex() {
213
+ if (requiredUserMessage == null) return null;
214
+ const identities = messages.flatMap((message, index) =>
215
+ message === taskBinding.original ? [index] : [],
216
+ );
217
+ const candidates = identities.length
218
+ ? identities
219
+ : taskBinding.allowContentRelocation === true
220
+ ? messages.map((_, index) => index)
221
+ : [];
222
+ const matches = candidates.filter((index) => {
223
+ const message = messages[index];
224
+ return (
225
+ message?.role === "user" &&
226
+ requiredUserMessage.role === "user" &&
227
+ isDeepStrictEqual(message, requiredUserMessage)
228
+ );
229
+ });
230
+ if (matches.length !== 1 || identities.length > 1) {
231
+ const error = new Error(
232
+ "The current user task was lost, changed or duplicated in provider context",
233
+ );
234
+ error.code = "CC_AGENT_TASK_CONTEXT_LOST";
235
+ throw error;
236
+ }
237
+ return matches[0];
238
+ }
239
+ let requiredUserIndex = resolveRequiredUserIndex();
240
+
206
241
  // Review can exclude recalled author/session data while retaining the exact
207
242
  // canonical context, tool and output-token planning boundary.
208
243
  const recallEnabled = options.contextMemoryRecall !== false;
@@ -211,9 +246,9 @@ export async function prepareCanonicalProviderContext(
211
246
  : null;
212
247
  const sink = String(options.contextMemorySink || `provider.${provider}`);
213
248
  const scopeAdmissions = defaultAdmissions(sessionId, options);
214
- const lastUser = [...messages]
215
- .reverse()
216
- .find((message) => message?.role === "user");
249
+ const lastUser =
250
+ requiredUserMessage ||
251
+ [...messages].reverse().find((message) => message?.role === "user");
217
252
  const query =
218
253
  typeof lastUser?.content === "string" && lastUser.content.trim()
219
254
  ? lastUser.content.trim().slice(0, 32 * 1024)
@@ -254,6 +289,9 @@ export async function prepareCanonicalProviderContext(
254
289
  }),
255
290
  };
256
291
  options.signal?.throwIfAborted();
292
+ // Recall awaits host and storage work. Revalidate the captured task and its
293
+ // current position before image transformation and immutable item creation.
294
+ requiredUserIndex = resolveRequiredUserIndex();
257
295
  const imageTransport = captureContextImageTransport(messages);
258
296
  const messageItems = adjustContextImageTokenEstimates(
259
297
  messagesToContextItems(imageTransport.messages, {
@@ -261,6 +299,13 @@ export async function prepareCanonicalProviderContext(
261
299
  allowedSinks: [sink],
262
300
  trustCurrentSystemPolicy: true,
263
301
  trustedSystemIndexes: options.contextMemoryTrustedSystemIndexes,
302
+ requiredUserMessage:
303
+ requiredUserIndex === null
304
+ ? null
305
+ : {
306
+ index: requiredUserIndex,
307
+ message: imageTransport.messages[requiredUserIndex],
308
+ },
264
309
  allowExternalization: false,
265
310
  }),
266
311
  imageTransport.captures,
@@ -1,3 +1,4 @@
1
+ import { projectMessagesWithTaskBinding } from "./message-roles.js";
1
2
  import {
2
3
  providerFetch,
3
4
  requireUnscopedToolExecution,
@@ -28,6 +29,7 @@ import { resolveLlmConnection } from "../lib/llm-connection-resolution.js";
28
29
  */
29
30
 
30
31
  import fs from "fs";
32
+ import { isDeepStrictEqual } from "node:util";
31
33
  import { isUtf8 } from "node:buffer";
32
34
  import {
33
35
  _isNonNegativeSafeInteger,
@@ -272,6 +274,8 @@ import { isMcpRpcError } from "../harness/mcp-client.js";
272
274
  import {
273
275
  projectCanonicalResumeMessages,
274
276
  markRuntimeOnlySessionMessage,
277
+ isRuntimeOnlySessionMessage,
278
+ encodePersistedMessage,
275
279
  } from "../lib/session-message-provenance.js";
276
280
  import { releaseOldLiveSessionResults } from "../lib/session-runtime-retention.js";
277
281
  import {
@@ -12748,6 +12752,10 @@ export async function chatWithTools(rawMessages, options) {
12748
12752
  message.tool_calls = providerMessage.tool_calls;
12749
12753
  }
12750
12754
  const out = { message };
12755
+ if (choice.finish_reason != null) {
12756
+ out.finishReason = choice.finish_reason;
12757
+ out.providerTerminationComplete = data.choices.length === 1;
12758
+ }
12751
12759
  const providerReceipt = supportsOpenAIRequestIdentity
12752
12760
  ? _openAIProviderRequestReceipt(response, data, providerRequestId)
12753
12761
  : null;
@@ -14800,6 +14808,30 @@ function permissionDecision(callId, tool, result) {
14800
14808
  }
14801
14809
 
14802
14810
  export async function* agentLoop(messages, options) {
14811
+ // Bind the actual task before compaction, runtime nudges or provider projection.
14812
+ // A new host-admitted user turn starts a new loop and receives its own binding.
14813
+ const currentUserTask = messages.findLast(
14814
+ (message) =>
14815
+ message?.role === "user" && !isRuntimeOnlySessionMessage(message),
14816
+ );
14817
+ const currentUserTaskSnapshot = currentUserTask
14818
+ ? encodePersistedMessage(currentUserTask)
14819
+ : null;
14820
+ const currentUserTaskBinding = currentUserTaskSnapshot
14821
+ ? {
14822
+ message: currentUserTaskSnapshot,
14823
+ original: currentUserTask,
14824
+ allowContentRelocation:
14825
+ messages.filter(
14826
+ (message) =>
14827
+ message?.role === "user" &&
14828
+ isDeepStrictEqual(
14829
+ encodePersistedMessage(message),
14830
+ currentUserTaskSnapshot,
14831
+ ),
14832
+ ).length === 1,
14833
+ }
14834
+ : null;
14803
14835
  if (options.serviceTier != null) {
14804
14836
  resolveModelServiceTier(
14805
14837
  resolveModelCapabilityProfile(options),
@@ -15384,6 +15416,8 @@ export async function* agentLoop(messages, options) {
15384
15416
  // one-shot guard means a model that keeps returning empty turns completes
15385
15417
  // rather than looping forever (the iteration budget is the hard backstop).
15386
15418
  let emptyThinkingReprompted = false;
15419
+ let outputLimitRecoveryUsed = false;
15420
+ let outputLimitRecoveryPending = null;
15387
15421
  let readFileLoopGuard = new ReadFileLoopGuard();
15388
15422
  let remoteReadLoopGuard = new RemoteReadLoopGuard();
15389
15423
  const prActionTask = isPrActionRequest(messages);
@@ -16316,27 +16350,43 @@ export async function* agentLoop(messages, options) {
16316
16350
  index === resultIndex ? { ...result, content } : message,
16317
16351
  );
16318
16352
  } else {
16319
- // Full compaction can leave only the pinned request. Reiterate that
16320
- // exact request after the source data, not a replacement objective.
16321
- const request = requestMessages.findLast(
16353
+ // Full compaction can leave only the pinned request. Move that same
16354
+ // message after the source data, preserving its metadata and identity.
16355
+ const requestIndex = requestMessages.findLastIndex(
16322
16356
  (message) => message.role === "user",
16323
16357
  );
16358
+ const request = requestMessages[requestIndex];
16324
16359
  callMessages = [
16325
- ...requestMessages,
16360
+ ...requestMessages.filter((_, index) => index !== requestIndex),
16326
16361
  ...readContext.map((content) => ({ role: "assistant", content })),
16327
- {
16328
- role: "user",
16329
- content: request?.content || "Continue the original user task.",
16330
- },
16362
+ ...(request ? [request] : []),
16331
16363
  ];
16332
16364
  }
16333
16365
  }
16334
16366
  // Role normalization is request-only: durable history remains unchanged
16335
16367
  // for tool settlement and canonical compaction authority comparisons.
16368
+ let projectedUserTaskBinding = currentUserTaskBinding;
16336
16369
  if (typeof options.projectProviderMessages === "function") {
16337
- callMessages = options.projectProviderMessages(callMessages);
16370
+ const projected = projectMessagesWithTaskBinding(
16371
+ options.projectProviderMessages,
16372
+ callMessages,
16373
+ currentUserTaskBinding,
16374
+ );
16375
+ callMessages = projected.messages;
16376
+ projectedUserTaskBinding = projected.taskBinding;
16338
16377
  }
16339
16378
  const contextMemoryTrustedSystemIndexes = [];
16379
+ if (outputLimitRecoveryPending !== null) {
16380
+ // A synthetic user turn would replace the protected real task under
16381
+ // canonical context pressure. Keep this one-request hint out of history
16382
+ // and explicitly admit it as trusted system policy in the next plan.
16383
+ callMessages = [
16384
+ ...callMessages,
16385
+ { role: "system", content: outputLimitRecoveryPending },
16386
+ ];
16387
+ contextMemoryTrustedSystemIndexes.push(callMessages.length - 1);
16388
+ outputLimitRecoveryPending = null;
16389
+ }
16340
16390
  if (Number.isFinite(budget.limit)) {
16341
16391
  // consume() already charged this dispatch. The current request may still
16342
16392
  // act even when no further provider request remains. Rebuild this hint
@@ -16496,6 +16546,11 @@ export async function* agentLoop(messages, options) {
16496
16546
  contextMemoryToolDefinitions:
16497
16547
  getProviderToolDefinitions(iterationToolOptions),
16498
16548
  contextMemoryTrustedSystemIndexes,
16549
+ // Independent review uses its existing merged requirement journal and
16550
+ // validates every original requirement after planning below.
16551
+ contextMemoryRequiredUserBinding: independentReviewContext?.active
16552
+ ? null
16553
+ : projectedUserTaskBinding,
16499
16554
  },
16500
16555
  );
16501
16556
  if (canonicalProviderContext.plan) {
@@ -16761,6 +16816,78 @@ export async function* agentLoop(messages, options) {
16761
16816
  throw error;
16762
16817
  }
16763
16818
 
16819
+ if (result?.finishReason === "content_filter") {
16820
+ // Usage has settled above. A provider-filtered round is never a complete
16821
+ // answer or an executable tool batch, and must not trigger a fresh call.
16822
+ await _awaitBackgroundUsageSettlement(
16823
+ backgroundSubAgents,
16824
+ backgroundUsageFailureState,
16825
+ );
16826
+ const error = new Error(
16827
+ "The provider filtered this response. No tools from that response were executed; the task remains incomplete.",
16828
+ );
16829
+ error.code = "CC_MODEL_CONTENT_FILTER";
16830
+ throw error;
16831
+ }
16832
+
16833
+ if (
16834
+ result?.providerTerminationComplete === false &&
16835
+ result.finishReason != null &&
16836
+ result.finishReason !== "length"
16837
+ ) {
16838
+ // Explicit ambiguity is not an executable or final provider round.
16839
+ // Keep absent-reason compatibility and the separate length policy.
16840
+ await _awaitBackgroundUsageSettlement(
16841
+ backgroundSubAgents,
16842
+ backgroundUsageFailureState,
16843
+ );
16844
+ const error = new Error(
16845
+ "The provider response has an incomplete or ambiguous termination. No tools from that response were executed; the task remains incomplete.",
16846
+ );
16847
+ error.code = "CC_MODEL_TERMINATION_INCOMPLETE";
16848
+ throw error;
16849
+ }
16850
+
16851
+ if (result?.finishReason === "length") {
16852
+ // Settle the completed physical request above before any decision. Never
16853
+ // execute even syntactically valid calls from an incomplete generation,
16854
+ // and never insert its partial assistant/tool group into canonical history.
16855
+ const canRecover =
16856
+ result.providerTerminationComplete === true &&
16857
+ !msg?._truncated &&
16858
+ _hasCompleteProviderUsage(result.usage) &&
16859
+ !outputLimitRecoveryUsed &&
16860
+ budget.hasRemaining() &&
16861
+ !prInvestigationExhausted &&
16862
+ !inspectionSynthesisRequired;
16863
+ if (!canRecover) {
16864
+ await _awaitBackgroundUsageSettlement(
16865
+ backgroundSubAgents,
16866
+ backgroundUsageFailureState,
16867
+ );
16868
+ const error = new Error(
16869
+ "Model output reached its token limit. No tools from that response were executed; the task remains incomplete.",
16870
+ );
16871
+ error.code = "CC_MODEL_OUTPUT_LIMIT";
16872
+ throw error;
16873
+ }
16874
+ outputLimitRecoveryUsed = true;
16875
+ outputLimitRecoveryPending =
16876
+ "The previous model response reached its output token limit and was discarded. " +
16877
+ "None of its tool calls were executed. Continue from the last confirmed tool results. " +
16878
+ "Produce one small, complete operation per response. For large files, write a minimal " +
16879
+ "complete scaffold first, then use separate small edit_file calls to add sections. " +
16880
+ "Do not resend the entire large file or continue an unfinished JSON fragment. " +
16881
+ "If no tools are needed, give a shorter complete answer.";
16882
+ yield {
16883
+ type: "iteration-warning",
16884
+ message:
16885
+ "Model output was truncated; requesting one smaller complete step within the remaining budget.",
16886
+ };
16887
+ // This is a fresh budgeted iteration with new input, never a transport retry.
16888
+ continue;
16889
+ }
16890
+
16764
16891
  if (!msg) {
16765
16892
  await _awaitBackgroundUsageSettlement(
16766
16893
  backgroundSubAgents,
@@ -1,3 +1,4 @@
1
+ import { isDeepStrictEqual } from "node:util";
1
2
  import { canonicalJson } from "@chainlesschain/context-memory-kernel";
2
3
 
3
4
  /**
@@ -119,6 +120,8 @@ export function mergeConsecutiveMessages(messages) {
119
120
  * or folding newly added interrupt/feedback turns. Canonical fingerprints also
120
121
  * recognize the same retained messages after compaction recreates objects.
121
122
  */
123
+ const RESUME_ROLE_PROJECTIONS = new WeakMap();
124
+
122
125
  export function createResumeRoleProjection(messages) {
123
126
  const pairs = new Map();
124
127
  const pairKey = (left, right) => canonicalJson([left, right]);
@@ -137,20 +140,81 @@ export function createResumeRoleProjection(messages) {
137
140
  pairs.set(key, (pairs.get(key) || 0) + 1);
138
141
  }
139
142
  }
140
- return (requestMessages) => {
143
+ const project = (requestMessages) => {
141
144
  const remaining = new Map(pairs);
142
145
  const projected = [];
146
+ const sources = [];
143
147
  for (let index = 0; index < requestMessages.length; index++) {
144
148
  const message = requestMessages[index];
145
149
  const key =
146
150
  index > 0 ? pairKey(requestMessages[index - 1], message) : null;
147
151
  if (key && remaining.get(key) > 0) {
148
152
  const previous = projected.pop();
153
+ const previousSources = sources.pop();
149
154
  projected.push(...mergeConsecutiveMessages([previous, message]));
155
+ sources.push([...previousSources, index]);
150
156
  remaining.set(key, remaining.get(key) - 1);
151
- } else projected.push(message);
157
+ } else {
158
+ projected.push(message);
159
+ sources.push([index]);
160
+ }
152
161
  }
153
- return projected;
162
+ return { messages: projected, sources };
163
+ };
164
+ const projection = (requestMessages) => project(requestMessages).messages;
165
+ RESUME_ROLE_PROJECTIONS.set(projection, project);
166
+ return projection;
167
+ }
168
+
169
+ /**
170
+ * Preserve a bound task through this module's exact, captured resume merges.
171
+ * Arbitrary callbacks, wrappers and forged properties never receive lineage.
172
+ */
173
+ export function projectMessagesWithTaskBinding(projection, input, taskBinding) {
174
+ const owned = RESUME_ROLE_PROJECTIONS.get(projection);
175
+ if (!owned) {
176
+ return { messages: projection(input), taskBinding };
177
+ }
178
+ const projected = owned(input);
179
+ if (!taskBinding) return { messages: projected.messages, taskBinding };
180
+ const identities = input.flatMap((message, index) =>
181
+ message === taskBinding.original ? [index] : [],
182
+ );
183
+ const candidates = identities.length
184
+ ? identities
185
+ : taskBinding.allowContentRelocation === true
186
+ ? input.map((_, index) => index)
187
+ : [];
188
+ const matches = candidates.filter(
189
+ (index) =>
190
+ input[index]?.role === "user" &&
191
+ isDeepStrictEqual(input[index], taskBinding.message),
192
+ );
193
+ // Leave invalid bindings intact so canonical admission rejects them normally.
194
+ if (matches.length !== 1 || identities.length > 1)
195
+ return { messages: projected.messages, taskBinding };
196
+ const sourceIndex = matches[0];
197
+ const targetIndex = projected.sources.findIndex((group) =>
198
+ group.includes(sourceIndex),
199
+ );
200
+ const target = projected.messages[targetIndex];
201
+ if (target === input[sourceIndex])
202
+ return { messages: projected.messages, taskBinding };
203
+ // A merge may discard right-message metadata. Only bare user content has a
204
+ // complete preservation proof under the established joinContent contract.
205
+ if (
206
+ Object.keys(taskBinding.message).some(
207
+ (key) => !["role", "content"].includes(key),
208
+ )
209
+ )
210
+ return { messages: projected.messages, taskBinding };
211
+ return {
212
+ messages: projected.messages,
213
+ taskBinding: {
214
+ message: JSON.parse(canonicalJson(target)),
215
+ original: target,
216
+ allowContentRelocation: false,
217
+ },
154
218
  };
155
219
  }
156
220
 
@@ -291,6 +291,9 @@ export function _openaiCachedTokens(usage) {
291
291
  export function _openaiInitState() {
292
292
  return {
293
293
  responseId: null,
294
+ finishReason: null,
295
+ streamDone: false,
296
+ terminationInvalid: false,
294
297
  text: "",
295
298
  tools: [],
296
299
  inputTokens: null,
@@ -304,13 +307,39 @@ export function _openaiReduceLine(state, raw, onToken) {
304
307
  const line = (raw || "").trim();
305
308
  if (!line.startsWith("data:")) return state;
306
309
  const payload = line.slice(5).trim();
307
- if (!payload || payload === "[DONE]") return state;
310
+ if (!payload) return state;
311
+ if (payload === "[DONE]") {
312
+ if (!state.finishReason || state.streamDone)
313
+ state.terminationInvalid = true;
314
+ state.streamDone = true;
315
+ return state;
316
+ }
308
317
  let obj;
309
318
  try {
310
319
  obj = JSON.parse(payload);
311
320
  } catch {
321
+ state.terminationInvalid = true;
312
322
  return state;
313
323
  }
324
+ if (state.streamDone || obj.error) state.terminationInvalid = true;
325
+ if (!Array.isArray(obj.choices) || obj.choices.length > 1) {
326
+ state.terminationInvalid = true;
327
+ }
328
+ const choice = obj.choices?.[0];
329
+ if (choice) {
330
+ if (state.finishReason || (choice.index != null && choice.index !== 0))
331
+ state.terminationInvalid = true;
332
+ if (choice.finish_reason != null) {
333
+ // Terminal filtering cannot be downgraded to stop/tools by a later
334
+ // conflicting frame. Length likewise remains non-complete.
335
+ if (
336
+ state.finishReason !== "content_filter" &&
337
+ (state.finishReason !== "length" ||
338
+ choice.finish_reason === "content_filter")
339
+ )
340
+ state.finishReason = choice.finish_reason;
341
+ }
342
+ }
314
343
  if (!state.responseId) state.responseId = _providerReceiptId(obj.id);
315
344
  const pricing = projectEnvelopePricingContext(obj.usage || {}, obj);
316
345
  for (const [key, value] of Object.entries(pricing)) {
@@ -376,6 +405,11 @@ export function _openaiFinalize(state) {
376
405
  const message = { role: "assistant", content: state.text };
377
406
  if (toolCalls.length) message.tool_calls = toolCalls;
378
407
  const out = { message };
408
+ if (state.finishReason != null) {
409
+ out.finishReason = state.finishReason;
410
+ out.providerTerminationComplete =
411
+ state.streamDone && !state.terminationInvalid;
412
+ }
379
413
  if (
380
414
  state.usageInvalid !== true &&
381
415
  _isNonNegativeSafeInteger(state.inputTokens) &&