@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.
- package/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +636 -613
- package/apps/agent-terminal/app.js +125 -65
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +171 -170
- package/apps/agent-terminal/panel-host-proof.html +152 -152
- package/apps/agent-terminal/panel-proof-content.html +41 -41
- package/apps/agent-terminal/right-workspace-dock.css +187 -159
- package/apps/agent-terminal/right-workspace-dock.js +623 -519
- package/apps/ai-profile-settings-host/index.html +25 -25
- package/apps/ai-profile-settings-host/page.css +1 -1
- package/apps/ai-profile-settings-host/page.js +113 -113
- package/apps/minimal-test-host/README.md +15 -15
- package/apps/minimal-test-host/host.css +2 -2
- package/apps/minimal-test-host/host.js +35 -35
- package/apps/minimal-test-host/index.html +58 -58
- package/apps/minimal-test-host/optional-mock/fault-injection.js +15 -15
- package/docs/sdk/FILE_LAYOUT.md +31 -31
- package/docs/sdk/INTEGRATION.md +126 -126
- package/docs/sdk/README.md +21 -21
- package/docs/sdk/SESSION_EVENTS.md +13 -13
- package/docs/sdk/openapi.yaml +8 -1
- package/package.json +1 -1
- package/packages/client/README.md +29 -29
- package/packages/client/index.d.ts +122 -122
- package/packages/client/index.js +260 -260
- package/packages/client/package.json +2 -2
- package/packages/contracts/README.md +9 -9
- package/packages/contracts/index.d.ts +519 -517
- package/packages/contracts/index.js +312 -312
- package/packages/contracts/package.json +1 -1
- package/packages/core/README.md +8 -8
- package/packages/core/index.d.ts +76 -76
- package/packages/core/index.js +222 -222
- package/packages/core/package.json +2 -2
- package/packages/server/README.md +17 -17
- package/packages/server/bin/agent-terminal-server.js +42 -42
- package/packages/server/bin/claude-workbench.js +225 -225
- package/packages/server/index.d.ts +171 -171
- package/packages/server/index.js +230 -230
- package/packages/server/lib/agent-discovery.js +133 -133
- package/packages/server/lib/agent-registry.js +143 -143
- package/packages/server/lib/agent-spawn-env.js +17 -17
- package/packages/server/lib/attachment-store.js +78 -78
- package/packages/server/lib/bridges/app-server-bridge.js +373 -373
- package/packages/server/lib/bridges/opencode-server-bridge.js +506 -506
- package/packages/server/lib/capture-settings-store.js +35 -35
- package/packages/server/lib/claude-history-recovery.js +87 -87
- package/packages/server/lib/cli-session-recovery.js +28 -28
- package/packages/server/lib/context-resolver.js +55 -55
- package/packages/server/lib/conversation-capture.js +51 -51
- package/packages/server/lib/delegation-phase-store.js +357 -357
- package/packages/server/lib/delegation-store.js +474 -468
- package/packages/server/lib/diagnostics.js +32 -32
- package/packages/server/lib/drivers/cli-json-runner.js +974 -974
- package/packages/server/lib/drivers/codex-driver.js +1442 -1442
- package/packages/server/lib/drivers/opencode-driver.js +1554 -1554
- package/packages/server/lib/drivers/zcode-driver.js +1250 -1250
- package/packages/server/lib/drivers/zcode-profile-runtime.js +71 -71
- package/packages/server/lib/event-stream.js +138 -138
- package/packages/server/lib/folder-picker.js +623 -623
- package/packages/server/lib/http-handler.js +339 -339
- package/packages/server/lib/json-file.js +26 -26
- package/packages/server/lib/knowledge-context.js +33 -33
- package/packages/server/lib/model-detection.js +295 -295
- package/packages/server/lib/parent-context.js +44 -0
- package/packages/server/lib/profile-store.js +283 -283
- package/packages/server/lib/project-store.js +102 -102
- package/packages/server/lib/runtime-loader.js +120 -120
- package/packages/server/lib/session-archive-store.js +53 -53
- package/packages/server/lib/session-history.js +612 -612
- package/packages/server/lib/system-doctor.js +125 -125
- package/packages/server/lib/ui-preferences-store.js +48 -48
- package/packages/server/lib/workbench-service.js +1696 -1675
- package/packages/server/package.json +2 -2
- package/packages/ui/README.md +30 -30
- package/packages/ui/ai-profile-settings.d.ts +45 -45
- package/packages/ui/ai-profile-settings.js +283 -283
- package/packages/ui/claude-terminal.d.ts +31 -31
- package/packages/ui/claude-terminal.js +343 -343
- package/packages/ui/index.d.ts +18 -18
- package/packages/ui/index.js +51 -51
- package/packages/ui/package.json +2 -2
- package/packages/ui/theme.css +13 -13
- package/src/assets/claude-prompts.json +21 -21
- package/src/backend/http/ai-profile-routes.js +61 -61
- package/src/backend/http/claude-routes.js +170 -170
- package/src/backend/http/node-helpers.js +45 -45
- package/src/backend/index.js +21 -21
- package/src/backend/lib/ai-adapter.js +144 -144
- package/src/backend/lib/ai-profile-store.js +259 -259
- package/src/backend/lib/ai-vendor-presets.js +83 -83
- package/src/backend/lib/ai-workspace.js +140 -140
- package/src/backend/lib/automation-config.js +253 -253
- package/src/backend/lib/claude-cli-runner.js +2392 -2392
- package/src/backend/lib/data-dir.js +169 -169
- package/src/backend/lib/llm-client.js +176 -176
- package/src/backend/lib/model-context-windows.js +82 -82
- package/src/backend/lib/native-task-control.js +217 -217
- package/src/backend/lib/native-task-history-index.js +475 -475
- package/src/backend/lib/native-task-history-reader.js +526 -526
- package/src/backend/lib/native-task-messages.js +457 -457
- package/src/backend/lib/native-task-registry.js +325 -325
- package/src/backend/lib/native-task-runtime.js +242 -242
- package/src/backend/lib/native-task-trace.js +440 -440
- package/src/backend/lib/native-task-transcript.js +393 -393
- package/src/backend/lib/prompt-registry.js +166 -166
- package/src/backend/lib/runtime-config.js +121 -121
- package/src/backend/lib/sdk-session-stream.js +143 -143
- package/src/backend/lib/sdk-turn-stream.js +16 -16
- package/src/backend/lib/store-scope.js +131 -131
- package/src/backend/lib/task-identity.js +104 -104
- package/src/backend/lib/task-state-projection.js +58 -58
- package/src/index.js +7 -7
- package/src/sdk-wave-a.ts +1 -1
- package/src/sdk-wave-b.ts +1 -1
- package/src/sdk-wave-c.ts +1 -1
- package/src/sdk-wave-d.ts +1 -1
- 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 };
|