@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/base-assets/connectors/flow/run-flow.js +1 -1
  3. package/dist/bridge/drivers/claude-sdk.js +213 -2
  4. package/dist/bridge/drivers/claude-sdk.js.map +1 -1
  5. package/dist/bridge/drivers/codex.js +1 -1
  6. package/dist/bridge/drivers/echo.js +1 -1
  7. package/dist/bridge/drivers/omp.js +1 -1
  8. package/dist/bridge/index.js +2 -2
  9. package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
  10. package/dist/bridge/src/drivers/scrub-transcript.d.ts +113 -0
  11. package/dist/bridge/src/drivers/scrub-transcript.d.ts.map +1 -0
  12. package/dist/bridge/src/types.d.ts +2 -2
  13. package/dist/bridge/src/types.d.ts.map +1 -1
  14. package/dist/{chunk-5VNUL5KL.js → chunk-AE6GCXGL.js} +2 -2
  15. package/dist/{chunk-5VNUL5KL.js.map → chunk-AE6GCXGL.js.map} +1 -1
  16. package/dist/{chunk-NMREHIHP.js → chunk-DTL7S57T.js} +2 -2
  17. package/dist/{chunk-NMREHIHP.js.map → chunk-DTL7S57T.js.map} +1 -1
  18. package/dist/{chunk-KMIWXGQ7.js → chunk-K3TMZI6D.js} +3 -3
  19. package/dist/{chunk-KMIWXGQ7.js.map → chunk-K3TMZI6D.js.map} +1 -1
  20. package/dist/{chunk-O32AN5P2.js → chunk-QZ6PY73K.js} +13 -7
  21. package/dist/chunk-QZ6PY73K.js.map +1 -0
  22. package/dist/{chunk-44ZICIN4.js → chunk-TODD4VNR.js} +9 -3
  23. package/dist/chunk-TODD4VNR.js.map +1 -0
  24. package/dist/cli/index.js +5 -5
  25. package/dist/runner/index.js +3 -3
  26. package/dist/runner/src/resource-handler.d.ts.map +1 -1
  27. package/dist/sdk/bridge.js +2 -2
  28. package/dist/sdk/index.js +3 -3
  29. package/dist/sdk/runner.js +3 -3
  30. package/dist/sdk/session.js +1 -1
  31. package/dist/session/index.js +1 -1
  32. package/dist/session/src/dispatcher.d.ts +4 -1
  33. package/dist/session/src/dispatcher.d.ts.map +1 -1
  34. package/dist/{setup-IZG3QE43.js → setup-PHFPBDBI.js} +4 -4
  35. package/dist/{setup-IZG3QE43.js.map → setup-PHFPBDBI.js.map} +1 -1
  36. package/dist/tui/index.js +3 -3
  37. package/dist/types/src/events.d.ts +6 -1
  38. package/dist/types/src/events.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/chunk-44ZICIN4.js.map +0 -1
  41. 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,4 +1,4 @@
1
- export { resumeFlow, runFlow } from '../../../chunk-NMREHIHP.js';
1
+ export { resumeFlow, runFlow } from '../../../chunk-DTL7S57T.js';
2
2
  import '../../../chunk-GCJXPUHG.js';
3
3
  import '../../../chunk-IPUYL6TD.js';
4
4
  import '../../../chunk-EPGHAOEU.js';
@@ -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-5VNUL5KL.js';
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");