@sanqianx/claude-ai-workbench 0.13.2 → 0.13.4

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 (122) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +652 -613
  4. package/apps/agent-terminal/app.js +143 -67
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +172 -170
  7. package/apps/agent-terminal/panel-host-proof.html +152 -152
  8. package/apps/agent-terminal/panel-proof-content.html +41 -41
  9. package/apps/agent-terminal/right-workspace-dock.css +189 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +652 -521
  11. package/apps/agent-terminal/user-question-card.js +68 -0
  12. package/apps/ai-profile-settings-host/index.html +25 -25
  13. package/apps/ai-profile-settings-host/page.css +1 -1
  14. package/apps/ai-profile-settings-host/page.js +113 -113
  15. package/apps/minimal-test-host/README.md +15 -15
  16. package/apps/minimal-test-host/host.css +2 -2
  17. package/apps/minimal-test-host/host.js +35 -35
  18. package/apps/minimal-test-host/index.html +58 -58
  19. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  20. package/docs/sdk/FILE_LAYOUT.md +31 -31
  21. package/docs/sdk/INTEGRATION.md +126 -126
  22. package/docs/sdk/README.md +21 -21
  23. package/docs/sdk/SESSION_EVENTS.md +15 -13
  24. package/docs/sdk/openapi.yaml +16 -1
  25. package/package.json +1 -1
  26. package/packages/client/README.md +29 -29
  27. package/packages/client/index.d.ts +122 -122
  28. package/packages/client/index.js +260 -260
  29. package/packages/client/package.json +2 -2
  30. package/packages/contracts/README.md +9 -9
  31. package/packages/contracts/index.d.ts +519 -517
  32. package/packages/contracts/index.js +312 -312
  33. package/packages/contracts/package.json +1 -1
  34. package/packages/core/README.md +8 -8
  35. package/packages/core/index.d.ts +76 -76
  36. package/packages/core/index.js +222 -222
  37. package/packages/core/package.json +2 -2
  38. package/packages/server/README.md +17 -17
  39. package/packages/server/bin/agent-terminal-server.js +42 -42
  40. package/packages/server/bin/claude-workbench.js +225 -225
  41. package/packages/server/index.d.ts +171 -171
  42. package/packages/server/index.js +230 -230
  43. package/packages/server/lib/agent-discovery.js +133 -133
  44. package/packages/server/lib/agent-registry.js +143 -143
  45. package/packages/server/lib/agent-spawn-env.js +17 -17
  46. package/packages/server/lib/attachment-store.js +78 -78
  47. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  48. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  49. package/packages/server/lib/capture-settings-store.js +35 -35
  50. package/packages/server/lib/claude-history-recovery.js +87 -87
  51. package/packages/server/lib/cli-session-recovery.js +28 -28
  52. package/packages/server/lib/context-resolver.js +55 -55
  53. package/packages/server/lib/conversation-capture.js +51 -51
  54. package/packages/server/lib/delegation-phase-store.js +357 -357
  55. package/packages/server/lib/delegation-store.js +474 -468
  56. package/packages/server/lib/diagnostics.js +32 -32
  57. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  58. package/packages/server/lib/drivers/codex-driver.js +1461 -1442
  59. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  60. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  61. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  62. package/packages/server/lib/event-stream.js +138 -138
  63. package/packages/server/lib/folder-picker.js +623 -623
  64. package/packages/server/lib/http-handler.js +339 -339
  65. package/packages/server/lib/json-file.js +26 -26
  66. package/packages/server/lib/knowledge-context.js +33 -33
  67. package/packages/server/lib/model-detection.js +295 -295
  68. package/packages/server/lib/parent-context.js +44 -0
  69. package/packages/server/lib/profile-store.js +283 -283
  70. package/packages/server/lib/project-store.js +102 -102
  71. package/packages/server/lib/runtime-loader.js +120 -120
  72. package/packages/server/lib/session-archive-store.js +53 -53
  73. package/packages/server/lib/session-history.js +612 -612
  74. package/packages/server/lib/system-doctor.js +125 -125
  75. package/packages/server/lib/ui-preferences-store.js +48 -48
  76. package/packages/server/lib/user-questions.js +67 -0
  77. package/packages/server/lib/workbench-service.js +1696 -1675
  78. package/packages/server/package.json +2 -2
  79. package/packages/ui/README.md +30 -30
  80. package/packages/ui/ai-profile-settings.d.ts +45 -45
  81. package/packages/ui/ai-profile-settings.js +283 -283
  82. package/packages/ui/claude-terminal.d.ts +31 -31
  83. package/packages/ui/claude-terminal.js +343 -343
  84. package/packages/ui/index.d.ts +18 -18
  85. package/packages/ui/index.js +51 -51
  86. package/packages/ui/package.json +2 -2
  87. package/packages/ui/theme.css +13 -13
  88. package/src/assets/claude-prompts.json +21 -21
  89. package/src/backend/http/ai-profile-routes.js +61 -61
  90. package/src/backend/http/claude-routes.js +170 -170
  91. package/src/backend/http/node-helpers.js +45 -45
  92. package/src/backend/index.js +21 -21
  93. package/src/backend/lib/ai-adapter.js +144 -144
  94. package/src/backend/lib/ai-profile-store.js +259 -259
  95. package/src/backend/lib/ai-vendor-presets.js +83 -83
  96. package/src/backend/lib/ai-workspace.js +140 -140
  97. package/src/backend/lib/automation-config.js +253 -253
  98. package/src/backend/lib/claude-cli-runner.js +2408 -2392
  99. package/src/backend/lib/data-dir.js +169 -169
  100. package/src/backend/lib/llm-client.js +176 -176
  101. package/src/backend/lib/model-context-windows.js +82 -82
  102. package/src/backend/lib/native-task-control.js +217 -217
  103. package/src/backend/lib/native-task-history-index.js +475 -475
  104. package/src/backend/lib/native-task-history-reader.js +526 -526
  105. package/src/backend/lib/native-task-messages.js +457 -457
  106. package/src/backend/lib/native-task-registry.js +325 -325
  107. package/src/backend/lib/native-task-runtime.js +242 -242
  108. package/src/backend/lib/native-task-trace.js +440 -440
  109. package/src/backend/lib/native-task-transcript.js +393 -393
  110. package/src/backend/lib/prompt-registry.js +166 -166
  111. package/src/backend/lib/runtime-config.js +121 -121
  112. package/src/backend/lib/sdk-session-stream.js +143 -143
  113. package/src/backend/lib/sdk-turn-stream.js +16 -16
  114. package/src/backend/lib/store-scope.js +131 -131
  115. package/src/backend/lib/task-identity.js +104 -104
  116. package/src/backend/lib/task-state-projection.js +58 -58
  117. package/src/index.js +7 -7
  118. package/src/sdk-wave-a.ts +1 -1
  119. package/src/sdk-wave-b.ts +1 -1
  120. package/src/sdk-wave-c.ts +1 -1
  121. package/src/sdk-wave-d.ts +1 -1
  122. package/src/sdk-wave-e.ts +1 -1
