@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3

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 (120) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +156 -156
  3. package/apps/agent-terminal/app.css +636 -613
  4. package/apps/agent-terminal/app.js +125 -65
  5. package/apps/agent-terminal/conversation-composer.js +44 -0
  6. package/apps/agent-terminal/index.html +171 -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 +187 -159
  10. package/apps/agent-terminal/right-workspace-dock.js +623 -519
  11. package/apps/ai-profile-settings-host/index.html +25 -25
  12. package/apps/ai-profile-settings-host/page.css +1 -1
  13. package/apps/ai-profile-settings-host/page.js +113 -113
  14. package/apps/minimal-test-host/README.md +15 -15
  15. package/apps/minimal-test-host/host.css +2 -2
  16. package/apps/minimal-test-host/host.js +35 -35
  17. package/apps/minimal-test-host/index.html +58 -58
  18. package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
  19. package/docs/sdk/FILE_LAYOUT.md +31 -31
  20. package/docs/sdk/INTEGRATION.md +126 -126
  21. package/docs/sdk/README.md +21 -21
  22. package/docs/sdk/SESSION_EVENTS.md +13 -13
  23. package/docs/sdk/openapi.yaml +8 -1
  24. package/package.json +1 -1
  25. package/packages/client/README.md +29 -29
  26. package/packages/client/index.d.ts +122 -122
  27. package/packages/client/index.js +260 -260
  28. package/packages/client/package.json +2 -2
  29. package/packages/contracts/README.md +9 -9
  30. package/packages/contracts/index.d.ts +519 -517
  31. package/packages/contracts/index.js +312 -312
  32. package/packages/contracts/package.json +1 -1
  33. package/packages/core/README.md +8 -8
  34. package/packages/core/index.d.ts +76 -76
  35. package/packages/core/index.js +222 -222
  36. package/packages/core/package.json +2 -2
  37. package/packages/server/README.md +17 -17
  38. package/packages/server/bin/agent-terminal-server.js +42 -42
  39. package/packages/server/bin/claude-workbench.js +225 -225
  40. package/packages/server/index.d.ts +171 -171
  41. package/packages/server/index.js +230 -230
  42. package/packages/server/lib/agent-discovery.js +133 -133
  43. package/packages/server/lib/agent-registry.js +143 -143
  44. package/packages/server/lib/agent-spawn-env.js +17 -17
  45. package/packages/server/lib/attachment-store.js +78 -78
  46. package/packages/server/lib/bridges/app-server-bridge.js +373 -373
  47. package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
  48. package/packages/server/lib/capture-settings-store.js +35 -35
  49. package/packages/server/lib/claude-history-recovery.js +87 -87
  50. package/packages/server/lib/cli-session-recovery.js +28 -28
  51. package/packages/server/lib/context-resolver.js +55 -55
  52. package/packages/server/lib/conversation-capture.js +51 -51
  53. package/packages/server/lib/delegation-phase-store.js +357 -357
  54. package/packages/server/lib/delegation-store.js +474 -468
  55. package/packages/server/lib/diagnostics.js +32 -32
  56. package/packages/server/lib/drivers/cli-json-runner.js +974 -974
  57. package/packages/server/lib/drivers/codex-driver.js +1442 -1442
  58. package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
  59. package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
  60. package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
  61. package/packages/server/lib/event-stream.js +138 -138
  62. package/packages/server/lib/folder-picker.js +623 -623
  63. package/packages/server/lib/http-handler.js +339 -339
  64. package/packages/server/lib/json-file.js +26 -26
  65. package/packages/server/lib/knowledge-context.js +33 -33
  66. package/packages/server/lib/model-detection.js +295 -295
  67. package/packages/server/lib/parent-context.js +44 -0
  68. package/packages/server/lib/profile-store.js +283 -283
  69. package/packages/server/lib/project-store.js +102 -102
  70. package/packages/server/lib/runtime-loader.js +120 -120
  71. package/packages/server/lib/session-archive-store.js +53 -53
  72. package/packages/server/lib/session-history.js +612 -612
  73. package/packages/server/lib/system-doctor.js +125 -125
  74. package/packages/server/lib/ui-preferences-store.js +48 -48
  75. package/packages/server/lib/workbench-service.js +1696 -1675
  76. package/packages/server/package.json +2 -2
  77. package/packages/ui/README.md +30 -30
  78. package/packages/ui/ai-profile-settings.d.ts +45 -45
  79. package/packages/ui/ai-profile-settings.js +283 -283
  80. package/packages/ui/claude-terminal.d.ts +31 -31
  81. package/packages/ui/claude-terminal.js +343 -343
  82. package/packages/ui/index.d.ts +18 -18
  83. package/packages/ui/index.js +51 -51
  84. package/packages/ui/package.json +2 -2
  85. package/packages/ui/theme.css +13 -13
  86. package/src/assets/claude-prompts.json +21 -21
  87. package/src/backend/http/ai-profile-routes.js +61 -61
  88. package/src/backend/http/claude-routes.js +170 -170
  89. package/src/backend/http/node-helpers.js +45 -45
  90. package/src/backend/index.js +21 -21
  91. package/src/backend/lib/ai-adapter.js +144 -144
  92. package/src/backend/lib/ai-profile-store.js +259 -259
  93. package/src/backend/lib/ai-vendor-presets.js +83 -83
  94. package/src/backend/lib/ai-workspace.js +140 -140
  95. package/src/backend/lib/automation-config.js +253 -253
  96. package/src/backend/lib/claude-cli-runner.js +2392 -2392
  97. package/src/backend/lib/data-dir.js +169 -169
  98. package/src/backend/lib/llm-client.js +176 -176
  99. package/src/backend/lib/model-context-windows.js +82 -82
  100. package/src/backend/lib/native-task-control.js +217 -217
  101. package/src/backend/lib/native-task-history-index.js +475 -475
  102. package/src/backend/lib/native-task-history-reader.js +526 -526
  103. package/src/backend/lib/native-task-messages.js +457 -457
  104. package/src/backend/lib/native-task-registry.js +325 -325
  105. package/src/backend/lib/native-task-runtime.js +242 -242
  106. package/src/backend/lib/native-task-trace.js +440 -440
  107. package/src/backend/lib/native-task-transcript.js +393 -393
  108. package/src/backend/lib/prompt-registry.js +166 -166
  109. package/src/backend/lib/runtime-config.js +121 -121
  110. package/src/backend/lib/sdk-session-stream.js +143 -143
  111. package/src/backend/lib/sdk-turn-stream.js +16 -16
  112. package/src/backend/lib/store-scope.js +131 -131
  113. package/src/backend/lib/task-identity.js +104 -104
  114. package/src/backend/lib/task-state-projection.js +58 -58
  115. package/src/index.js +7 -7
  116. package/src/sdk-wave-a.ts +1 -1
  117. package/src/sdk-wave-b.ts +1 -1
  118. package/src/sdk-wave-c.ts +1 -1
  119. package/src/sdk-wave-d.ts +1 -1
  120. 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 };