@khalilgharbaoui/opencode-claude-code-plugin 0.44.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 +252 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +28 -6
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",
|
|
@@ -5897,7 +6005,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5897
6005
|
"two different skills share a name; bridging the one the nearest root claimed",
|
|
5898
6006
|
"unknown permissionPreset; no preset applied and every permission setting is left exactly as configured",
|
|
5899
6007
|
"unsupported file part data type",
|
|
5900
|
-
"unsupported media type for Claude
|
|
6008
|
+
"unsupported media type for Claude attachment block, skipping"
|
|
5901
6009
|
]);
|
|
5902
6010
|
|
|
5903
6011
|
// src/diagnostic-bundle.ts
|
|
@@ -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",
|
|
@@ -7253,7 +7370,7 @@ var baseCapabilities = {
|
|
|
7253
7370
|
temperature: false,
|
|
7254
7371
|
attachment: true,
|
|
7255
7372
|
toolcall: true,
|
|
7256
|
-
input: { text: true, audio: false, image: true, video: false, pdf:
|
|
7373
|
+
input: { text: true, audio: false, image: true, video: false, pdf: true },
|
|
7257
7374
|
output: { text: true, audio: false, image: false, video: false, pdf: false },
|
|
7258
7375
|
interleaved: false
|
|
7259
7376
|
};
|
|
@@ -8010,13 +8127,14 @@ function stripContextReminders(prompt) {
|
|
|
8010
8127
|
});
|
|
8011
8128
|
return removed > 0 ? { prompt: out, removed } : { prompt, removed: 0 };
|
|
8012
8129
|
}
|
|
8130
|
+
var PDF_MEDIA_TYPE = "application/pdf";
|
|
8013
8131
|
var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
|
|
8014
8132
|
"image/jpeg",
|
|
8015
8133
|
"image/png",
|
|
8016
8134
|
"image/gif",
|
|
8017
8135
|
"image/webp"
|
|
8018
8136
|
]);
|
|
8019
|
-
function
|
|
8137
|
+
function toAttachmentBlock(part) {
|
|
8020
8138
|
const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
|
|
8021
8139
|
if (!raw) {
|
|
8022
8140
|
log.warn("file part without data, skipping");
|
|
@@ -8048,8 +8166,14 @@ function toImageBlock(part) {
|
|
|
8048
8166
|
log.warn("unsupported file part data type", { dataType: typeof raw });
|
|
8049
8167
|
return null;
|
|
8050
8168
|
}
|
|
8169
|
+
if (resolvedMediaType.toLowerCase() === PDF_MEDIA_TYPE) {
|
|
8170
|
+
return {
|
|
8171
|
+
type: "document",
|
|
8172
|
+
source: { type: "base64", media_type: PDF_MEDIA_TYPE, data: base64 }
|
|
8173
|
+
};
|
|
8174
|
+
}
|
|
8051
8175
|
if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
|
|
8052
|
-
log.warn("unsupported media type for Claude
|
|
8176
|
+
log.warn("unsupported media type for Claude attachment block, skipping", {
|
|
8053
8177
|
mediaType: resolvedMediaType
|
|
8054
8178
|
});
|
|
8055
8179
|
return null;
|
|
@@ -8269,7 +8393,7 @@ The complete prior conversation appears above. The synthesis instructions follow
|
|
|
8269
8393
|
content.push({
|
|
8270
8394
|
type: "text",
|
|
8271
8395
|
text: `<conversation_history>
|
|
8272
|
-
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:
|
|
8273
8397
|
|
|
8274
8398
|
${historyContext}
|
|
8275
8399
|
|
|
@@ -8301,11 +8425,11 @@ Now continuing with the current message:
|
|
|
8301
8425
|
content.push({ type: "text", text: part.text });
|
|
8302
8426
|
}
|
|
8303
8427
|
} else if (part.type === "file" || part.type === "image") {
|
|
8304
|
-
const block =
|
|
8428
|
+
const block = toAttachmentBlock(part);
|
|
8305
8429
|
if (block) {
|
|
8306
8430
|
content.push(block);
|
|
8307
8431
|
} else {
|
|
8308
|
-
log.debug("skipped
|
|
8432
|
+
log.debug("skipped unsupported file part", {
|
|
8309
8433
|
mediaType: part.mediaType
|
|
8310
8434
|
});
|
|
8311
8435
|
}
|
|
@@ -8365,7 +8489,7 @@ function getTrailingUserMessages(prompt, opts = {}) {
|
|
|
8365
8489
|
if (part.type === "text") {
|
|
8366
8490
|
if (part.text && part.text.trim()) blocks.push({ type: "text", text: part.text });
|
|
8367
8491
|
} else if (part.type === "file" || part.type === "image") {
|
|
8368
|
-
const block =
|
|
8492
|
+
const block = toAttachmentBlock(part);
|
|
8369
8493
|
if (block) blocks.push(block);
|
|
8370
8494
|
}
|
|
8371
8495
|
}
|
|
@@ -9780,12 +9904,14 @@ function decodeUserEnvelope(chunk, saveImage) {
|
|
|
9780
9904
|
const images = [];
|
|
9781
9905
|
let dropped = 0;
|
|
9782
9906
|
for (const block of content) {
|
|
9783
|
-
const
|
|
9907
|
+
const base64 = block?.source?.type === "base64" && typeof block.source.data === "string";
|
|
9908
|
+
const mediaType = String(block?.source?.media_type).toLowerCase();
|
|
9909
|
+
const extension = base64 && block.type === "image" ? IMAGE_EXTENSIONS[mediaType] : base64 && block.type === "document" && mediaType === "application/pdf" ? "pdf" : void 0;
|
|
9784
9910
|
if (block?.type === "text" && typeof block.text === "string") {
|
|
9785
9911
|
parts.push(block.text);
|
|
9786
9912
|
} else if (extension && saveImage) {
|
|
9787
9913
|
const file = saveImage(Buffer.from(block.source.data, "base64"), extension);
|
|
9788
|
-
if (file) images.push(file);
|
|
9914
|
+
if (file) images.push(extension === "pdf" ? `@${file}` : file);
|
|
9789
9915
|
else dropped++;
|
|
9790
9916
|
} else if (block?.type === "tool_result") {
|
|
9791
9917
|
const v = block.content;
|
|
@@ -9808,7 +9934,7 @@ ${text2}`
|
|
|
9808
9934
|
}
|
|
9809
9935
|
function stageImage(data, extension) {
|
|
9810
9936
|
try {
|
|
9811
|
-
const file = join10(pluginTmpDir(), `
|
|
9937
|
+
const file = join10(pluginTmpDir(), `attachment-${randomUUID5()}.${extension}`);
|
|
9812
9938
|
writeFileSync5(file, data, { mode: 384, flag: "wx" });
|
|
9813
9939
|
return file;
|
|
9814
9940
|
} catch (err) {
|
|
@@ -11317,6 +11443,7 @@ function createTurnState(init) {
|
|
|
11317
11443
|
accountLimitHit: null,
|
|
11318
11444
|
apiErrorTextShown: null,
|
|
11319
11445
|
usageLimitNote: false,
|
|
11446
|
+
compactionFailureText: null,
|
|
11320
11447
|
accountBlock: null,
|
|
11321
11448
|
modelRefusal: null,
|
|
11322
11449
|
drainBuffer: [],
|
|
@@ -12411,6 +12538,7 @@ ${plan}
|
|
|
12411
12538
|
resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
12412
12539
|
}));
|
|
12413
12540
|
const limitErrorReply = limitFromReply && ctx.usageLimitNoteActive;
|
|
12541
|
+
const compactionApiError = ctx.compactionMode && (apiErrorReply || typeof msg.error === "string");
|
|
12414
12542
|
if (limitFromReply) {
|
|
12415
12543
|
state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
|
|
12416
12544
|
const said = limitFactsFromText(
|
|
@@ -12432,6 +12560,10 @@ ${plan}
|
|
|
12432
12560
|
for (const block2 of msg.message.content) {
|
|
12433
12561
|
if (block2.type === "text" && block2.text) {
|
|
12434
12562
|
if (limitErrorReply) continue;
|
|
12563
|
+
if (compactionApiError) {
|
|
12564
|
+
state.compactionFailureText ??= block2.text;
|
|
12565
|
+
continue;
|
|
12566
|
+
}
|
|
12435
12567
|
if (apiErrorReply) state.apiErrorTextShown = block2.text;
|
|
12436
12568
|
resetLastVisibleTextBlock(state);
|
|
12437
12569
|
const blockId = state.startTextBlock();
|
|
@@ -12657,7 +12789,14 @@ ${plan}
|
|
|
12657
12789
|
state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
|
|
12658
12790
|
// and says what to run; two notes about one failure is one too many.
|
|
12659
12791
|
!state.accountBlock && msg.is_error === true;
|
|
12660
|
-
|
|
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.
|
|
12661
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) {
|
|
12662
12801
|
const errId = state.startTextBlock();
|
|
12663
12802
|
state.controller.enqueue({
|
|
@@ -12672,21 +12811,23 @@ ${plan}
|
|
|
12672
12811
|
if (state.accountBlock && msg.is_error) {
|
|
12673
12812
|
state.resultFailure ??= state.accountBlock;
|
|
12674
12813
|
const offeringSwitch = ctx.failoverAskActive;
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
12688
|
-
|
|
12689
|
-
|
|
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
|
+
}
|
|
12690
12831
|
log.warn(`Claude account "${ctx.sourceAccount}" cannot serve requests`, {
|
|
12691
12832
|
sessionKey: state.sessionKey,
|
|
12692
12833
|
kind: state.accountBlock,
|
|
@@ -12696,11 +12837,14 @@ ${plan}
|
|
|
12696
12837
|
const failure = describeResultFailure(msg);
|
|
12697
12838
|
if (failure) {
|
|
12698
12839
|
state.resultFailure = msg.subtype;
|
|
12699
|
-
|
|
12700
|
-
|
|
12701
|
-
|
|
12702
|
-
|
|
12703
|
-
|
|
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;
|
|
12704
12848
|
log.warn(failure, { sessionKey: state.sessionKey, subtype: msg.subtype });
|
|
12705
12849
|
}
|
|
12706
12850
|
const turnStats = extractTurnStats(msg);
|
|
@@ -13510,13 +13654,18 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13510
13654
|
}
|
|
13511
13655
|
}
|
|
13512
13656
|
}
|
|
13657
|
+
let replayReason = "no-session-for-key";
|
|
13513
13658
|
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
13514
13659
|
const configDir = resolveConfigDir(this.config.configDir);
|
|
13660
|
+
const transcriptPath = (id) => join12(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`);
|
|
13515
13661
|
const resumePoint = findResumePoint({
|
|
13516
13662
|
sessionKey: sk,
|
|
13517
13663
|
prompt: options.prompt,
|
|
13518
13664
|
cliPath,
|
|
13519
|
-
transcriptPath
|
|
13665
|
+
transcriptPath,
|
|
13666
|
+
onRefused: (reason) => {
|
|
13667
|
+
replayReason = reason;
|
|
13668
|
+
}
|
|
13520
13669
|
});
|
|
13521
13670
|
if (resumePoint) {
|
|
13522
13671
|
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
@@ -13525,8 +13674,47 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13525
13674
|
sessionKey: sk,
|
|
13526
13675
|
matchedMessages: resumePoint.matched
|
|
13527
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
|
+
}
|
|
13528
13709
|
}
|
|
13529
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
|
+
}
|
|
13530
13718
|
if (this.config.forkSessions === true && !compactionMode) {
|
|
13531
13719
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
13532
13720
|
}
|
|
@@ -14279,6 +14467,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14279
14467
|
})
|
|
14280
14468
|
});
|
|
14281
14469
|
state.endTextBlock();
|
|
14470
|
+
state.resultFailure ??= "usage_limit";
|
|
14282
14471
|
}
|
|
14283
14472
|
if (runAutoContinue(state, msg)) return;
|
|
14284
14473
|
for (const [idx, reasoningId] of state.reasoningIds) {
|
|
@@ -14312,6 +14501,18 @@ var ClaudeCodeLanguageModel = class {
|
|
|
14312
14501
|
});
|
|
14313
14502
|
state.endTextBlock();
|
|
14314
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
|
+
}
|
|
14315
14516
|
const usage = toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage));
|
|
14316
14517
|
controller.enqueue({
|
|
14317
14518
|
type: "finish",
|
|
@@ -15510,6 +15711,7 @@ function createClaudeCode(settings = {}) {
|
|
|
15510
15711
|
turnStats: settings.turnStats === true,
|
|
15511
15712
|
forkSessions: settings.forkSessions === true,
|
|
15512
15713
|
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
15714
|
+
resumeAcrossModelChanges: settings.resumeAcrossModelChanges !== false,
|
|
15513
15715
|
transport: settings.transport,
|
|
15514
15716
|
interactive: settings.interactive,
|
|
15515
15717
|
interactiveBypass: settings.interactiveBypass,
|