@@ -1,143 +1,143 @@
1
- 'use strict';
2
-
3
- // One input queue and one reader own a native session. A turn result makes
4
- // the composer available; it does not terminate the process hosting tasks.
5
- class SdkSessionStream {
6
- constructor({ onMessage = () => {}, idleMs = 30000, setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
7
- this.onMessage = onMessage;
8
- this.idleMs = idleMs;
9
- this.setTimer = setTimer;
10
- this.clearTimer = clearTimer;
11
- this.foregroundRunning = false;
12
- this.completedTurns = 0;
13
- this.tasks = new Map();
14
- this.queue = [];
15
- this.waiter = null;
16
- this.closed = false;
17
- this.closedSignal = new Promise(resolve => { this.resolveClosed = resolve; });
18
- const self = this;
19
- this.input = {
20
- [Symbol.asyncIterator]() { return this; },
21
- next() {
22
- if (self.queue.length) return Promise.resolve({ value: self.queue.shift(), done: false });
23
- if (self.closed) return Promise.resolve({ done: true });
24
- return new Promise(resolve => { self.waiter = resolve; });
25
- },
26
- return() { return Promise.resolve({ done: true }); },
27
- };
28
- }
29
-
30
- // 'unknown' marks tasks whose only observed status was missing or an
31
- // unrecognized literal: still active, never a claimed terminal state.
32
- get activeTaskCount() { return [...this.tasks.values()].filter(status => status === 'running' || status === 'pending' || status === UNKNOWN_TASK_STATUS).length; }
33
-
34
- send(message) {
35
- if (this.closed) throw Object.assign(new Error('session transport is closed'), { code: 'SESSION_CLOSED' });
36
- if (this.foregroundRunning) throw Object.assign(new Error('session already has an active turn'), { code: 'TURN_BUSY', status: 409 });
37
- this.cancelIdle();
38
- this.foregroundRunning = true;
39
- if (this.waiter) { const resolve = this.waiter; this.waiter = null; resolve({ value: message, done: false }); }
40
- else this.queue.push(message);
41
- }
42
-
43
- cancelIdle() { if (this.idleTimer) { this.clearTimer(this.idleTimer); } this.idleTimer = null; }
44
-
45
- _observeTask(message) {
46
- if (!['task_started', 'task_progress', 'task_updated', 'task_notification'].includes(message.subtype)) return;
47
- const prior = this.tasks.get(message.task_id);
48
- if (prior !== undefined && TERMINAL_TASK_STATUS.has(prior)) return;
49
- const raw = message.subtype === 'task_updated' ? message.patch?.status : message.status;
50
- const known = normalizeTaskStatus(raw);
51
- if (known !== null) { this.tasks.set(message.task_id, known); return; }
52
- if (prior !== undefined) return;
53
- this.tasks.set(message.task_id, message.subtype === 'task_started' && !raw ? 'running' : UNKNOWN_TASK_STATUS);
54
- }
55
-
56
- observe(message) {
57
- const nested = Boolean(message.parent_tool_use_id);
58
- if (message.type === 'system' && message.task_id) this._observeTask(message);
59
- if (!nested && (message.type === 'result' || mainReplyEnded(message))) {
60
- this.foregroundRunning = false;
61
- if (message.type === 'result') this.completedTurns++;
62
- } else if (!nested && message.type === 'stream_event' && message.event?.type === 'message_start') {
63
- this.foregroundRunning = true;
64
- }
65
- this.onMessage(message);
66
- this.cancelIdle();
67
- if (!this.foregroundRunning && !this.activeTaskCount) {
68
- this.idleTimer = this.setTimer(() => this.close('idle'), this.idleMs);
69
- this.idleTimer?.unref?.();
70
- }
71
- }
72
-
73
- async consume(query) {
74
- if (this.query) throw new Error('session transport already has a reader');
75
- this.query = query;
76
- const iterator = query[Symbol.asyncIterator]();
77
- try {
78
- while (!this.closed) {
79
- const item = await Promise.race([iterator.next(), this.closedSignal]);
80
- if (this.closed) break;
81
- if (item.done) {
82
- if (this.foregroundRunning || this.activeTaskCount || !this.completedTurns) {
83
- throw Object.assign(new Error('Claude Code stream closed before all active work completed'), { code: 'INCOMPLETE_RESULT' });
84
- }
85
- break;
86
- }
87
- this.observe(item.value);
88
- }
89
- } finally { this.close('stream-ended'); }
90
- }
91
-
92
- close(reason = 'closed') {
93
- if (this.closed) return;
94
- this.closed = true;
95
- this.closeReason = reason;
96
- this.cancelIdle();
97
- this.queue.length = 0;
98
- this.waiter?.({ done: true }); this.waiter = null;
99
- this.resolveClosed({ done: true });
100
- this.query?.close?.();
101
- }
102
- }
103
-
104
- const TASK_STATUS = new Set(['pending', 'running', 'completed', 'failed', 'stopped']);
105
- const TERMINAL_TASK_STATUS = new Set(['completed', 'failed', 'stopped']);
106
- const UNKNOWN_TASK_STATUS = 'unknown';
107
- function normalizeTaskStatus(raw) {
108
- if (typeof raw !== 'string' || raw === '') return null;
109
- if (raw === 'killed') return 'stopped';
110
- return TASK_STATUS.has(raw) ? raw : null;
111
- }
112
- function mainReplyEnded(message) {
113
- if (message?.parent_tool_use_id) return false;
114
- const reason = message?.type === 'stream_event' && message.event?.type === 'message_delta'
115
- ? message.event.delta?.stop_reason : message?.type === 'assistant' ? message.message?.stop_reason : null;
116
- return ['end_turn', 'stop_sequence', 'refusal'].includes(reason);
117
- }
118
- function sdkTaskEvent(message, parentSessionId) {
119
- if (message?.type !== 'system' || !['task_started', 'task_progress', 'task_updated', 'task_notification'].includes(message.subtype) || !message.task_id) return null;
120
- const patch = message.patch || {};
121
- const rawStatus = typeof message.status === 'string' && message.status !== '' ? message.status
122
- : typeof patch.status === 'string' && patch.status !== '' ? patch.status : null;
123
- const known = normalizeTaskStatus(rawStatus);
124
- // Only the task_started fact may imply 'running'; every other missing or
125
- // unrecognized status is reported as an explicit unknown, never a success.
126
- const status = known !== null ? known : message.subtype === 'task_started' && rawStatus === null ? 'running' : null;
127
- const usage = message.usage && Object.fromEntries(['total_tokens', 'tool_uses', 'duration_ms'].filter(key => Number.isFinite(message.usage[key])).map(key => [key, Math.max(0, message.usage[key])]));
128
- return {
129
- type: `claude/${message.subtype.replaceAll('_', '-')}`,
130
- taskId: String(message.task_id), parentSessionId,
131
- sdkSessionId: typeof message.session_id === 'string' && message.session_id !== '' ? message.session_id : null,
132
- taskType: typeof message.task_type === 'string' && message.task_type !== '' ? message.task_type : null,
133
- ...(message.tool_use_id ? { toolUseId: String(message.tool_use_id) } : {}),
134
- ...(message.description || patch.description ? { description: String(message.description || patch.description).slice(0, 2000) } : {}),
135
- status,
136
- ...(status === null ? { diagnostic: rawStatus === null ? 'task-status-missing' : 'task-status-unknown', ...(rawStatus !== null ? { rawStatus: rawStatus.slice(0, 40) } : {}) } : {}),
137
- ...(message.summary || patch.error ? { summary: String(message.summary || patch.error).slice(0, 8000) } : {}),
138
- ...(message.last_tool_name ? { lastToolName: String(message.last_tool_name) } : {}),
139
- ...(usage ? { usage } : {}),
140
- };
141
- }
142
-
143
- module.exports = { SdkSessionStream, sdkTaskEvent, mainReplyEnded };
1
+ 'use strict';
2
+
3
+ // One input queue and one reader own a native session. A turn result makes
4
+ // the composer available; it does not terminate the process hosting tasks.
5
+ class SdkSessionStream {
6
+ constructor({ onMessage = () => {}, idleMs = 30000, setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
7
+ this.onMessage = onMessage;
8
+ this.idleMs = idleMs;
9
+ this.setTimer = setTimer;
10
+ this.clearTimer = clearTimer;
11
+ this.foregroundRunning = false;
12
+ this.completedTurns = 0;
13
+ this.tasks = new Map();
14
+ this.queue = [];
15
+ this.waiter = null;
16
+ this.closed = false;
17
+ this.closedSignal = new Promise(resolve => { this.resolveClosed = resolve; });
18
+ const self = this;
19
+ this.input = {
20
+ [Symbol.asyncIterator]() { return this; },
21
+ next() {
22
+ if (self.queue.length) return Promise.resolve({ value: self.queue.shift(), done: false });
23
+ if (self.closed) return Promise.resolve({ done: true });
24
+ return new Promise(resolve => { self.waiter = resolve; });
25
+ },
26
+ return() { return Promise.resolve({ done: true }); },
27
+ };
28
+ }
29
+
30
+ // 'unknown' marks tasks whose only observed status was missing or an
31
+ // unrecognized literal: still active, never a claimed terminal state.
32
+ get activeTaskCount() { return [...this.tasks.values()].filter(status => status === 'running' || status === 'pending' || status === UNKNOWN_TASK_STATUS).length; }
33
+
34
+ send(message) {
35
+ if (this.closed) throw Object.assign(new Error('session transport is closed'), { code: 'SESSION_CLOSED' });
36
+ if (this.foregroundRunning) throw Object.assign(new Error('session already has an active turn'), { code: 'TURN_BUSY', status: 409 });
37
+ this.cancelIdle();
38
+ this.foregroundRunning = true;
39
+ if (this.waiter) { const resolve = this.waiter; this.waiter = null; resolve({ value: message, done: false }); }
40
+ else this.queue.push(message);
41
+ }
42
+
43
+ cancelIdle() { if (this.idleTimer) { this.clearTimer(this.idleTimer); } this.idleTimer = null; }
44
+
45
+ _observeTask(message) {
46
+ if (!['task_started', 'task_progress', 'task_updated', 'task_notification'].includes(message.subtype)) return;
47
+ const prior = this.tasks.get(message.task_id);
48
+ if (prior !== undefined && TERMINAL_TASK_STATUS.has(prior)) return;
49
+ const raw = message.subtype === 'task_updated' ? message.patch?.status : message.status;
50
+ const known = normalizeTaskStatus(raw);
51
+ if (known !== null) { this.tasks.set(message.task_id, known); return; }
52
+ if (prior !== undefined) return;
53
+ this.tasks.set(message.task_id, message.subtype === 'task_started' && !raw ? 'running' : UNKNOWN_TASK_STATUS);
54
+ }
55
+
56
+ observe(message) {
57
+ const nested = Boolean(message.parent_tool_use_id);
58
+ if (message.type === 'system' && message.task_id) this._observeTask(message);
59
+ if (!nested && (message.type === 'result' || mainReplyEnded(message))) {
60
+ this.foregroundRunning = false;
61
+ if (message.type === 'result') this.completedTurns++;
62
+ } else if (!nested && message.type === 'stream_event' && message.event?.type === 'message_start') {
63
+ this.foregroundRunning = true;
64
+ }
65
+ this.onMessage(message);
66
+ this.cancelIdle();
67
+ if (!this.foregroundRunning && !this.activeTaskCount) {
68
+ this.idleTimer = this.setTimer(() => this.close('idle'), this.idleMs);
69
+ this.idleTimer?.unref?.();
70
+ }
71
+ }
72
+
73
+ async consume(query) {
74
+ if (this.query) throw new Error('session transport already has a reader');
75
+ this.query = query;
76
+ const iterator = query[Symbol.asyncIterator]();
77
+ try {
78
+ while (!this.closed) {
79
+ const item = await Promise.race([iterator.next(), this.closedSignal]);
80
+ if (this.closed) break;
81
+ if (item.done) {
82
+ if (this.foregroundRunning || this.activeTaskCount || !this.completedTurns) {
83
+ throw Object.assign(new Error('Claude Code stream closed before all active work completed'), { code: 'INCOMPLETE_RESULT' });
84
+ }
85
+ break;
86
+ }
87
+ this.observe(item.value);
88
+ }
89
+ } finally { this.close('stream-ended'); }
90
+ }
91
+
92
+ close(reason = 'closed') {
93
+ if (this.closed) return;
94
+ this.closed = true;
95
+ this.closeReason = reason;
96
+ this.cancelIdle();
97
+ this.queue.length = 0;
98
+ this.waiter?.({ done: true }); this.waiter = null;
99
+ this.resolveClosed({ done: true });
100
+ this.query?.close?.();
101
+ }
102
+ }
103
+
104
+ const TASK_STATUS = new Set(['pending', 'running', 'completed', 'failed', 'stopped']);
105
+ const TERMINAL_TASK_STATUS = new Set(['completed', 'failed', 'stopped']);
106
+ const UNKNOWN_TASK_STATUS = 'unknown';
107
+ function normalizeTaskStatus(raw) {
108
+ if (typeof raw !== 'string' || raw === '') return null;
109
+ if (raw === 'killed') return 'stopped';
110
+ return TASK_STATUS.has(raw) ? raw : null;
111
+ }
112
+ function mainReplyEnded(message) {
113
+ if (message?.parent_tool_use_id) return false;
114
+ const reason = message?.type === 'stream_event' && message.event?.type === 'message_delta'
115
+ ? message.event.delta?.stop_reason : message?.type === 'assistant' ? message.message?.stop_reason : null;
116
+ return ['end_turn', 'stop_sequence', 'refusal'].includes(reason);
117
+ }
118
+ function sdkTaskEvent(message, parentSessionId) {
119
+ if (message?.type !== 'system' || !['task_started', 'task_progress', 'task_updated', 'task_notification'].includes(message.subtype) || !message.task_id) return null;
120
+ const patch = message.patch || {};
121
+ const rawStatus = typeof message.status === 'string' && message.status !== '' ? message.status
122
+ : typeof patch.status === 'string' && patch.status !== '' ? patch.status : null;
123
+ const known = normalizeTaskStatus(rawStatus);
124
+ // Only the task_started fact may imply 'running'; every other missing or
125
+ // unrecognized status is reported as an explicit unknown, never a success.
126
+ const status = known !== null ? known : message.subtype === 'task_started' && rawStatus === null ? 'running' : null;
127
+ const usage = message.usage && Object.fromEntries(['total_tokens', 'tool_uses', 'duration_ms'].filter(key => Number.isFinite(message.usage[key])).map(key => [key, Math.max(0, message.usage[key])]));
128
+ return {
129
+ type: `claude/${message.subtype.replaceAll('_', '-')}`,
130
+ taskId: String(message.task_id), parentSessionId,
131
+ sdkSessionId: typeof message.session_id === 'string' && message.session_id !== '' ? message.session_id : null,
132
+ taskType: typeof message.task_type === 'string' && message.task_type !== '' ? message.task_type : null,
133
+ ...(message.tool_use_id ? { toolUseId: String(message.tool_use_id) } : {}),
134
+ ...(message.description || patch.description ? { description: String(message.description || patch.description).slice(0, 2000) } : {}),
135
+ status,
136
+ ...(status === null ? { diagnostic: rawStatus === null ? 'task-status-missing' : 'task-status-unknown', ...(rawStatus !== null ? { rawStatus: rawStatus.slice(0, 40) } : {}) } : {}),
137
+ ...(message.summary || patch.error ? { summary: String(message.summary || patch.error).slice(0, 8000) } : {}),
138
+ ...(message.last_tool_name ? { lastToolName: String(message.last_tool_name) } : {}),
139
+ ...(usage ? { usage } : {}),
140
+ };
141
+ }
142
+
143
+ module.exports = { SdkSessionStream, sdkTaskEvent, mainReplyEnded };
@@ -1,16 +1,16 @@
1
- 'use strict';
2
-
3
- // Each Workbench turn owns a query. A result is its protocol boundary; an
4
- // SDK transport can remain open for later notifications after that boundary.
5
- async function consumeSdkTurn(query, onMessage) {
6
- const iterator = query[Symbol.asyncIterator]();
7
- try {
8
- while (true) {
9
- const item = await iterator.next();
10
- if (item.done) throw Object.assign(new Error('Claude Code stream closed before its final result'), { code: 'INCOMPLETE_RESULT' });
11
- onMessage(item.value);
12
- if (item.value?.type === 'result') return item.value;
13
- }
14
- } finally { query.close?.(); }
15
- }
16
- module.exports = { consumeSdkTurn };
1
+ 'use strict';
2
+
3
+ // Each Workbench turn owns a query. A result is its protocol boundary; an
4
+ // SDK transport can remain open for later notifications after that boundary.
5
+ async function consumeSdkTurn(query, onMessage) {
6
+ const iterator = query[Symbol.asyncIterator]();
7
+ try {
8
+ while (true) {
9
+ const item = await iterator.next();
10
+ if (item.done) throw Object.assign(new Error('Claude Code stream closed before its final result'), { code: 'INCOMPLETE_RESULT' });
11
+ onMessage(item.value);
12
+ if (item.value?.type === 'result') return item.value;
13
+ }
14
+ } finally { query.close?.(); }
15
+ }
16
+ module.exports = { consumeSdkTurn };
@@ -1,131 +1,131 @@
1
- 'use strict';
2
-
3
- // Stable store identity for a workbench data directory (W1-05).
4
- //
5
- // <dataDir>/store-id.v1.json holds { schema, storeId }; the storeId UUID v4 is
6
- // the persistent storeScope that survives restarts. Volatile values (PID,
7
- // port, runtime epoch, path hashing) may never stand in for it, and the
8
- // dataDir path itself never leaves this module.
9
- //
10
- // Publication protocol - a half-written file is never observable as final:
11
- // 1. read the final file; only a genuine ENOENT may start creation,
12
- // 2. write the complete document to a unique temp file opened 'wx' (mode
13
- // 0600), fsync, close,
14
- // 3. publish atomically and exclusively with a hard link; EEXIST means a
15
- // concurrent winner exists - adopt its bytes, never overwrite them,
16
- // 4. remove our own temp name only; foreign or stale temps stay untouched.
17
- // Every failure maps to a fixed errorCode (no paths, no raw OS messages) and
18
- // an existing final file always keeps its exact bytes.
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const crypto = require('crypto');
23
-
24
- const STORE_FILE = 'store-id.v1.json';
25
- const STORE_SCHEMA = 'workbench-store/v1';
26
- // Canonical lowercase UUID v4 is the only accepted storeId spelling.
27
- const STORE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
28
-
29
- function unavailable(errorCode) {
30
- return { available: false, storeScope: null, errorCode };
31
- }
32
-
33
- function parseStoreDocument(text) {
34
- let parsed;
35
- try { parsed = JSON.parse(text); } catch { return null; }
36
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
37
- const keys = Object.keys(parsed);
38
- if (keys.length !== 2 || !('schema' in parsed) || !('storeId' in parsed)) return null;
39
- if (parsed.schema !== STORE_SCHEMA) return null;
40
- return typeof parsed.storeId === 'string' && STORE_ID_PATTERN.test(parsed.storeId) ? parsed.storeId : null;
41
- }
42
-
43
- // Returns { found: false } only for a genuine ENOENT; any other error throws.
44
- function readFinalFile(fssystem, file) {
45
- try {
46
- return { found: true, text: fssystem.readFileSync(file, 'utf8') };
47
- } catch (error) {
48
- if (error && error.code === 'ENOENT') return { found: false };
49
- throw error;
50
- }
51
- }
52
-
53
- function removeOwnTemp(fssystem, file) {
54
- try { fssystem.unlinkSync(file); } catch { /* best effort; must not mask the primary failure */ }
55
- }
56
-
57
- function adoptFinalFile(fssystem, file) {
58
- let winner;
59
- try {
60
- winner = readFinalFile(fssystem, file);
61
- } catch {
62
- return unavailable('STORE_SCOPE_READ_FAILED');
63
- }
64
- if (!winner.found) return unavailable('STORE_SCOPE_READ_FAILED');
65
- const storeId = parseStoreDocument(winner.text);
66
- return storeId ? { available: true, storeScope: storeId, errorCode: null } : unavailable('STORE_SCOPE_INVALID');
67
- }
68
-
69
- /**
70
- * Resolve (creating on first use) the persistent store scope of dataDir.
71
- * Returns { available: true, storeScope, errorCode: null } or
72
- * { available: false, storeScope: null, errorCode } where errorCode is one of
73
- * the fixed codes STORE_SCOPE_MKDIR_FAILED | STORE_SCOPE_READ_FAILED |
74
- * STORE_SCOPE_INVALID | STORE_SCOPE_WRITE_FAILED | STORE_SCOPE_LINK_FAILED.
75
- * Never throws, never leaks the dataDir path or a raw OS error message.
76
- * options.fs injects the sync fs primitives for deterministic tests.
77
- */
78
- function ensureStoreScope(dataDir, options = {}) {
79
- const fssystem = options.fs || fs;
80
- const finalFile = path.join(dataDir, STORE_FILE);
81
- let initial;
82
- try {
83
- initial = readFinalFile(fssystem, finalFile);
84
- } catch {
85
- return unavailable('STORE_SCOPE_READ_FAILED');
86
- }
87
- if (initial.found) {
88
- const storeId = parseStoreDocument(initial.text);
89
- return storeId ? { available: true, storeScope: storeId, errorCode: null } : unavailable('STORE_SCOPE_INVALID');
90
- }
91
- try {
92
- fssystem.mkdirSync(dataDir, { recursive: true });
93
- } catch {
94
- return unavailable('STORE_SCOPE_MKDIR_FAILED');
95
- }
96
- const storeId = crypto.randomUUID();
97
- const payload = Buffer.from(JSON.stringify({ schema: STORE_SCHEMA, storeId }) + '\n', 'utf8');
98
- const tempFile = path.join(dataDir, STORE_FILE + '.' + crypto.randomUUID() + '.tmp');
99
- let fd = null;
100
- let created = false;
101
- try {
102
- fd = fssystem.openSync(tempFile, 'wx', 0o600);
103
- created = true;
104
- for (let written = 0; written < payload.length; ) {
105
- // Same stance as SessionHistory.flush: a write reporting no progress
106
- // must fail loudly - looping would hang boot, succeeding would publish
107
- // a truncated document.
108
- const progress = fssystem.writeSync(fd, payload, written, payload.length - written);
109
- if (!progress) throw Object.assign(new Error('store scope write made no progress'), { code: 'EIO' });
110
- written += progress;
111
- }
112
- fssystem.fsyncSync(fd);
113
- fssystem.closeSync(fd);
114
- fd = null;
115
- } catch {
116
- if (fd !== null) { try { fssystem.closeSync(fd); } catch { /* primary failure is reported below */ } }
117
- if (created) removeOwnTemp(fssystem, tempFile);
118
- return unavailable('STORE_SCOPE_WRITE_FAILED');
119
- }
120
- try {
121
- fssystem.linkSync(tempFile, finalFile);
122
- } catch (error) {
123
- removeOwnTemp(fssystem, tempFile);
124
- if (error && error.code === 'EEXIST') return adoptFinalFile(fssystem, finalFile);
125
- return unavailable('STORE_SCOPE_LINK_FAILED');
126
- }
127
- removeOwnTemp(fssystem, tempFile);
128
- return { available: true, storeScope: storeId, errorCode: null };
129
- }
130
-
131
- module.exports = { ensureStoreScope };
1
+ 'use strict';
2
+
3
+ // Stable store identity for a workbench data directory (W1-05).
4
+ //
5
+ // <dataDir>/store-id.v1.json holds { schema, storeId }; the storeId UUID v4 is
6
+ // the persistent storeScope that survives restarts. Volatile values (PID,
7
+ // port, runtime epoch, path hashing) may never stand in for it, and the
8
+ // dataDir path itself never leaves this module.
9
+ //
10
+ // Publication protocol - a half-written file is never observable as final:
11
+ // 1. read the final file; only a genuine ENOENT may start creation,
12
+ // 2. write the complete document to a unique temp file opened 'wx' (mode
13
+ // 0600), fsync, close,
14
+ // 3. publish atomically and exclusively with a hard link; EEXIST means a
15
+ // concurrent winner exists - adopt its bytes, never overwrite them,
16
+ // 4. remove our own temp name only; foreign or stale temps stay untouched.
17
+ // Every failure maps to a fixed errorCode (no paths, no raw OS messages) and
18
+ // an existing final file always keeps its exact bytes.
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const crypto = require('crypto');
23
+
24
+ const STORE_FILE = 'store-id.v1.json';
25
+ const STORE_SCHEMA = 'workbench-store/v1';
26
+ // Canonical lowercase UUID v4 is the only accepted storeId spelling.
27
+ const STORE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
28
+
29
+ function unavailable(errorCode) {
30
+ return { available: false, storeScope: null, errorCode };
31
+ }
32
+
33
+ function parseStoreDocument(text) {
34
+ let parsed;
35
+ try { parsed = JSON.parse(text); } catch { return null; }
36
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
37
+ const keys = Object.keys(parsed);
38
+ if (keys.length !== 2 || !('schema' in parsed) || !('storeId' in parsed)) return null;
39
+ if (parsed.schema !== STORE_SCHEMA) return null;
40
+ return typeof parsed.storeId === 'string' && STORE_ID_PATTERN.test(parsed.storeId) ? parsed.storeId : null;
41
+ }
42
+
43
+ // Returns { found: false } only for a genuine ENOENT; any other error throws.
44
+ function readFinalFile(fssystem, file) {
45
+ try {
46
+ return { found: true, text: fssystem.readFileSync(file, 'utf8') };
47
+ } catch (error) {
48
+ if (error && error.code === 'ENOENT') return { found: false };
49
+ throw error;
50
+ }
51
+ }
52
+
53
+ function removeOwnTemp(fssystem, file) {
54
+ try { fssystem.unlinkSync(file); } catch { /* best effort; must not mask the primary failure */ }
55
+ }
56
+
57
+ function adoptFinalFile(fssystem, file) {
58
+ let winner;
59
+ try {
60
+ winner = readFinalFile(fssystem, file);
61
+ } catch {
62
+ return unavailable('STORE_SCOPE_READ_FAILED');
63
+ }
64
+ if (!winner.found) return unavailable('STORE_SCOPE_READ_FAILED');
65
+ const storeId = parseStoreDocument(winner.text);
66
+ return storeId ? { available: true, storeScope: storeId, errorCode: null } : unavailable('STORE_SCOPE_INVALID');
67
+ }
68
+
69
+ /**
70
+ * Resolve (creating on first use) the persistent store scope of dataDir.
71
+ * Returns { available: true, storeScope, errorCode: null } or
72
+ * { available: false, storeScope: null, errorCode } where errorCode is one of
73
+ * the fixed codes STORE_SCOPE_MKDIR_FAILED | STORE_SCOPE_READ_FAILED |
74
+ * STORE_SCOPE_INVALID | STORE_SCOPE_WRITE_FAILED | STORE_SCOPE_LINK_FAILED.
75
+ * Never throws, never leaks the dataDir path or a raw OS error message.
76
+ * options.fs injects the sync fs primitives for deterministic tests.
77
+ */
78
+ function ensureStoreScope(dataDir, options = {}) {
79
+ const fssystem = options.fs || fs;
80
+ const finalFile = path.join(dataDir, STORE_FILE);
81
+ let initial;
82
+ try {
83
+ initial = readFinalFile(fssystem, finalFile);
84
+ } catch {
85
+ return unavailable('STORE_SCOPE_READ_FAILED');
86
+ }
87
+ if (initial.found) {
88
+ const storeId = parseStoreDocument(initial.text);
89
+ return storeId ? { available: true, storeScope: storeId, errorCode: null } : unavailable('STORE_SCOPE_INVALID');
90
+ }
91
+ try {
92
+ fssystem.mkdirSync(dataDir, { recursive: true });
93
+ } catch {
94
+ return unavailable('STORE_SCOPE_MKDIR_FAILED');
95
+ }
96
+ const storeId = crypto.randomUUID();
97
+ const payload = Buffer.from(JSON.stringify({ schema: STORE_SCHEMA, storeId }) + '\n', 'utf8');
98
+ const tempFile = path.join(dataDir, STORE_FILE + '.' + crypto.randomUUID() + '.tmp');
99
+ let fd = null;
100
+ let created = false;
101
+ try {
102
+ fd = fssystem.openSync(tempFile, 'wx', 0o600);
103
+ created = true;
104
+ for (let written = 0; written < payload.length; ) {
105
+ // Same stance as SessionHistory.flush: a write reporting no progress
106
+ // must fail loudly - looping would hang boot, succeeding would publish
107
+ // a truncated document.
108
+ const progress = fssystem.writeSync(fd, payload, written, payload.length - written);
109
+ if (!progress) throw Object.assign(new Error('store scope write made no progress'), { code: 'EIO' });
110
+ written += progress;
111
+ }
112
+ fssystem.fsyncSync(fd);
113
+ fssystem.closeSync(fd);
114
+ fd = null;
115
+ } catch {
116
+ if (fd !== null) { try { fssystem.closeSync(fd); } catch { /* primary failure is reported below */ } }
117
+ if (created) removeOwnTemp(fssystem, tempFile);
118
+ return unavailable('STORE_SCOPE_WRITE_FAILED');
119
+ }
120
+ try {
121
+ fssystem.linkSync(tempFile, finalFile);
122
+ } catch (error) {
123
+ removeOwnTemp(fssystem, tempFile);
124
+ if (error && error.code === 'EEXIST') return adoptFinalFile(fssystem, finalFile);
125
+ return unavailable('STORE_SCOPE_LINK_FAILED');
126
+ }
127
+ removeOwnTemp(fssystem, tempFile);
128
+ return { available: true, storeScope: storeId, errorCode: null };
129
+ }
130
+
131
+ module.exports = { ensureStoreScope };