@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/base-assets/connectors/flow/run-flow.js +1 -1
  3. package/dist/bridge/drivers/claude-sdk.js +263 -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 +23 -0
  10. package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
  11. package/dist/bridge/src/drivers/scrub-transcript.d.ts +113 -0
  12. package/dist/bridge/src/drivers/scrub-transcript.d.ts.map +1 -0
  13. package/dist/bridge/src/types.d.ts +2 -2
  14. package/dist/bridge/src/types.d.ts.map +1 -1
  15. package/dist/{chunk-O32AN5P2.js → chunk-BYZI6FMB.js} +26 -25
  16. package/dist/chunk-BYZI6FMB.js.map +1 -0
  17. package/dist/{chunk-44ZICIN4.js → chunk-D3VO6WNC.js} +92 -4
  18. package/dist/chunk-D3VO6WNC.js.map +1 -0
  19. package/dist/{chunk-NMREHIHP.js → chunk-NPNRWHCU.js} +2 -2
  20. package/dist/{chunk-NMREHIHP.js.map → chunk-NPNRWHCU.js.map} +1 -1
  21. package/dist/{chunk-KMIWXGQ7.js → chunk-OSJH4SPO.js} +3 -3
  22. package/dist/{chunk-KMIWXGQ7.js.map → chunk-OSJH4SPO.js.map} +1 -1
  23. package/dist/{chunk-5VNUL5KL.js → chunk-S7RACIZI.js} +2 -2
  24. package/dist/{chunk-5VNUL5KL.js.map → chunk-S7RACIZI.js.map} +1 -1
  25. package/dist/{chunk-O5AE4QDX.js → chunk-TDSRLMDB.js} +4 -4
  26. package/dist/chunk-TDSRLMDB.js.map +1 -0
  27. package/dist/chunk-W3UDISS2.js +31 -0
  28. package/dist/chunk-W3UDISS2.js.map +1 -0
  29. package/dist/{chunk-5IC6CJL4.js → chunk-YWQ3NGCS.js} +2 -2
  30. package/dist/{chunk-5IC6CJL4.js.map → chunk-YWQ3NGCS.js.map} +1 -1
  31. package/dist/cli/index.js +8 -7
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/runner/index.js +6 -5
  34. package/dist/runner/prompt-assembly.js +4 -0
  35. package/dist/runner/prompt-assembly.js.map +1 -0
  36. package/dist/runner/src/capability-registry.d.ts.map +1 -1
  37. package/dist/runner/src/capability-roundtrip.d.ts +18 -0
  38. package/dist/runner/src/capability-roundtrip.d.ts.map +1 -1
  39. package/dist/runner/src/define-capability.d.ts +7 -0
  40. package/dist/runner/src/define-capability.d.ts.map +1 -1
  41. package/dist/runner/src/prompt-assembly.d.ts +39 -0
  42. package/dist/runner/src/prompt-assembly.d.ts.map +1 -1
  43. package/dist/runner/src/resource-handler.d.ts.map +1 -1
  44. package/dist/runner/src/serve.d.ts.map +1 -1
  45. package/dist/sdk/bridge.js +2 -2
  46. package/dist/sdk/index.js +6 -5
  47. package/dist/sdk/index.js.map +1 -1
  48. package/dist/sdk/runner.js +6 -5
  49. package/dist/sdk/session.js +2 -2
  50. package/dist/sdk/types.js +1 -1
  51. package/dist/session/index.js +2 -2
  52. package/dist/session/src/dispatcher.d.ts +61 -1
  53. package/dist/session/src/dispatcher.d.ts.map +1 -1
  54. package/dist/{setup-IZG3QE43.js → setup-QIEPIYH2.js} +4 -4
  55. package/dist/{setup-IZG3QE43.js.map → setup-QIEPIYH2.js.map} +1 -1
  56. package/dist/tui/index.js +6 -5
  57. package/dist/tui/index.js.map +1 -1
  58. package/dist/types/index.js +1 -1
  59. package/dist/types/src/capabilities.d.ts +13 -0
  60. package/dist/types/src/capabilities.d.ts.map +1 -1
  61. package/dist/types/src/events.d.ts +35 -2
  62. package/dist/types/src/events.d.ts.map +1 -1
  63. package/dist/types/src/index.d.ts +1 -1
  64. package/dist/types/src/index.d.ts.map +1 -1
  65. package/dist/types/src/version.d.ts +19 -1
  66. package/dist/types/src/version.d.ts.map +1 -1
  67. package/dist/workspace-plugin/adapters/mcp.js +2 -2
  68. package/dist/workspace-plugin/index.js +1 -1
  69. package/package.json +7 -1
  70. package/dist/chunk-44ZICIN4.js.map +0 -1
  71. package/dist/chunk-O32AN5P2.js.map +0 -1
  72. 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,4 +1,4 @@
1
- export { resumeFlow, runFlow } from '../../../chunk-NMREHIHP.js';
1
+ export { resumeFlow, runFlow } from '../../../chunk-NPNRWHCU.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-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();