@skaile/workspaces 0.9.0 → 0.9.1
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 +47 -0
- package/dist/base-assets/connectors/flow/run-flow.js +1 -1
- package/dist/bridge/drivers/claude-sdk.js +213 -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.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-5VNUL5KL.js → chunk-AE6GCXGL.js} +2 -2
- package/dist/{chunk-5VNUL5KL.js.map → chunk-AE6GCXGL.js.map} +1 -1
- package/dist/{chunk-NMREHIHP.js → chunk-DTL7S57T.js} +2 -2
- package/dist/{chunk-NMREHIHP.js.map → chunk-DTL7S57T.js.map} +1 -1
- package/dist/{chunk-KMIWXGQ7.js → chunk-K3TMZI6D.js} +3 -3
- package/dist/{chunk-KMIWXGQ7.js.map → chunk-K3TMZI6D.js.map} +1 -1
- package/dist/{chunk-O32AN5P2.js → chunk-QZ6PY73K.js} +13 -7
- package/dist/chunk-QZ6PY73K.js.map +1 -0
- package/dist/{chunk-44ZICIN4.js → chunk-TODD4VNR.js} +9 -3
- package/dist/chunk-TODD4VNR.js.map +1 -0
- package/dist/cli/index.js +5 -5
- package/dist/runner/index.js +3 -3
- package/dist/runner/src/resource-handler.d.ts.map +1 -1
- package/dist/sdk/bridge.js +2 -2
- package/dist/sdk/index.js +3 -3
- package/dist/sdk/runner.js +3 -3
- package/dist/sdk/session.js +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/src/dispatcher.d.ts +4 -1
- package/dist/session/src/dispatcher.d.ts.map +1 -1
- package/dist/{setup-IZG3QE43.js → setup-PHFPBDBI.js} +4 -4
- package/dist/{setup-IZG3QE43.js.map → setup-PHFPBDBI.js.map} +1 -1
- package/dist/tui/index.js +3 -3
- package/dist/types/src/events.d.ts +6 -1
- package/dist/types/src/events.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-44ZICIN4.js.map +0 -1
- package/dist/chunk-O32AN5P2.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 4e7f03a: **Fix B-30: private `@mention_` messages no longer reach the agent.**
|
|
8
|
+
`SessionDispatcher.sendCommand` now suppresses the agent forward when
|
|
9
|
+
`visibility.visibilityMode` is `Private` and `privateRecipientIds` does not
|
|
10
|
+
include the `"__agent__"` sentinel. Private human-to-human messages are still
|
|
11
|
+
persisted and broadcast to their named human recipients, but the LLM never
|
|
12
|
+
sees their content. Private messages that explicitly address the agent
|
|
13
|
+
(`@agent_`, encoded as the `"__agent__"` sentinel) continue to forward
|
|
14
|
+
normally, as do `Public` and visibility-less messages. This closes the same
|
|
15
|
+
privacy hole that the existing `HumansOnly` gate already covered, for the
|
|
16
|
+
`Private` recipient-scoped case.
|
|
17
|
+
- 43895e8: **Self-heal poisoned Claude SDK transcripts.** The `claude-sdk` driver now
|
|
18
|
+
recovers from a conversation history that the Anthropic Messages API
|
|
19
|
+
permanently rejects because a content block is malformed. Previously such a
|
|
20
|
+
transcript bricked the session: every replayed turn failed with the same
|
|
21
|
+
`400 invalid_request_error` and there was no in-band recovery.
|
|
22
|
+
|
|
23
|
+
When `ClaudeSdkDriver.prompt()` catches that error it calls
|
|
24
|
+
`scrubPoisonedTranscript()` to repair the on-disk SDK JSONL transcript, then
|
|
25
|
+
retries the resume once, keeping the same session so conversation context is
|
|
26
|
+
preserved. Four poison classes are repaired in a single pass:
|
|
27
|
+
- **Image `media_type` mismatch** — corrected by sniffing the base64 magic
|
|
28
|
+
bytes (Claude Code `Read`-tool bug, anthropics/claude-code#55338 / #30124).
|
|
29
|
+
- **Missing image `media_type`** — filled in from the sniffed bytes (#33179).
|
|
30
|
+
- **Oversized images** — an image whose decoded payload exceeds the API's 5 MB
|
|
31
|
+
per-image limit is replaced with a text stub (#34566).
|
|
32
|
+
- **`cache_control` on empty text blocks** — the rejected marker is stripped
|
|
33
|
+
(#59626).
|
|
34
|
+
|
|
35
|
+
Genuinely unidentifiable image blocks are also replaced with a text stub. Adds
|
|
36
|
+
the `scrubPoisonedTranscript` and `sniffImageMediaType` exports, the
|
|
37
|
+
`ScrubTranscriptResult` type (with `corrected` / `stubbed` / `cacheStripped`
|
|
38
|
+
counters), and a `jsonl_poisoned` reason on the `resume_failed` event.
|
|
39
|
+
|
|
40
|
+
- 0f140a1: **Binary-aware filesystem-mount write.** `handleMountResourceRequest`'s `write`
|
|
41
|
+
operation now decodes `content.data` as base64 when `content.encoding` is
|
|
42
|
+
`"binary"`, and creates missing parent directories (`mkdir -p`) before writing.
|
|
43
|
+
Previously it wrote `content.data` verbatim as utf-8, so a base64-encoded
|
|
44
|
+
payload landed on disk as its literal base64 string, and writing into a
|
|
45
|
+
not-yet-existing subdirectory failed. This brings the mount `write` op to
|
|
46
|
+
parity with the already-binary-capable `read` op, and is the runner-side write
|
|
47
|
+
path the platform's `workspace-upload` (drag & drop file upload) route
|
|
48
|
+
dispatches into.
|
|
49
|
+
|
|
3
50
|
## 0.9.0
|
|
4
51
|
|
|
5
52
|
### 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-AE6GCXGL.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,54 @@ 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 configDir = this.config.env?.CLAUDE_CONFIG_DIR || process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
371
|
+
const scrub = scrubPoisonedTranscript({
|
|
372
|
+
configDir,
|
|
373
|
+
sessionId: poisonSessionId,
|
|
374
|
+
log: this.log
|
|
375
|
+
});
|
|
376
|
+
if (scrub.changed) {
|
|
377
|
+
this.log.warn("scrubbed poisoned Claude Code transcript, retrying resume", {
|
|
378
|
+
sessionId: poisonSessionId,
|
|
379
|
+
corrected: scrub.corrected,
|
|
380
|
+
stubbed: scrub.stubbed,
|
|
381
|
+
cacheStripped: scrub.cacheStripped
|
|
382
|
+
});
|
|
383
|
+
this.emit("agent-event", {
|
|
384
|
+
type: "resume_failed",
|
|
385
|
+
resumeSessionId: poisonSessionId,
|
|
386
|
+
reason: "jsonl_poisoned"
|
|
387
|
+
});
|
|
388
|
+
const plural = (n) => n === 1 ? "" : "s";
|
|
389
|
+
const repairs = [];
|
|
390
|
+
if (scrub.corrected > 0)
|
|
391
|
+
repairs.push(`${scrub.corrected} media type${plural(scrub.corrected)} corrected`);
|
|
392
|
+
if (scrub.stubbed > 0)
|
|
393
|
+
repairs.push(`${scrub.stubbed} image${plural(scrub.stubbed)} removed`);
|
|
394
|
+
if (scrub.cacheStripped > 0)
|
|
395
|
+
repairs.push(
|
|
396
|
+
`${scrub.cacheStripped} malformed block${plural(scrub.cacheStripped)} cleaned`
|
|
397
|
+
);
|
|
398
|
+
this.emit("agent-event", {
|
|
399
|
+
type: "error",
|
|
400
|
+
error: `Recovered corrupt data in the conversation history (${repairs.join(", ")}). Retrying \u2014 earlier context is preserved.`,
|
|
401
|
+
fatal: false
|
|
402
|
+
});
|
|
403
|
+
this.query = null;
|
|
404
|
+
this.turnResolve = null;
|
|
405
|
+
this.turnReject = null;
|
|
406
|
+
this.running = false;
|
|
407
|
+
retrying = true;
|
|
408
|
+
return this.prompt(message, _retryCount + 1);
|
|
409
|
+
}
|
|
410
|
+
this.log.warn("poisoned-transcript error but scrub found nothing to repair", {
|
|
411
|
+
sessionId: poisonSessionId,
|
|
412
|
+
filePath: scrub.filePath
|
|
413
|
+
});
|
|
414
|
+
}
|
|
204
415
|
const isAuthError = err instanceof AuthError && _retryCount === 0;
|
|
205
416
|
if (isAuthError && this.config.onAuthError) {
|
|
206
417
|
this.log.info("auth error caught; invoking onAuthError refresh callback");
|