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 +13 -3
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/data/changelog.json +13 -0
- package/src/lib/context-memory-kernel/message-adapter.js +27 -6
- package/src/lib/context-memory-kernel/provider-context.js +48 -3
- package/src/runtime/agent-core.js +136 -9
- package/src/runtime/message-roles.js +67 -3
- package/src/runtime/provider-stream-state.js +35 -1
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.
|
|
5
|
+
## Source candidate 0.166.132 (unreleased, 2026-10-30)
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
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
package/src/data/changelog.json
CHANGED
|
@@ -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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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[
|
|
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 =
|
|
215
|
-
|
|
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.
|
|
16320
|
-
//
|
|
16321
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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) &&
|