@skaile/workspaces 0.9.0 → 0.10.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/CHANGELOG.md +69 -0
- package/dist/base-assets/connectors/flow/run-flow.js +1 -1
- package/dist/bridge/drivers/claude-sdk.js +263 -2
- package/dist/bridge/drivers/claude-sdk.js.map +1 -1
- package/dist/bridge/drivers/codex.js +1 -1
- package/dist/bridge/drivers/echo.js +1 -1
- package/dist/bridge/drivers/omp.js +1 -1
- package/dist/bridge/index.js +2 -2
- package/dist/bridge/src/drivers/claude-sdk.d.ts +23 -0
- package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
- package/dist/bridge/src/drivers/scrub-transcript.d.ts +113 -0
- package/dist/bridge/src/drivers/scrub-transcript.d.ts.map +1 -0
- package/dist/bridge/src/types.d.ts +2 -2
- package/dist/bridge/src/types.d.ts.map +1 -1
- package/dist/{chunk-O32AN5P2.js → chunk-BYZI6FMB.js} +26 -25
- package/dist/chunk-BYZI6FMB.js.map +1 -0
- package/dist/{chunk-44ZICIN4.js → chunk-D3VO6WNC.js} +92 -4
- package/dist/chunk-D3VO6WNC.js.map +1 -0
- package/dist/{chunk-NMREHIHP.js → chunk-NPNRWHCU.js} +2 -2
- package/dist/{chunk-NMREHIHP.js.map → chunk-NPNRWHCU.js.map} +1 -1
- package/dist/{chunk-KMIWXGQ7.js → chunk-OSJH4SPO.js} +3 -3
- package/dist/{chunk-KMIWXGQ7.js.map → chunk-OSJH4SPO.js.map} +1 -1
- package/dist/{chunk-5VNUL5KL.js → chunk-S7RACIZI.js} +2 -2
- package/dist/{chunk-5VNUL5KL.js.map → chunk-S7RACIZI.js.map} +1 -1
- package/dist/{chunk-O5AE4QDX.js → chunk-TDSRLMDB.js} +4 -4
- package/dist/chunk-TDSRLMDB.js.map +1 -0
- package/dist/chunk-W3UDISS2.js +31 -0
- package/dist/chunk-W3UDISS2.js.map +1 -0
- package/dist/{chunk-5IC6CJL4.js → chunk-YWQ3NGCS.js} +2 -2
- package/dist/{chunk-5IC6CJL4.js.map → chunk-YWQ3NGCS.js.map} +1 -1
- package/dist/cli/index.js +8 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/runner/index.js +6 -5
- package/dist/runner/prompt-assembly.js +4 -0
- package/dist/runner/prompt-assembly.js.map +1 -0
- package/dist/runner/src/capability-registry.d.ts.map +1 -1
- package/dist/runner/src/capability-roundtrip.d.ts +18 -0
- package/dist/runner/src/capability-roundtrip.d.ts.map +1 -1
- package/dist/runner/src/define-capability.d.ts +7 -0
- package/dist/runner/src/define-capability.d.ts.map +1 -1
- package/dist/runner/src/prompt-assembly.d.ts +39 -0
- package/dist/runner/src/prompt-assembly.d.ts.map +1 -1
- package/dist/runner/src/resource-handler.d.ts.map +1 -1
- package/dist/runner/src/serve.d.ts.map +1 -1
- package/dist/sdk/bridge.js +2 -2
- package/dist/sdk/index.js +6 -5
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/runner.js +6 -5
- package/dist/sdk/session.js +2 -2
- package/dist/sdk/types.js +1 -1
- package/dist/session/index.js +2 -2
- package/dist/session/src/dispatcher.d.ts +61 -1
- package/dist/session/src/dispatcher.d.ts.map +1 -1
- package/dist/{setup-IZG3QE43.js → setup-QIEPIYH2.js} +4 -4
- package/dist/{setup-IZG3QE43.js.map → setup-QIEPIYH2.js.map} +1 -1
- package/dist/tui/index.js +6 -5
- package/dist/tui/index.js.map +1 -1
- package/dist/types/index.js +1 -1
- package/dist/types/src/capabilities.d.ts +13 -0
- package/dist/types/src/capabilities.d.ts.map +1 -1
- package/dist/types/src/events.d.ts +35 -2
- package/dist/types/src/events.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +19 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/dist/workspace-plugin/adapters/mcp.js +2 -2
- package/dist/workspace-plugin/index.js +1 -1
- package/package.json +7 -1
- package/dist/chunk-44ZICIN4.js.map +0 -1
- package/dist/chunk-O32AN5P2.js.map +0 -1
- package/dist/chunk-O5AE4QDX.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- eb14ab2: Add runtime support for agent-to-agent (session-to-session) communication:
|
|
8
|
+
- New `a2a_message` event kind in the agent event union.
|
|
9
|
+
- `SessionDispatcher.onceNextFinished` — a one-shot hook that resolves on the
|
|
10
|
+
session's next finished assistant turn, used to capture a peer's answer to a
|
|
11
|
+
synchronous `ask`.
|
|
12
|
+
- `SessionDispatcher.deliverPrompt` — delivers a turn-triggering prompt without
|
|
13
|
+
persisting it to the message store (A2A message persistence is owned by the
|
|
14
|
+
caller).
|
|
15
|
+
- Per-capability `callTimeoutMs` on the capability definition + wire format, so
|
|
16
|
+
a long-running capability call (e.g. a 5-minute `ask_session`) can exceed the
|
|
17
|
+
default capability-call timeout.
|
|
18
|
+
- `buildLinkedPeersPromptSection` — renders a `<LINKED_PEERS>` system-prompt
|
|
19
|
+
block from a session's linked peers, exported via the new
|
|
20
|
+
`@skaile/workspaces/runner/prompt-assembly` subpath.
|
|
21
|
+
- `PROTOCOL_VERSION` bumped to `3.3.0` for the additive A2A wire surface
|
|
22
|
+
(`a2a_message` event + capability `callTimeoutMs`); also backfills the
|
|
23
|
+
`3.2.0` changelog entry for the previously-undocumented resume cascade.
|
|
24
|
+
|
|
25
|
+
## 0.9.1
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- 4e7f03a: **Fix B-30: private `@mention_` messages no longer reach the agent.**
|
|
30
|
+
`SessionDispatcher.sendCommand` now suppresses the agent forward when
|
|
31
|
+
`visibility.visibilityMode` is `Private` and `privateRecipientIds` does not
|
|
32
|
+
include the `"__agent__"` sentinel. Private human-to-human messages are still
|
|
33
|
+
persisted and broadcast to their named human recipients, but the LLM never
|
|
34
|
+
sees their content. Private messages that explicitly address the agent
|
|
35
|
+
(`@agent_`, encoded as the `"__agent__"` sentinel) continue to forward
|
|
36
|
+
normally, as do `Public` and visibility-less messages. This closes the same
|
|
37
|
+
privacy hole that the existing `HumansOnly` gate already covered, for the
|
|
38
|
+
`Private` recipient-scoped case.
|
|
39
|
+
- 43895e8: **Self-heal poisoned Claude SDK transcripts.** The `claude-sdk` driver now
|
|
40
|
+
recovers from a conversation history that the Anthropic Messages API
|
|
41
|
+
permanently rejects because a content block is malformed. Previously such a
|
|
42
|
+
transcript bricked the session: every replayed turn failed with the same
|
|
43
|
+
`400 invalid_request_error` and there was no in-band recovery.
|
|
44
|
+
|
|
45
|
+
When `ClaudeSdkDriver.prompt()` catches that error it calls
|
|
46
|
+
`scrubPoisonedTranscript()` to repair the on-disk SDK JSONL transcript, then
|
|
47
|
+
retries the resume once, keeping the same session so conversation context is
|
|
48
|
+
preserved. Four poison classes are repaired in a single pass:
|
|
49
|
+
- **Image `media_type` mismatch** — corrected by sniffing the base64 magic
|
|
50
|
+
bytes (Claude Code `Read`-tool bug, anthropics/claude-code#55338 / #30124).
|
|
51
|
+
- **Missing image `media_type`** — filled in from the sniffed bytes (#33179).
|
|
52
|
+
- **Oversized images** — an image whose decoded payload exceeds the API's 5 MB
|
|
53
|
+
per-image limit is replaced with a text stub (#34566).
|
|
54
|
+
- **`cache_control` on empty text blocks** — the rejected marker is stripped
|
|
55
|
+
(#59626).
|
|
56
|
+
|
|
57
|
+
Genuinely unidentifiable image blocks are also replaced with a text stub. Adds
|
|
58
|
+
the `scrubPoisonedTranscript` and `sniffImageMediaType` exports, the
|
|
59
|
+
`ScrubTranscriptResult` type (with `corrected` / `stubbed` / `cacheStripped`
|
|
60
|
+
counters), and a `jsonl_poisoned` reason on the `resume_failed` event.
|
|
61
|
+
|
|
62
|
+
- 0f140a1: **Binary-aware filesystem-mount write.** `handleMountResourceRequest`'s `write`
|
|
63
|
+
operation now decodes `content.data` as base64 when `content.encoding` is
|
|
64
|
+
`"binary"`, and creates missing parent directories (`mkdir -p`) before writing.
|
|
65
|
+
Previously it wrote `content.data` verbatim as utf-8, so a base64-encoded
|
|
66
|
+
payload landed on disk as its literal base64 string, and writing into a
|
|
67
|
+
not-yet-existing subdirectory failed. This brings the mount `write` op to
|
|
68
|
+
parity with the already-binary-capable `read` op, and is the runner-side write
|
|
69
|
+
path the platform's `workspace-upload` (drag & drop file upload) route
|
|
70
|
+
dispatches into.
|
|
71
|
+
|
|
3
72
|
## 0.9.0
|
|
4
73
|
|
|
5
74
|
### Minor Changes
|
|
@@ -1,14 +1,177 @@
|
|
|
1
1
|
import { classifyClaudeSdkError, AuthError } from '../../chunk-EWP5HZBV.js';
|
|
2
2
|
import { fetchProviderModels } from '../../chunk-KOVLSBXK.js';
|
|
3
3
|
import { dispatchCapability } from '../../chunk-RRVQAE5D.js';
|
|
4
|
-
import { registerDriver, DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-
|
|
4
|
+
import { registerDriver, DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-S7RACIZI.js';
|
|
5
5
|
import '../../chunk-24UIWON4.js';
|
|
6
6
|
import '../../chunk-NSBPE2FW.js';
|
|
7
7
|
import { spawnSync } from 'child_process';
|
|
8
|
-
import { existsSync } from 'fs';
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, renameSync } from 'fs';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import { join } from 'path';
|
|
10
12
|
import * as zNS from 'zod';
|
|
11
13
|
|
|
14
|
+
var STUB_TEXT = "[image removed: unprocessable image data]";
|
|
15
|
+
var STUB_TEXT_OVERSIZED = "[image removed: image too large]";
|
|
16
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
17
|
+
function sniffImageMediaType(base64) {
|
|
18
|
+
let buf;
|
|
19
|
+
try {
|
|
20
|
+
buf = Buffer.from(base64.slice(0, 32), "base64");
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (buf.length < 4) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (buf[0] === 255 && buf[1] === 216) {
|
|
28
|
+
return "image/jpeg";
|
|
29
|
+
}
|
|
30
|
+
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) {
|
|
31
|
+
return "image/png";
|
|
32
|
+
}
|
|
33
|
+
if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70 && buf[3] === 56) {
|
|
34
|
+
return "image/gif";
|
|
35
|
+
}
|
|
36
|
+
if (buf.length >= 12 && buf[0] === 82 && buf[1] === 73 && buf[2] === 70 && buf[3] === 70 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) {
|
|
37
|
+
return "image/webp";
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function totalChanges(c) {
|
|
42
|
+
return c.corrected + c.stubbed + c.cacheStripped;
|
|
43
|
+
}
|
|
44
|
+
function scrubBlock(block, counters) {
|
|
45
|
+
if (block.type === "tool_result" && Array.isArray(block.content)) {
|
|
46
|
+
return {
|
|
47
|
+
...block,
|
|
48
|
+
content: block.content.map((inner) => scrubBlock(inner, counters))
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (block.type === "text" && block.cache_control != null) {
|
|
52
|
+
const text = typeof block.text === "string" ? block.text : "";
|
|
53
|
+
if (text.trim() === "") {
|
|
54
|
+
counters.cacheStripped++;
|
|
55
|
+
const clone = { ...block };
|
|
56
|
+
delete clone.cache_control;
|
|
57
|
+
return clone;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (block.type === "image" && block.source?.type === "base64" && typeof block.source.data === "string") {
|
|
61
|
+
const data = block.source.data;
|
|
62
|
+
if (Math.floor(data.length * 3 / 4) > MAX_IMAGE_BYTES) {
|
|
63
|
+
counters.stubbed++;
|
|
64
|
+
return { type: "text", text: STUB_TEXT_OVERSIZED };
|
|
65
|
+
}
|
|
66
|
+
const sniffed = sniffImageMediaType(data);
|
|
67
|
+
if (sniffed === null) {
|
|
68
|
+
counters.stubbed++;
|
|
69
|
+
return { type: "text", text: STUB_TEXT };
|
|
70
|
+
}
|
|
71
|
+
if (sniffed !== block.source.media_type) {
|
|
72
|
+
counters.corrected++;
|
|
73
|
+
return { ...block, source: { ...block.source, media_type: sniffed } };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return block;
|
|
77
|
+
}
|
|
78
|
+
function locateTranscript(configDir, sessionId) {
|
|
79
|
+
const projectsDir = join(configDir, "projects");
|
|
80
|
+
if (!existsSync(projectsDir)) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
let entries;
|
|
84
|
+
try {
|
|
85
|
+
entries = readdirSync(projectsDir);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const candidate = join(projectsDir, entry, `${sessionId}.jsonl`);
|
|
91
|
+
if (existsSync(candidate)) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function scrubPoisonedTranscript(opts) {
|
|
98
|
+
const { configDir, sessionId, log } = opts;
|
|
99
|
+
const result = {
|
|
100
|
+
filePath: null,
|
|
101
|
+
corrected: 0,
|
|
102
|
+
stubbed: 0,
|
|
103
|
+
cacheStripped: 0,
|
|
104
|
+
changed: false
|
|
105
|
+
};
|
|
106
|
+
const filePath = locateTranscript(configDir, sessionId);
|
|
107
|
+
if (!filePath) {
|
|
108
|
+
log?.warn("scrub-transcript: no transcript found", { configDir, sessionId });
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
result.filePath = filePath;
|
|
112
|
+
let raw;
|
|
113
|
+
try {
|
|
114
|
+
raw = readFileSync(filePath, "utf8");
|
|
115
|
+
} catch (err) {
|
|
116
|
+
log?.warn("scrub-transcript: failed to read transcript", {
|
|
117
|
+
filePath,
|
|
118
|
+
error: err instanceof Error ? err.message : String(err)
|
|
119
|
+
});
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
const counters = { corrected: 0, stubbed: 0, cacheStripped: 0 };
|
|
123
|
+
const lines = raw.split("\n");
|
|
124
|
+
let dirty = false;
|
|
125
|
+
const repaired = lines.map((line) => {
|
|
126
|
+
if (line.trim() === "") {
|
|
127
|
+
return line;
|
|
128
|
+
}
|
|
129
|
+
let entry;
|
|
130
|
+
try {
|
|
131
|
+
entry = JSON.parse(line);
|
|
132
|
+
} catch {
|
|
133
|
+
return line;
|
|
134
|
+
}
|
|
135
|
+
const content = entry.message?.content;
|
|
136
|
+
if (!Array.isArray(content)) {
|
|
137
|
+
return line;
|
|
138
|
+
}
|
|
139
|
+
const before = totalChanges(counters);
|
|
140
|
+
const scrubbed = content.map((block) => scrubBlock(block, counters));
|
|
141
|
+
if (totalChanges(counters) === before) {
|
|
142
|
+
return line;
|
|
143
|
+
}
|
|
144
|
+
dirty = true;
|
|
145
|
+
return JSON.stringify({ ...entry, message: { ...entry.message, content: scrubbed } });
|
|
146
|
+
});
|
|
147
|
+
result.corrected = counters.corrected;
|
|
148
|
+
result.stubbed = counters.stubbed;
|
|
149
|
+
result.cacheStripped = counters.cacheStripped;
|
|
150
|
+
if (!dirty) {
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
const tmpPath = `${filePath}.scrub-tmp`;
|
|
154
|
+
try {
|
|
155
|
+
writeFileSync(tmpPath, repaired.join("\n"), "utf8");
|
|
156
|
+
renameSync(tmpPath, filePath);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
log?.warn("scrub-transcript: failed to rewrite transcript", {
|
|
159
|
+
filePath,
|
|
160
|
+
error: err instanceof Error ? err.message : String(err)
|
|
161
|
+
});
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
result.changed = true;
|
|
165
|
+
log?.info("scrub-transcript: repaired poisoned transcript", {
|
|
166
|
+
filePath,
|
|
167
|
+
corrected: result.corrected,
|
|
168
|
+
stubbed: result.stubbed,
|
|
169
|
+
cacheStripped: result.cacheStripped
|
|
170
|
+
});
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// bridge/src/drivers/claude-sdk.ts
|
|
12
175
|
var zStatic = zNS.z ?? zNS.default ?? zNS;
|
|
13
176
|
function jsonSchemaToZodLoose(schema, z2) {
|
|
14
177
|
const props = schema?.properties ?? null;
|
|
@@ -201,6 +364,53 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
201
364
|
retrying = true;
|
|
202
365
|
return this.prompt(message, _retryCount + 1);
|
|
203
366
|
}
|
|
367
|
+
const isPoisonedHistory = _retryCount === 0 && /invalid_request_error/i.test(errMsg) && /media[_ ]?type|could not process image|image exceeds|cache_control/i.test(errMsg);
|
|
368
|
+
const poisonSessionId = this.config.resumeSessionId || this.sessionId;
|
|
369
|
+
if (isPoisonedHistory && poisonSessionId) {
|
|
370
|
+
const scrub = scrubPoisonedTranscript({
|
|
371
|
+
configDir: this.resolveClaudeConfigDir(),
|
|
372
|
+
sessionId: poisonSessionId,
|
|
373
|
+
log: this.log
|
|
374
|
+
});
|
|
375
|
+
if (scrub.changed) {
|
|
376
|
+
this.log.warn("scrubbed poisoned Claude Code transcript, retrying resume", {
|
|
377
|
+
sessionId: poisonSessionId,
|
|
378
|
+
corrected: scrub.corrected,
|
|
379
|
+
stubbed: scrub.stubbed,
|
|
380
|
+
cacheStripped: scrub.cacheStripped
|
|
381
|
+
});
|
|
382
|
+
this.emit("agent-event", {
|
|
383
|
+
type: "resume_failed",
|
|
384
|
+
resumeSessionId: poisonSessionId,
|
|
385
|
+
reason: "jsonl_poisoned"
|
|
386
|
+
});
|
|
387
|
+
const plural = (n) => n === 1 ? "" : "s";
|
|
388
|
+
const repairs = [];
|
|
389
|
+
if (scrub.corrected > 0)
|
|
390
|
+
repairs.push(`${scrub.corrected} media type${plural(scrub.corrected)} corrected`);
|
|
391
|
+
if (scrub.stubbed > 0)
|
|
392
|
+
repairs.push(`${scrub.stubbed} image${plural(scrub.stubbed)} removed`);
|
|
393
|
+
if (scrub.cacheStripped > 0)
|
|
394
|
+
repairs.push(
|
|
395
|
+
`${scrub.cacheStripped} malformed block${plural(scrub.cacheStripped)} cleaned`
|
|
396
|
+
);
|
|
397
|
+
this.emit("agent-event", {
|
|
398
|
+
type: "error",
|
|
399
|
+
error: `Recovered corrupt data in the conversation history (${repairs.join(", ")}). Retrying \u2014 earlier context is preserved.`,
|
|
400
|
+
fatal: false
|
|
401
|
+
});
|
|
402
|
+
this.query = null;
|
|
403
|
+
this.turnResolve = null;
|
|
404
|
+
this.turnReject = null;
|
|
405
|
+
this.running = false;
|
|
406
|
+
retrying = true;
|
|
407
|
+
return this.prompt(message, _retryCount + 1);
|
|
408
|
+
}
|
|
409
|
+
this.log.warn("poisoned-transcript error but scrub found nothing to repair", {
|
|
410
|
+
sessionId: poisonSessionId,
|
|
411
|
+
filePath: scrub.filePath
|
|
412
|
+
});
|
|
413
|
+
}
|
|
204
414
|
const isAuthError = err instanceof AuthError && _retryCount === 0;
|
|
205
415
|
if (isAuthError && this.config.onAuthError) {
|
|
206
416
|
this.log.info("auth error caught; invoking onAuthError refresh callback");
|
|
@@ -239,8 +449,59 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
239
449
|
}
|
|
240
450
|
}
|
|
241
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Resolve the Claude Code config directory — the parent of `projects/` — from
|
|
454
|
+
* the driver config, the process environment, or the `~/.claude` default.
|
|
455
|
+
*/
|
|
456
|
+
resolveClaudeConfigDir() {
|
|
457
|
+
return this.config.env?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Preventively repair the on-disk SDK transcript before a resume.
|
|
461
|
+
*
|
|
462
|
+
* The reactive {@link scrubPoisonedTranscript} pass in `prompt()` only fires
|
|
463
|
+
* *after* a turn has already failed with a `400 invalid_request_error`,
|
|
464
|
+
* costing a wasted round-trip and surfacing a scary (if non-fatal) error to
|
|
465
|
+
* the user. A transcript poisoned in a prior driver lifetime — most commonly
|
|
466
|
+
* an image block whose `media_type` does not match its bytes, produced by the
|
|
467
|
+
* Claude Code `Read` tool on a PDF with embedded JPEGs (anthropics/claude-code
|
|
468
|
+
* #55338) — would otherwise 400 on the very first resumed turn.
|
|
469
|
+
*
|
|
470
|
+
* Running the same magic-byte scrub *before* handing the transcript to the
|
|
471
|
+
* SDK means a known poison class never reaches the API, so recovery is
|
|
472
|
+
* invisible. This is regex-free (unlike the reactive gate) and idempotent: a
|
|
473
|
+
* clean transcript is left byte-for-byte untouched. The reactive path remains
|
|
474
|
+
* the safety net for poison introduced mid-turn within the current lifetime.
|
|
475
|
+
*/
|
|
476
|
+
preventivelyScrubTranscript() {
|
|
477
|
+
const sessionId = this.config.resumeSessionId || this.sessionId;
|
|
478
|
+
if (!sessionId) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const scrub = scrubPoisonedTranscript({
|
|
483
|
+
configDir: this.resolveClaudeConfigDir(),
|
|
484
|
+
sessionId,
|
|
485
|
+
log: this.log
|
|
486
|
+
});
|
|
487
|
+
if (scrub.changed) {
|
|
488
|
+
this.log.warn("preventively scrubbed poisoned Claude Code transcript before resume", {
|
|
489
|
+
sessionId,
|
|
490
|
+
corrected: scrub.corrected,
|
|
491
|
+
stubbed: scrub.stubbed,
|
|
492
|
+
cacheStripped: scrub.cacheStripped
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
} catch (err) {
|
|
496
|
+
this.log.warn("preventive transcript scrub failed; continuing", {
|
|
497
|
+
sessionId,
|
|
498
|
+
error: err instanceof Error ? err.message : String(err)
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
242
502
|
async startQuery(message) {
|
|
243
503
|
this.abortController = new AbortController();
|
|
504
|
+
this.preventivelyScrubTranscript();
|
|
244
505
|
const apiKey = this.config.apiKeys?.anthropic || this.config.env?.ANTHROPIC_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
245
506
|
this.usingOauthCredential = !apiKey;
|
|
246
507
|
const claudePath = this.findClaudeBinary();
|