@khalilgharbaoui/opencode-claude-code-plugin 0.45.0 → 0.46.0
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/dist/index.d.ts +27 -0
- package/dist/index.js +233 -40
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +25 -4
package/dist/index.d.ts
CHANGED
|
@@ -433,6 +433,8 @@ interface ClaudeCodeConfig {
|
|
|
433
433
|
forkSessions?: boolean;
|
|
434
434
|
/** Resume a conversation's Claude session after opencode restarts. Default true. */
|
|
435
435
|
resumeAfterRestart?: boolean;
|
|
436
|
+
/** Carry a conversation's Claude session over when its model or effort changes. Default true. */
|
|
437
|
+
resumeAcrossModelChanges?: boolean;
|
|
436
438
|
logging?: LoggingConfig;
|
|
437
439
|
}
|
|
438
440
|
interface LoggingConfig {
|
|
@@ -829,6 +831,31 @@ interface ClaudeCodeProviderSettings {
|
|
|
829
831
|
* before. `false` never reads or writes the file.
|
|
830
832
|
*/
|
|
831
833
|
resumeAfterRestart?: boolean;
|
|
834
|
+
/**
|
|
835
|
+
* Carry a conversation's Claude session over when the operator changes its
|
|
836
|
+
* model or its reasoning effort mid-conversation, instead of replaying the
|
|
837
|
+
* whole thread as text into a fresh one. Default `true`.
|
|
838
|
+
*
|
|
839
|
+
* Both the model and the effort level are in the session key, so changing
|
|
840
|
+
* either lands the same opencode conversation on a key nothing has ever
|
|
841
|
+
* answered. That was the other half of issue #91: at least 18 replays in
|
|
842
|
+
* one ~15-hour session. Neither is a reason to start a new Claude
|
|
843
|
+
* conversation, because both are spawn-time choices (`--model`,
|
|
844
|
+
* `CLAUDE_CODE_EFFORT_LEVEL`) that the CLI applies to a transcript it
|
|
845
|
+
* resumes, measured on 2.1.288.
|
|
846
|
+
*
|
|
847
|
+
* It takes the Claude session off the sibling key rather than sharing it, so
|
|
848
|
+
* one conversation is owned by exactly one key, and it is refused on every
|
|
849
|
+
* term `resumeAfterRestart` is refused on (another account, a transcript
|
|
850
|
+
* that is gone, a conversation that has changed) plus two of its own: a
|
|
851
|
+
* sibling whose transcript may still be written to, and anything that is not
|
|
852
|
+
* the same cwd, request scope, opencode session, agent and prompt-cache TTL.
|
|
853
|
+
* A compaction turn and an account-failover switch never take one.
|
|
854
|
+
*
|
|
855
|
+
* It reads the record `resumeAfterRestart` writes, so `resumeAfterRestart:
|
|
856
|
+
* false` turns this off too.
|
|
857
|
+
*/
|
|
858
|
+
resumeAcrossModelChanges?: boolean;
|
|
832
859
|
/**
|
|
833
860
|
* Routing for Claude's built-in `WebSearch` tool.
|
|
834
861
|
*
|
package/dist/index.js
CHANGED
|
@@ -916,6 +916,10 @@ function formatResultFailureNote(message) {
|
|
|
916
916
|
${RESULT_ERROR_MARKER} ${message}
|
|
917
917
|
`;
|
|
918
918
|
}
|
|
919
|
+
function formatCompactionFailure(cause, detail) {
|
|
920
|
+
const said = detail?.trim();
|
|
921
|
+
return `Claude Code could not compact this conversation (${cause}), so no summary was produced.` + (said ? ` The CLI said: ${said}` : "");
|
|
922
|
+
}
|
|
919
923
|
var STREAM_TIMEOUT_MARKER = "\u258C **stream timeout:**";
|
|
920
924
|
function formatStreamTimeoutNote(silenceMs) {
|
|
921
925
|
const seconds = Math.max(1, Math.round(silenceMs / 1e3));
|
|
@@ -4250,7 +4254,8 @@ function messageDigest(msg) {
|
|
|
4250
4254
|
}
|
|
4251
4255
|
return hash.digest("hex").slice(0, 32);
|
|
4252
4256
|
}
|
|
4253
|
-
|
|
4257
|
+
var ASSISTANT_TURN_DIGEST = "assistant-turn";
|
|
4258
|
+
function conversationDigests(prompt, opts) {
|
|
4254
4259
|
const out = [];
|
|
4255
4260
|
for (const msg of prompt) {
|
|
4256
4261
|
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool") {
|
|
@@ -4258,7 +4263,7 @@ function conversationDigests(prompt) {
|
|
|
4258
4263
|
}
|
|
4259
4264
|
out.push({
|
|
4260
4265
|
role: msg.role,
|
|
4261
|
-
digest: messageDigest(msg)
|
|
4266
|
+
digest: opts?.assistantContent === false && msg.role === "assistant" ? ASSISTANT_TURN_DIGEST : messageDigest(msg)
|
|
4262
4267
|
});
|
|
4263
4268
|
}
|
|
4264
4269
|
return out;
|
|
@@ -4285,7 +4290,24 @@ function forkSiblingSignature(sessionKey2) {
|
|
|
4285
4290
|
blanked[3] = "*";
|
|
4286
4291
|
return blanked.join("::");
|
|
4287
4292
|
}
|
|
4288
|
-
function
|
|
4293
|
+
function modelSiblingSignature(sessionKey2) {
|
|
4294
|
+
const parts = sessionKey2.split("::");
|
|
4295
|
+
if (parts.length < 5) return null;
|
|
4296
|
+
if (parts[2] === "compaction") return null;
|
|
4297
|
+
const withoutEffort = parts[parts.length - 1].startsWith("effort=") ? parts.slice(0, -1) : parts;
|
|
4298
|
+
if (withoutEffort.length < 5) return null;
|
|
4299
|
+
const blanked = [...withoutEffort];
|
|
4300
|
+
blanked[1] = "*";
|
|
4301
|
+
return blanked.join("::");
|
|
4302
|
+
}
|
|
4303
|
+
function moveForkFingerprint(fromKey, toKey) {
|
|
4304
|
+
const record = fingerprints.get(fromKey);
|
|
4305
|
+
if (!record) return;
|
|
4306
|
+
fingerprints.delete(fromKey);
|
|
4307
|
+
fingerprints.delete(toKey);
|
|
4308
|
+
fingerprints.set(toKey, { ...record, seq: ++fingerprintSeq });
|
|
4309
|
+
}
|
|
4310
|
+
function splitForkHistory(prompt, opts) {
|
|
4289
4311
|
const messages = prompt;
|
|
4290
4312
|
let lastAssistant = -1;
|
|
4291
4313
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -4304,7 +4326,7 @@ function splitForkHistory(prompt) {
|
|
|
4304
4326
|
}
|
|
4305
4327
|
}
|
|
4306
4328
|
return {
|
|
4307
|
-
history: conversationDigests(messages.slice(0, lastAssistant + 1)),
|
|
4329
|
+
history: conversationDigests(messages.slice(0, lastAssistant + 1), opts),
|
|
4308
4330
|
forkable: true
|
|
4309
4331
|
};
|
|
4310
4332
|
}
|
|
@@ -4343,8 +4365,8 @@ function findForkParent(opts) {
|
|
|
4343
4365
|
}
|
|
4344
4366
|
return best;
|
|
4345
4367
|
}
|
|
4346
|
-
function continuesRecordedConversation(recorded, prompt) {
|
|
4347
|
-
const { history, forkable } = splitForkHistory(prompt);
|
|
4368
|
+
function continuesRecordedConversation(recorded, prompt, opts) {
|
|
4369
|
+
const { history, forkable } = splitForkHistory(prompt, opts);
|
|
4348
4370
|
if (!forkable || recorded.length === 0 || recorded.length > history.length) return false;
|
|
4349
4371
|
return isPrefix(recorded, history) && tailIsReplyOnly(history, recorded.length);
|
|
4350
4372
|
}
|
|
@@ -4381,7 +4403,7 @@ function isDigest(value) {
|
|
|
4381
4403
|
}
|
|
4382
4404
|
function isRecord2(value) {
|
|
4383
4405
|
const record = value;
|
|
4384
|
-
return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest);
|
|
4406
|
+
return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest) && (record.shape === void 0 || Array.isArray(record.shape) && record.shape.every(isDigest));
|
|
4385
4407
|
}
|
|
4386
4408
|
function load() {
|
|
4387
4409
|
if (records) return records;
|
|
@@ -4430,13 +4452,14 @@ function save(update) {
|
|
|
4430
4452
|
function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath) {
|
|
4431
4453
|
const chain = conversationDigests(prompt);
|
|
4432
4454
|
if (chain.length === 0) return;
|
|
4455
|
+
const shape2 = conversationDigests(prompt, { assistantContent: false });
|
|
4433
4456
|
const current = load().get(sessionKey2);
|
|
4434
|
-
if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
|
|
4457
|
+
if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.shape !== void 0 && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
|
|
4435
4458
|
return;
|
|
4436
4459
|
}
|
|
4437
4460
|
save((merged) => {
|
|
4438
4461
|
merged.delete(sessionKey2);
|
|
4439
|
-
merged.set(sessionKey2, { claudeSessionId, cliPath, chain, updatedAt: Date.now() });
|
|
4462
|
+
merged.set(sessionKey2, { claudeSessionId, cliPath, chain, shape: shape2, updatedAt: Date.now() });
|
|
4440
4463
|
});
|
|
4441
4464
|
}
|
|
4442
4465
|
function forgetResumePoint(sessionKey2) {
|
|
@@ -4445,25 +4468,84 @@ function forgetResumePoint(sessionKey2) {
|
|
|
4445
4468
|
merged.delete(sessionKey2);
|
|
4446
4469
|
});
|
|
4447
4470
|
}
|
|
4471
|
+
function moveResumePoint(fromKey, toKey) {
|
|
4472
|
+
const record = load().get(fromKey);
|
|
4473
|
+
if (!record) return;
|
|
4474
|
+
save((merged) => {
|
|
4475
|
+
merged.delete(fromKey);
|
|
4476
|
+
if (merged.has(toKey)) return;
|
|
4477
|
+
merged.set(toKey, { ...record, updatedAt: Date.now() });
|
|
4478
|
+
});
|
|
4479
|
+
}
|
|
4448
4480
|
function findResumePoint(opts) {
|
|
4449
4481
|
const record = load().get(opts.sessionKey);
|
|
4450
|
-
if (!record)
|
|
4482
|
+
if (!record) {
|
|
4483
|
+
opts.onRefused?.("no-session-recorded");
|
|
4484
|
+
return void 0;
|
|
4485
|
+
}
|
|
4451
4486
|
const refuse = (reason) => {
|
|
4452
4487
|
log.info("not resuming the claude session from before the restart", {
|
|
4453
4488
|
sessionKey: opts.sessionKey,
|
|
4454
4489
|
reason
|
|
4455
4490
|
});
|
|
4491
|
+
opts.onRefused?.(reason);
|
|
4456
4492
|
return void 0;
|
|
4457
4493
|
};
|
|
4458
|
-
if (record.cliPath !== opts.cliPath) return refuse("another
|
|
4494
|
+
if (record.cliPath !== opts.cliPath) return refuse("another-claude-binary");
|
|
4459
4495
|
if (!continuesRecordedConversation(record.chain, opts.prompt)) {
|
|
4460
|
-
return refuse("
|
|
4496
|
+
return refuse("conversation-changed");
|
|
4461
4497
|
}
|
|
4462
4498
|
if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
|
|
4463
|
-
return refuse("
|
|
4499
|
+
return refuse("transcript-gone");
|
|
4464
4500
|
}
|
|
4465
4501
|
return { claudeSessionId: record.claudeSessionId, matched: record.chain.length };
|
|
4466
4502
|
}
|
|
4503
|
+
function findSiblingResumePoint(opts) {
|
|
4504
|
+
const signature = modelSiblingSignature(opts.sessionKey);
|
|
4505
|
+
if (!signature) {
|
|
4506
|
+
opts.onRefused?.("no-sibling-possible");
|
|
4507
|
+
return void 0;
|
|
4508
|
+
}
|
|
4509
|
+
let best;
|
|
4510
|
+
let bestUpdatedAt = -1;
|
|
4511
|
+
let refusal;
|
|
4512
|
+
for (const [key, record] of load()) {
|
|
4513
|
+
if (key === opts.sessionKey) continue;
|
|
4514
|
+
if (modelSiblingSignature(key) !== signature) continue;
|
|
4515
|
+
if (record.cliPath !== opts.cliPath) {
|
|
4516
|
+
refusal = "sibling-another-claude-binary";
|
|
4517
|
+
continue;
|
|
4518
|
+
}
|
|
4519
|
+
if (!record.shape || !continuesRecordedConversation(record.shape, opts.prompt, {
|
|
4520
|
+
assistantContent: false
|
|
4521
|
+
})) {
|
|
4522
|
+
refusal = "sibling-conversation-changed";
|
|
4523
|
+
continue;
|
|
4524
|
+
}
|
|
4525
|
+
if (opts.isBusy(key)) {
|
|
4526
|
+
refusal = "sibling-still-writing";
|
|
4527
|
+
continue;
|
|
4528
|
+
}
|
|
4529
|
+
if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
|
|
4530
|
+
refusal = "sibling-transcript-gone";
|
|
4531
|
+
continue;
|
|
4532
|
+
}
|
|
4533
|
+
if (record.updatedAt >= bestUpdatedAt) {
|
|
4534
|
+
bestUpdatedAt = record.updatedAt;
|
|
4535
|
+
best = {
|
|
4536
|
+
claudeSessionId: record.claudeSessionId,
|
|
4537
|
+
siblingKey: key,
|
|
4538
|
+
matched: record.chain.length
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
if (!best) {
|
|
4543
|
+
opts.onRefused?.(
|
|
4544
|
+
refusal ?? "no-sibling-recorded"
|
|
4545
|
+
);
|
|
4546
|
+
}
|
|
4547
|
+
return best;
|
|
4548
|
+
}
|
|
4467
4549
|
|
|
4468
4550
|
// src/permission-presets.ts
|
|
4469
4551
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
@@ -5164,6 +5246,23 @@ function deleteClaudeSessionId(key) {
|
|
|
5164
5246
|
forgetForkFingerprint(key);
|
|
5165
5247
|
forgetResumePoint(key);
|
|
5166
5248
|
}
|
|
5249
|
+
function transferClaudeSession(fromKey, toKey) {
|
|
5250
|
+
if (fromKey === toKey) return void 0;
|
|
5251
|
+
const claudeSessionId = claudeSessions.get(fromKey);
|
|
5252
|
+
if (!claudeSessionId) return void 0;
|
|
5253
|
+
if (claudeSessions.has(toKey)) return void 0;
|
|
5254
|
+
claudeSessions.delete(fromKey);
|
|
5255
|
+
setClaudeSessionId(toKey, claudeSessionId);
|
|
5256
|
+
moveResumePoint(fromKey, toKey);
|
|
5257
|
+
moveForkFingerprint(fromKey, toKey);
|
|
5258
|
+
clearExitPlanModeQuestions(fromKey);
|
|
5259
|
+
clearAccountFailoverQuestions(fromKey);
|
|
5260
|
+
log.notice("carrying the claude conversation over to the new session key", {
|
|
5261
|
+
fromKey,
|
|
5262
|
+
toKey
|
|
5263
|
+
});
|
|
5264
|
+
return claudeSessionId;
|
|
5265
|
+
}
|
|
5167
5266
|
function effortSessionKey(baseKey, effort) {
|
|
5168
5267
|
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
5169
5268
|
}
|
|
@@ -5186,8 +5285,14 @@ function invalidateOtherEffortSessions(baseKey, effort) {
|
|
|
5186
5285
|
);
|
|
5187
5286
|
}
|
|
5188
5287
|
}
|
|
5288
|
+
const currentKey = effortSessionKey(baseKey, effort);
|
|
5189
5289
|
for (const key of staleKeys) {
|
|
5190
5290
|
deleteActiveProcess(key);
|
|
5291
|
+
if (transferClaudeSession(key, currentKey)) {
|
|
5292
|
+
clearCompression(key);
|
|
5293
|
+
clearBackgroundTasks(key);
|
|
5294
|
+
continue;
|
|
5295
|
+
}
|
|
5191
5296
|
deleteClaudeSessionId(key);
|
|
5192
5297
|
clearCompression(key);
|
|
5193
5298
|
clearBackgroundTasks(key);
|
|
@@ -5270,12 +5375,11 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
5270
5375
|
}
|
|
5271
5376
|
}
|
|
5272
5377
|
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
5273
|
-
log.info("process exited with error
|
|
5378
|
+
log.info("process exited with error; keeping the claude session to resume", {
|
|
5274
5379
|
code,
|
|
5275
|
-
sessionKey: sessionKey2
|
|
5380
|
+
sessionKey: sessionKey2,
|
|
5381
|
+
resumable: claudeSessions.has(sessionKey2)
|
|
5276
5382
|
});
|
|
5277
|
-
claudeSessions.delete(sessionKey2);
|
|
5278
|
-
forgetResumePoint(sessionKey2);
|
|
5279
5383
|
}
|
|
5280
5384
|
});
|
|
5281
5385
|
proc.stderr?.on("data", (data) => {
|
|
@@ -5788,6 +5892,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5788
5892
|
"btw: a turn is running but no claude process appeared for it",
|
|
5789
5893
|
"btw: early aside failed; the message will ask again",
|
|
5790
5894
|
"cannot interrupt this transport; waiting for the turn to end",
|
|
5895
|
+
"carrying the claude conversation over to the new session key",
|
|
5791
5896
|
"claude --version output unparseable",
|
|
5792
5897
|
"claude account is out of usage; the turn ends with a note",
|
|
5793
5898
|
"claude already registers a different skill under this name; not bridging ours",
|
|
@@ -5795,6 +5900,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5795
5900
|
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
5796
5901
|
"claude code compacted its own context",
|
|
5797
5902
|
"claude code reset its conversation",
|
|
5903
|
+
"claude could not compact the conversation; no summary was stored",
|
|
5798
5904
|
"claude finished the turn without a reply",
|
|
5799
5905
|
"claude kept missing the probe deadline; keeping the conservative answer for this process",
|
|
5800
5906
|
"claude process belongs to another account; starting fresh",
|
|
@@ -5810,6 +5916,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5810
5916
|
"cleanup-stale: could not record the sweep marker",
|
|
5811
5917
|
"cleanup-stale: error processing cache root",
|
|
5812
5918
|
"cleanup-stale: rmSync failed",
|
|
5919
|
+
"continuing this conversation's claude session under the new model or effort instead of replaying it",
|
|
5813
5920
|
"delivering proxy results after interrupted continuation",
|
|
5814
5921
|
"draining proxy calls queued between turns",
|
|
5815
5922
|
"empty user content; sending sentinel to satisfy CLI",
|
|
@@ -5881,6 +5988,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5881
5988
|
"rejected pending proxy call",
|
|
5882
5989
|
"remote URL images are not supported by Claude CLI, skipping",
|
|
5883
5990
|
"replaying stdout the child emitted between turns",
|
|
5991
|
+
"replaying the conversation as text: no claude session was available to continue",
|
|
5884
5992
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
5885
5993
|
"resuming the claude session from before the restart instead of replaying it",
|
|
5886
5994
|
"shared Claude capability already exists; leaving untouched",
|
|
@@ -5936,6 +6044,7 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
5936
6044
|
inFlight: "flag",
|
|
5937
6045
|
anthropicApiKeyInEnv: "flag",
|
|
5938
6046
|
includeHistoryContext: "flag",
|
|
6047
|
+
resumable: "flag",
|
|
5939
6048
|
interactive: "flag",
|
|
5940
6049
|
interactiveTransport: "flag",
|
|
5941
6050
|
isError: "flag",
|
|
@@ -5957,6 +6066,8 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
5957
6066
|
dropped: "count",
|
|
5958
6067
|
exitCode: "count",
|
|
5959
6068
|
historyLength: "count",
|
|
6069
|
+
matchedMessages: "count",
|
|
6070
|
+
messages: "count",
|
|
5960
6071
|
inputTokens: "count",
|
|
5961
6072
|
lastTextLength: "count",
|
|
5962
6073
|
lines: "count",
|
|
@@ -6058,6 +6169,12 @@ var BUNDLE_DATA_ALLOWLIST = {
|
|
|
6058
6169
|
sessionId: "id",
|
|
6059
6170
|
sessionID: "id",
|
|
6060
6171
|
sessionKey: "id",
|
|
6172
|
+
// The two ends of a conversation carried from one session key to another
|
|
6173
|
+
// (h #g215), and the fork lookup's parent. All three are session keys.
|
|
6174
|
+
fromKey: "id",
|
|
6175
|
+
toKey: "id",
|
|
6176
|
+
siblingKey: "id",
|
|
6177
|
+
parentKey: "id",
|
|
6061
6178
|
toolCallId: "id",
|
|
6062
6179
|
toolCallIds: "id",
|
|
6063
6180
|
toolUseId: "id",
|
|
@@ -8276,7 +8393,7 @@ The complete prior conversation appears above. The synthesis instructions follow
|
|
|
8276
8393
|
content.push({
|
|
8277
8394
|
type: "text",
|
|
8278
8395
|
text: `<conversation_history>
|
|
8279
|
-
The following is a summary of our conversation so far
|
|
8396
|
+
The following is a summary of our conversation so far, replayed as text because no Claude session was available to continue it:
|
|
8280
8397
|
|
|
8281
8398
|
${historyContext}
|
|
8282
8399
|
|
|
@@ -11326,6 +11443,7 @@ function createTurnState(init) {
|
|
|
11326
11443
|
accountLimitHit: null,
|
|
11327
11444
|
apiErrorTextShown: null,
|
|
11328
11445
|
usageLimitNote: false,
|
|
11446
|
+
compactionFailureText: null,
|
|
11329
11447
|
accountBlock: null,
|
|
11330
11448
|
modelRefusal: null,
|
|
11331
11449
|
drainBuffer: [],
|
|
@@ -12420,6 +12538,7 @@ ${plan}
|
|
|
12420
12538
|
resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
12421
12539
|
}));
|
|
12422
12540
|
const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
|
|
12541
|
+
const compactionApiError = ctx.compactionMode && (apiErrorReply || typeof msg.error === "string");
|
|
12423
12542
|
if (limitFromReply) {
|
|
12424
12543
|
state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
|
|
12425
12544
|
const said = limitFactsFromText(
|
|
@@ -12441,6 +12560,10 @@ ${plan}
|
|
|
12441
12560
|
for (const block2 of msg.message.content) {
|
|
12442
12561
|
if (block2.type === "text" && block2.text) {
|
|
12443
12562
|
if (limitErrorReply) continue;
|
|
12563
|
+
if (compactionApiError) {
|
|
12564
|
+
state.compactionFailureText ??= block2.text;
|
|
12565
|
+
continue;
|
|
12566
|
+
}
|
|
12444
12567
|
if (apiErrorReply) state.apiErrorTextShown = block2.text;
|
|
12445
12568
|
resetLastVisibleTextBlock(state);
|
|
12446
12569
|
const blockId = state.startTextBlock();
|
|
@@ -12666,7 +12789,14 @@ ${plan}
|
|
|
12666
12789
|
state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
|
|
12667
12790
|
// and says what to run; two notes about one failure is one too many.
|
|
12668
12791
|
!state.accountBlock && msg.is_error === true;
|
|
12669
|
-
|
|
12792
|
+
const compactionFailure = ctx.compactionMode && (msg.is_error === true || typeof msg.subtype === "string" && msg.subtype !== "success");
|
|
12793
|
+
if (compactionFailure) {
|
|
12794
|
+
if (typeof msg.result === "string" && msg.result.trim()) {
|
|
12795
|
+
state.compactionFailureText ??= msg.result.trim();
|
|
12796
|
+
}
|
|
12797
|
+
state.resultFailure ??= state.accountBlock ?? (state.accountLimitHit ? "usage_limit" : void 0) ?? (typeof msg.subtype === "string" && msg.subtype !== "success" ? msg.subtype : "error");
|
|
12798
|
+
}
|
|
12799
|
+
if (!state.currentTextId && msg.is_error && !compactionFailure && // Already on screen from the reply frame, word for word.
|
|
12670
12800
|
!(typeof msg.result === "string" && state.apiErrorTextShown !== null && msg.result.trim() === state.apiErrorTextShown.trim()) && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
12671
12801
|
const errId = state.startTextBlock();
|
|
12672
12802
|
state.controller.enqueue({
|
|
@@ -12681,21 +12811,23 @@ ${plan}
|
|
|
12681
12811
|
if (state.accountBlock && msg.is_error) {
|
|
12682
12812
|
state.resultFailure ??= state.accountBlock;
|
|
12683
12813
|
const offeringSwitch = ctx.failoverAskActive;
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
12693
|
-
|
|
12694
|
-
|
|
12695
|
-
|
|
12696
|
-
|
|
12697
|
-
|
|
12698
|
-
|
|
12814
|
+
if (!compactionFailure) {
|
|
12815
|
+
state.controller.enqueue({
|
|
12816
|
+
type: "text-delta",
|
|
12817
|
+
id: state.startTextBlock(),
|
|
12818
|
+
delta: formatAccountBlockNote({
|
|
12819
|
+
kind: state.accountBlock,
|
|
12820
|
+
account: ctx.sourceAccount,
|
|
12821
|
+
configDir: ctx.config.configDir,
|
|
12822
|
+
offeringSwitch,
|
|
12823
|
+
// Read only when the form is not taking the turn, which is the
|
|
12824
|
+
// default: moving the work by hand is then the operator's only
|
|
12825
|
+
// route and nothing else on screen says it exists (h #g194).
|
|
12826
|
+
candidates: ctx.failoverAccounts
|
|
12827
|
+
})
|
|
12828
|
+
});
|
|
12829
|
+
state.endTextBlock();
|
|
12830
|
+
}
|
|
12699
12831
|
log.warn(`Claude account "${ctx.sourceAccount}" cannot serve requests`, {
|
|
12700
12832
|
sessionKey: state.sessionKey,
|
|
12701
12833
|
kind: state.accountBlock,
|
|
@@ -12705,11 +12837,14 @@ ${plan}
|
|
|
12705
12837
|
const failure = describeResultFailure(msg);
|
|
12706
12838
|
if (failure) {
|
|
12707
12839
|
state.resultFailure = msg.subtype;
|
|
12708
|
-
|
|
12709
|
-
|
|
12710
|
-
|
|
12711
|
-
|
|
12712
|
-
|
|
12840
|
+
if (!compactionFailure) {
|
|
12841
|
+
state.controller.enqueue({
|
|
12842
|
+
type: "text-delta",
|
|
12843
|
+
id: state.startTextBlock(),
|
|
12844
|
+
delta: formatResultFailureNote(failure)
|
|
12845
|
+
});
|
|
12846
|
+
}
|
|
12847
|
+
if (compactionFailure) state.compactionFailureText ??= failure;
|
|
12713
12848
|
log.warn(failure, { sessionKey: state.sessionKey, subtype: msg.subtype });
|
|
12714
12849
|
}
|
|
12715
12850
|
const turnStats = extractTurnStats(msg);
|
|
@@ -13519,13 +13654,18 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13519
13654
|
}
|
|
13520
13655
|
}
|
|
13521
13656
|
}
|
|
13657
|
+
let replayReason = "no-session-for-key";
|
|
13522
13658
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
13523
13659
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
13660
|
+
const transcriptPath = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
13524
13661
|
const resumePoint = findResumePoint({
|
|
13525
13662
|
sessionKey: sk,
|
|
13526
13663
|
prompt: options.prompt,
|
|
13527
13664
|
cliPath,
|
|
13528
|
-
transcriptPath
|
|
13665
|
+
transcriptPath,
|
|
13666
|
+
onRefused: (reason) => {
|
|
13667
|
+
replayReason = reason;
|
|
13668
|
+
}
|
|
13529
13669
|
});
|
|
13530
13670
|
if (resumePoint) {
|
|
13531
13671
|
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
@@ -13534,8 +13674,47 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13534
13674
|
sessionKey: sk,
|
|
13535
13675
|
matchedMessages: resumePoint.matched
|
|
13536
13676
|
});
|
|
13677
|
+
} else if (this.config.resumeAcrossModelChanges !== false) {
|
|
13678
|
+
const sibling = findSiblingResumePoint({
|
|
13679
|
+
sessionKey: sk,
|
|
13680
|
+
prompt: options.prompt,
|
|
13681
|
+
cliPath,
|
|
13682
|
+
transcriptPath,
|
|
13683
|
+
// `claudeSessionIsWriting`, not `getActiveProcess`: this walks every
|
|
13684
|
+
// sibling key, and that one refreshes LRU order and cancels idle
|
|
13685
|
+
// timers on everything it touches (the same reason the fork lookup
|
|
13686
|
+
// above uses it).
|
|
13687
|
+
isBusy: claudeSessionIsWriting,
|
|
13688
|
+
onRefused: (reason) => {
|
|
13689
|
+
replayReason = reason;
|
|
13690
|
+
}
|
|
13691
|
+
});
|
|
13692
|
+
if (sibling) {
|
|
13693
|
+
deleteActiveProcess(sibling.siblingKey);
|
|
13694
|
+
const carried = transferClaudeSession(sibling.siblingKey, sk);
|
|
13695
|
+
if (carried) {
|
|
13696
|
+
includeHistoryContext = false;
|
|
13697
|
+
log.notice(
|
|
13698
|
+
"continuing this conversation's claude session under the new model or effort instead of replaying it",
|
|
13699
|
+
{
|
|
13700
|
+
sessionKey: sk,
|
|
13701
|
+
siblingKey: sibling.siblingKey,
|
|
13702
|
+
matchedMessages: sibling.matched
|
|
13703
|
+
}
|
|
13704
|
+
);
|
|
13705
|
+
} else {
|
|
13706
|
+
replayReason = "sibling-carry-over-failed";
|
|
13707
|
+
}
|
|
13708
|
+
}
|
|
13537
13709
|
}
|
|
13538
13710
|
}
|
|
13711
|
+
if (includeHistoryContext && !compactionMode) {
|
|
13712
|
+
log.notice("replaying the conversation as text: no claude session was available to continue", {
|
|
13713
|
+
sessionKey: sk,
|
|
13714
|
+
reason: replayReason,
|
|
13715
|
+
messages: options.prompt.length
|
|
13716
|
+
});
|
|
13717
|
+
}
|
|
13539
13718
|
if (this.config.forkSessions === true && !compactionMode) {
|
|
13540
13719
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
13541
13720
|
}
|
|
@@ -14288,6 +14467,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14288
14467
|
})
|
|
14289
14468
|
});
|
|
14290
14469
|
state.endTextBlock();
|
|
14470
|
+
state.resultFailure ??= "usage_limit";
|
|
14291
14471
|
}
|
|
14292
14472
|
if (runAutoContinue(state, msg)) return;
|
|
14293
14473
|
for (const [idx, reasoningId] of state.reasoningIds) {
|
|
@@ -14321,6 +14501,18 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14321
14501
|
});
|
|
14322
14502
|
state.endTextBlock();
|
|
14323
14503
|
}
|
|
14504
|
+
if (compactionMode && state.resultFailure) {
|
|
14505
|
+
log.warn("claude could not compact the conversation; no summary was stored", {
|
|
14506
|
+
sessionKey: sk,
|
|
14507
|
+
cause: state.resultFailure
|
|
14508
|
+
});
|
|
14509
|
+
controller.enqueue({
|
|
14510
|
+
type: "error",
|
|
14511
|
+
error: new Error(
|
|
14512
|
+
formatCompactionFailure(state.resultFailure, state.compactionFailureText)
|
|
14513
|
+
)
|
|
14514
|
+
});
|
|
14515
|
+
}
|
|
14324
14516
|
const usage = toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage));
|
|
14325
14517
|
controller.enqueue({
|
|
14326
14518
|
type: "finish",
|
|
@@ -15519,6 +15711,7 @@ function createClaudeCode(settings = {}) {
|
|
|
15519
15711
|
turnStats: settings.turnStats === true,
|
|
15520
15712
|
forkSessions: settings.forkSessions === true,
|
|
15521
15713
|
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
15714
|
+
resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,
|
|
15522
15715
|
transport: settings.transport,
|
|
15523
15716
|
interactive: settings.interactive,
|
|
15524
15717
|
interactiveBypass: settings.interactiveBypass,
|