@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.
- package/LICENSE +201 -201
- package/README.md +156 -156
- package/apps/agent-terminal/app.css +652 -613
- package/apps/agent-terminal/app.js +143 -67
- package/apps/agent-terminal/conversation-composer.js +44 -0
- package/apps/agent-terminal/index.html +172 -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 +189 -159
- package/apps/agent-terminal/right-workspace-dock.js +652 -521
- package/apps/agent-terminal/user-question-card.js +68 -0
- 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 +15 -13
- package/docs/sdk/openapi.yaml +16 -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 +1461 -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/user-questions.js +67 -0
- 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 +2408 -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,71 +1,71 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const http = require('node:http');
|
|
3
|
-
const https = require('node:https');
|
|
4
|
-
const fs = require('node:fs');
|
|
5
|
-
const os = require('node:os');
|
|
6
|
-
const path = require('node:path');
|
|
7
|
-
const crypto = require('node:crypto');
|
|
8
|
-
const { writeJson } = require('../json-file');
|
|
9
|
-
|
|
10
|
-
// ZCode 0.16.9 removed inline provider configuration. Its requestAuth override
|
|
11
|
-
// only applies to off-peak account models, not ordinary API-key providers.
|
|
12
|
-
// A private loopback transport keeps the upstream credential in memory; the
|
|
13
|
-
// native provider file contains only a short-lived local capability.
|
|
14
|
-
async function createZCodeProfileRuntime(spec) {
|
|
15
|
-
const upstream = new URL(spec.provider.baseURL);
|
|
16
|
-
if (!['http:', 'https:'].includes(upstream.protocol) || upstream.username || upstream.password || upstream.search || upstream.hash) throw new Error('ZCode 模型地址不支持内嵌凭据或查询参数。');
|
|
17
|
-
const key = spec.provider.apiKey.value;
|
|
18
|
-
const token = crypto.randomBytes(24).toString('hex');
|
|
19
|
-
const requests = new Set();
|
|
20
|
-
const prefix = `/${token}`;
|
|
21
|
-
const server = http.createServer((req, res) => {
|
|
22
|
-
const parsed = new URL(req.url, 'http://127.0.0.1');
|
|
23
|
-
const supplied = req.headers['x-api-key'] || String(req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
24
|
-
const suffix = parsed.pathname.slice(prefix.length);
|
|
25
|
-
if (supplied !== token || !parsed.pathname.startsWith(prefix + '/') || req.method !== 'POST' || !['/v1/messages', '/messages', '/chat/completions', '/v1/chat/completions', '/responses', '/v1/responses'].includes(suffix) || parsed.search) { req.resume(); res.writeHead(404); res.end(); return; }
|
|
26
|
-
const target = new URL(upstream);
|
|
27
|
-
target.pathname = upstream.pathname.replace(/\/$/, '') + suffix;
|
|
28
|
-
if (upstream.pathname.endsWith('/v1') && suffix.startsWith('/v1/')) target.pathname = upstream.pathname + suffix.slice(3);
|
|
29
|
-
const headers = { 'content-type': req.headers['content-type'] || 'application/json' };
|
|
30
|
-
for (const name of ['anthropic-version', 'anthropic-beta', 'content-length']) if (req.headers[name]) headers[name] = req.headers[name];
|
|
31
|
-
if (spec.provider.kind === 'anthropic') headers['x-api-key'] = key;
|
|
32
|
-
else headers.authorization = `Bearer ${key}`;
|
|
33
|
-
const remote = (target.protocol === 'https:' ? https : http).request(target, { method: 'POST', headers }, incoming => {
|
|
34
|
-
const allowed = {};
|
|
35
|
-
for (const name of ['content-type', 'cache-control', 'x-request-id', 'request-id']) if (incoming.headers[name]) allowed[name] = incoming.headers[name];
|
|
36
|
-
res.writeHead(incoming.statusCode, allowed); res.flushHeaders(); incoming.pipe(res);
|
|
37
|
-
});
|
|
38
|
-
requests.add(remote);
|
|
39
|
-
remote.once('close', () => requests.delete(remote));
|
|
40
|
-
remote.on('error', () => { if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' }); res.end('{"error":{"message":"模型连接失败"}}'); });
|
|
41
|
-
remote.setTimeout(300000, () => remote.destroy());
|
|
42
|
-
req.on('aborted', () => remote.destroy());
|
|
43
|
-
res.on('close', () => { if (!res.writableFinished) remote.destroy(); });
|
|
44
|
-
let size = 0;
|
|
45
|
-
req.on('data', bytes => { size += bytes.length; if (size > 8 * 1024 * 1024) { remote.destroy(); req.destroy(); } });
|
|
46
|
-
req.pipe(remote);
|
|
47
|
-
});
|
|
48
|
-
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
|
|
49
|
-
let root;
|
|
50
|
-
let file;
|
|
51
|
-
const selection = { ...spec.model, options: { reasoningLevel: 'enabled' } };
|
|
52
|
-
try {
|
|
53
|
-
root = fs.mkdtempSync(path.join(os.tmpdir(), 'workbench-zcode-runtime-'));
|
|
54
|
-
file = path.join(root, 'provider-config.json');
|
|
55
|
-
writeJson(file, { schemaVersion: 1, config: { providerOrder: [selection.providerId], defaultModelSelection: selection, providerConfigRules: { providerRules: [{ providerId: selection.providerId, providerName: 'Workbench', enabled: true, config: { group: 'standard-personal', access: { type: 'api-key', apiKey: token }, api: { type: spec.provider.kind === 'anthropic' ? 'anthropic-messages' : 'openai-chat-completions', baseUrl: `http://127.0.0.1:${server.address().port}${prefix}` }, personalModelIds: [selection.modelId] } }] }, modelConfigRules: { providerModelRules: [], manualProviderModelRules: [] } } });
|
|
56
|
-
} catch (error) {
|
|
57
|
-
await new Promise(resolve => server.close(resolve));
|
|
58
|
-
if (file) { fs.rmSync(file, { force: true }); fs.rmSync(`${file}.${process.pid}.tmp`, { force: true }); }
|
|
59
|
-
if (root) { try { fs.rmdirSync(root); } catch {} }
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
let closed = false;
|
|
63
|
-
return { selection, file, async close() {
|
|
64
|
-
if (closed) return; closed = true;
|
|
65
|
-
for (const request of requests) request.destroy();
|
|
66
|
-
server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
|
|
67
|
-
fs.rmSync(file, { force: true });
|
|
68
|
-
try { fs.rmdirSync(root); } catch { /* never recursively remove native-created files */ }
|
|
69
|
-
} };
|
|
70
|
-
}
|
|
71
|
-
module.exports = { createZCodeProfileRuntime };
|
|
1
|
+
'use strict';
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
const https = require('node:https');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const crypto = require('node:crypto');
|
|
8
|
+
const { writeJson } = require('../json-file');
|
|
9
|
+
|
|
10
|
+
// ZCode 0.16.9 removed inline provider configuration. Its requestAuth override
|
|
11
|
+
// only applies to off-peak account models, not ordinary API-key providers.
|
|
12
|
+
// A private loopback transport keeps the upstream credential in memory; the
|
|
13
|
+
// native provider file contains only a short-lived local capability.
|
|
14
|
+
async function createZCodeProfileRuntime(spec) {
|
|
15
|
+
const upstream = new URL(spec.provider.baseURL);
|
|
16
|
+
if (!['http:', 'https:'].includes(upstream.protocol) || upstream.username || upstream.password || upstream.search || upstream.hash) throw new Error('ZCode 模型地址不支持内嵌凭据或查询参数。');
|
|
17
|
+
const key = spec.provider.apiKey.value;
|
|
18
|
+
const token = crypto.randomBytes(24).toString('hex');
|
|
19
|
+
const requests = new Set();
|
|
20
|
+
const prefix = `/${token}`;
|
|
21
|
+
const server = http.createServer((req, res) => {
|
|
22
|
+
const parsed = new URL(req.url, 'http://127.0.0.1');
|
|
23
|
+
const supplied = req.headers['x-api-key'] || String(req.headers.authorization || '').replace(/^Bearer\s+/i, '');
|
|
24
|
+
const suffix = parsed.pathname.slice(prefix.length);
|
|
25
|
+
if (supplied !== token || !parsed.pathname.startsWith(prefix + '/') || req.method !== 'POST' || !['/v1/messages', '/messages', '/chat/completions', '/v1/chat/completions', '/responses', '/v1/responses'].includes(suffix) || parsed.search) { req.resume(); res.writeHead(404); res.end(); return; }
|
|
26
|
+
const target = new URL(upstream);
|
|
27
|
+
target.pathname = upstream.pathname.replace(/\/$/, '') + suffix;
|
|
28
|
+
if (upstream.pathname.endsWith('/v1') && suffix.startsWith('/v1/')) target.pathname = upstream.pathname + suffix.slice(3);
|
|
29
|
+
const headers = { 'content-type': req.headers['content-type'] || 'application/json' };
|
|
30
|
+
for (const name of ['anthropic-version', 'anthropic-beta', 'content-length']) if (req.headers[name]) headers[name] = req.headers[name];
|
|
31
|
+
if (spec.provider.kind === 'anthropic') headers['x-api-key'] = key;
|
|
32
|
+
else headers.authorization = `Bearer ${key}`;
|
|
33
|
+
const remote = (target.protocol === 'https:' ? https : http).request(target, { method: 'POST', headers }, incoming => {
|
|
34
|
+
const allowed = {};
|
|
35
|
+
for (const name of ['content-type', 'cache-control', 'x-request-id', 'request-id']) if (incoming.headers[name]) allowed[name] = incoming.headers[name];
|
|
36
|
+
res.writeHead(incoming.statusCode, allowed); res.flushHeaders(); incoming.pipe(res);
|
|
37
|
+
});
|
|
38
|
+
requests.add(remote);
|
|
39
|
+
remote.once('close', () => requests.delete(remote));
|
|
40
|
+
remote.on('error', () => { if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' }); res.end('{"error":{"message":"模型连接失败"}}'); });
|
|
41
|
+
remote.setTimeout(300000, () => remote.destroy());
|
|
42
|
+
req.on('aborted', () => remote.destroy());
|
|
43
|
+
res.on('close', () => { if (!res.writableFinished) remote.destroy(); });
|
|
44
|
+
let size = 0;
|
|
45
|
+
req.on('data', bytes => { size += bytes.length; if (size > 8 * 1024 * 1024) { remote.destroy(); req.destroy(); } });
|
|
46
|
+
req.pipe(remote);
|
|
47
|
+
});
|
|
48
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
|
|
49
|
+
let root;
|
|
50
|
+
let file;
|
|
51
|
+
const selection = { ...spec.model, options: { reasoningLevel: 'enabled' } };
|
|
52
|
+
try {
|
|
53
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'workbench-zcode-runtime-'));
|
|
54
|
+
file = path.join(root, 'provider-config.json');
|
|
55
|
+
writeJson(file, { schemaVersion: 1, config: { providerOrder: [selection.providerId], defaultModelSelection: selection, providerConfigRules: { providerRules: [{ providerId: selection.providerId, providerName: 'Workbench', enabled: true, config: { group: 'standard-personal', access: { type: 'api-key', apiKey: token }, api: { type: spec.provider.kind === 'anthropic' ? 'anthropic-messages' : 'openai-chat-completions', baseUrl: `http://127.0.0.1:${server.address().port}${prefix}` }, personalModelIds: [selection.modelId] } }] }, modelConfigRules: { providerModelRules: [], manualProviderModelRules: [] } } });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
await new Promise(resolve => server.close(resolve));
|
|
58
|
+
if (file) { fs.rmSync(file, { force: true }); fs.rmSync(`${file}.${process.pid}.tmp`, { force: true }); }
|
|
59
|
+
if (root) { try { fs.rmdirSync(root); } catch {} }
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
let closed = false;
|
|
63
|
+
return { selection, file, async close() {
|
|
64
|
+
if (closed) return; closed = true;
|
|
65
|
+
for (const request of requests) request.destroy();
|
|
66
|
+
server.closeAllConnections(); await new Promise(resolve => server.close(resolve));
|
|
67
|
+
fs.rmSync(file, { force: true });
|
|
68
|
+
try { fs.rmdirSync(root); } catch { /* never recursively remove native-created files */ }
|
|
69
|
+
} };
|
|
70
|
+
}
|
|
71
|
+
module.exports = { createZCodeProfileRuntime };
|
|
@@ -1,138 +1,138 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
function normalizeEvent(event, sessionId, contextId, sequence, startedAt) {
|
|
4
|
-
const input = event && typeof event === 'object' ? event : { type: 'claude/error', message: String(event) };
|
|
5
|
-
const own = Number(input.sequence);
|
|
6
|
-
return {
|
|
7
|
-
...input,
|
|
8
|
-
type: String(input.type || 'claude/state'),
|
|
9
|
-
sessionId,
|
|
10
|
-
contextId,
|
|
11
|
-
// Publisher-stamped events keep their stable identity; the passed-in
|
|
12
|
-
// number only positions buffers that were never stamped.
|
|
13
|
-
sequence: Number.isFinite(own) && own > 0 ? own : sequence,
|
|
14
|
-
// Real observation time when present; a missing timestamp falls back to
|
|
15
|
-
// the session start, never a synthetic startedAt+sequence offset.
|
|
16
|
-
at: input.at || new Date(Date.parse(startedAt || '') || Date.now()).toISOString(),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Publish-side identity: every emitted event gets the session's next
|
|
21
|
-
// monotonic sequence and, unless the producer recorded one, the observation
|
|
22
|
-
// time. The counter lives on the session, so tail compaction (dropping the
|
|
23
|
-
// oldest buffered events) never disturbs the numbering.
|
|
24
|
-
function stampEvent(session, event) {
|
|
25
|
-
const next = (Number(session.eventSequence) || 0) + 1;
|
|
26
|
-
session.eventSequence = next;
|
|
27
|
-
event.sequence = next;
|
|
28
|
-
if (!event.at) event.at = new Date().toISOString();
|
|
29
|
-
return event;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Records written before stable identity carry no sequence fields. The
|
|
33
|
-
// retained tail is explicitly renumbered 1..N at restore time and the counter
|
|
34
|
-
// starts at N: the unknown cumulative history is never guessed. Renumbering
|
|
35
|
-
// any event marks a legacy identity transition, which the session carries so
|
|
36
|
-
// subscribeNormalized can distrust pre-transition cursors.
|
|
37
|
-
function resequenceRestoredEvents(events, recordedHighWater) {
|
|
38
|
-
const list = Array.isArray(events) ? events.filter(item => item && typeof item === 'object') : [];
|
|
39
|
-
let sequence = 0;
|
|
40
|
-
let renumbered = 0;
|
|
41
|
-
const stamped = list.map(event => {
|
|
42
|
-
const own = Number(event.sequence);
|
|
43
|
-
if (Number.isFinite(own) && own > 0) {
|
|
44
|
-
sequence = Math.max(sequence, own);
|
|
45
|
-
return event;
|
|
46
|
-
}
|
|
47
|
-
renumbered += 1;
|
|
48
|
-
sequence += 1;
|
|
49
|
-
return { ...event, sequence };
|
|
50
|
-
});
|
|
51
|
-
const recorded = Number(recordedHighWater);
|
|
52
|
-
return {
|
|
53
|
-
events: stamped,
|
|
54
|
-
eventSequence: Math.max(sequence, Number.isFinite(recorded) ? recorded : 0),
|
|
55
|
-
legacyRenumbered: renumbered > 0,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function subscribeNormalized(runner, sessionId, afterSequence, onEvent) {
|
|
60
|
-
const state = runner.getState(sessionId);
|
|
61
|
-
if (!state) throw Object.assign(new Error('session not found'), { status: 404 });
|
|
62
|
-
const session = typeof runner.getSession === 'function' ? runner.getSession(sessionId) : null;
|
|
63
|
-
const requested = Number(afterSequence);
|
|
64
|
-
const after = Number.isFinite(requested) && requested > 0 ? Math.floor(requested) : 0;
|
|
65
|
-
// Number.isFinite(null) is false, so counters absent on unstamped runners
|
|
66
|
-
// never masquerade as a zero high-water mark.
|
|
67
|
-
const highWater = session && Number.isFinite(session.eventSequence)
|
|
68
|
-
? session.eventSequence : NaN;
|
|
69
|
-
const buffer = session && Array.isArray(session.outputBuffer) ? session.outputBuffer : [];
|
|
70
|
-
let firstRetained = 0;
|
|
71
|
-
for (const item of buffer) {
|
|
72
|
-
const own = Number(item && item.sequence);
|
|
73
|
-
if (Number.isFinite(own) && own > 0) { firstRetained = own; break; }
|
|
74
|
-
}
|
|
75
|
-
// Three cursors cannot be served incrementally and must resync explicitly:
|
|
76
|
-
// one from a numbering era that no longer exists (beyond the high-water
|
|
77
|
-
// mark: a restart renumbered an unstamped legacy tail, or history was
|
|
78
|
-
// cleared), one on a session whose retained tail was renumbered from
|
|
79
|
-
// unstamped legacy records — there ANY non-zero cursor could predate the
|
|
80
|
-
// transition, including ones at or below the high-water mark, and no
|
|
81
|
-
// numeric test can prove otherwise — and one the retained tail can no
|
|
82
|
-
// longer serve continuously (compaction dropped the events between the
|
|
83
|
-
// cursor and the tail). All open with a sequence-less control marker so
|
|
84
|
-
// clients reset their dedupe cursor before the full retained replay
|
|
85
|
-
// instead of silently skipping live events until a fresh local count
|
|
86
|
-
// catches up.
|
|
87
|
-
const staleEra = Number.isFinite(highWater) && after > highWater;
|
|
88
|
-
const legacyTransition = session ? session.legacySequenceReset === true : false;
|
|
89
|
-
const legacyReset = !staleEra && legacyTransition && after > 0;
|
|
90
|
-
const historyGap = Number.isFinite(highWater) && after > 0
|
|
91
|
-
&& firstRetained > 0 && (after + 1 < firstRetained
|
|
92
|
-
// A retained row that carries compaction provenance subsumes every
|
|
93
|
-
// sequence from compactFrom to its own: a cursor inside that span
|
|
94
|
-
// already rendered part of the fragment, so serving the merged row as
|
|
95
|
-
// a delta would duplicate text — the client must resync instead.
|
|
96
|
-
|| buffer.some(item => {
|
|
97
|
-
const from = Number(item && item.compactFrom);
|
|
98
|
-
const own = Number(item && item.sequence);
|
|
99
|
-
return Number.isSafeInteger(from) && from > 0 && Number.isSafeInteger(own) && from <= after && after < own;
|
|
100
|
-
}));
|
|
101
|
-
const cursor = staleEra || legacyReset || historyGap ? 0 : after;
|
|
102
|
-
if (staleEra || legacyReset || historyGap) {
|
|
103
|
-
onEvent({
|
|
104
|
-
type: 'workbench/resync',
|
|
105
|
-
sessionId,
|
|
106
|
-
contextId: state.projectSlug,
|
|
107
|
-
at: new Date().toISOString(),
|
|
108
|
-
reason: staleEra ? 'stale-cursor' : legacyReset ? 'legacy-reset' : 'history-gap',
|
|
109
|
-
from: after,
|
|
110
|
-
highWater,
|
|
111
|
-
firstSequence: firstRetained,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
let legacySequence = 0;
|
|
115
|
-
return runner.subscribe(sessionId, raw => {
|
|
116
|
-
const own = Number(raw && raw.sequence);
|
|
117
|
-
if (Number.isFinite(own) && own > 0) {
|
|
118
|
-
if (own <= cursor) return;
|
|
119
|
-
onEvent(normalizeEvent(raw, sessionId, state.projectSlug, own, state.startedAt));
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
// Unstamped runner events (in-memory fakes) keep the historical
|
|
123
|
-
// per-subscription counting contract.
|
|
124
|
-
legacySequence += 1;
|
|
125
|
-
if (legacySequence <= cursor) return;
|
|
126
|
-
onEvent(normalizeEvent(raw, sessionId, state.projectSlug, legacySequence, state.startedAt));
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function snapshotEvents(runner, sessionId) {
|
|
131
|
-
const session = runner.getSession(sessionId);
|
|
132
|
-
const state = runner.getState(sessionId);
|
|
133
|
-
if (!session || !state) return [];
|
|
134
|
-
return (session.outputBuffer || []).map((event, index) =>
|
|
135
|
-
normalizeEvent(event, sessionId, state.projectSlug, index + 1, state.startedAt));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
module.exports = { normalizeEvent, subscribeNormalized, snapshotEvents, stampEvent, resequenceRestoredEvents };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function normalizeEvent(event, sessionId, contextId, sequence, startedAt) {
|
|
4
|
+
const input = event && typeof event === 'object' ? event : { type: 'claude/error', message: String(event) };
|
|
5
|
+
const own = Number(input.sequence);
|
|
6
|
+
return {
|
|
7
|
+
...input,
|
|
8
|
+
type: String(input.type || 'claude/state'),
|
|
9
|
+
sessionId,
|
|
10
|
+
contextId,
|
|
11
|
+
// Publisher-stamped events keep their stable identity; the passed-in
|
|
12
|
+
// number only positions buffers that were never stamped.
|
|
13
|
+
sequence: Number.isFinite(own) && own > 0 ? own : sequence,
|
|
14
|
+
// Real observation time when present; a missing timestamp falls back to
|
|
15
|
+
// the session start, never a synthetic startedAt+sequence offset.
|
|
16
|
+
at: input.at || new Date(Date.parse(startedAt || '') || Date.now()).toISOString(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Publish-side identity: every emitted event gets the session's next
|
|
21
|
+
// monotonic sequence and, unless the producer recorded one, the observation
|
|
22
|
+
// time. The counter lives on the session, so tail compaction (dropping the
|
|
23
|
+
// oldest buffered events) never disturbs the numbering.
|
|
24
|
+
function stampEvent(session, event) {
|
|
25
|
+
const next = (Number(session.eventSequence) || 0) + 1;
|
|
26
|
+
session.eventSequence = next;
|
|
27
|
+
event.sequence = next;
|
|
28
|
+
if (!event.at) event.at = new Date().toISOString();
|
|
29
|
+
return event;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Records written before stable identity carry no sequence fields. The
|
|
33
|
+
// retained tail is explicitly renumbered 1..N at restore time and the counter
|
|
34
|
+
// starts at N: the unknown cumulative history is never guessed. Renumbering
|
|
35
|
+
// any event marks a legacy identity transition, which the session carries so
|
|
36
|
+
// subscribeNormalized can distrust pre-transition cursors.
|
|
37
|
+
function resequenceRestoredEvents(events, recordedHighWater) {
|
|
38
|
+
const list = Array.isArray(events) ? events.filter(item => item && typeof item === 'object') : [];
|
|
39
|
+
let sequence = 0;
|
|
40
|
+
let renumbered = 0;
|
|
41
|
+
const stamped = list.map(event => {
|
|
42
|
+
const own = Number(event.sequence);
|
|
43
|
+
if (Number.isFinite(own) && own > 0) {
|
|
44
|
+
sequence = Math.max(sequence, own);
|
|
45
|
+
return event;
|
|
46
|
+
}
|
|
47
|
+
renumbered += 1;
|
|
48
|
+
sequence += 1;
|
|
49
|
+
return { ...event, sequence };
|
|
50
|
+
});
|
|
51
|
+
const recorded = Number(recordedHighWater);
|
|
52
|
+
return {
|
|
53
|
+
events: stamped,
|
|
54
|
+
eventSequence: Math.max(sequence, Number.isFinite(recorded) ? recorded : 0),
|
|
55
|
+
legacyRenumbered: renumbered > 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function subscribeNormalized(runner, sessionId, afterSequence, onEvent) {
|
|
60
|
+
const state = runner.getState(sessionId);
|
|
61
|
+
if (!state) throw Object.assign(new Error('session not found'), { status: 404 });
|
|
62
|
+
const session = typeof runner.getSession === 'function' ? runner.getSession(sessionId) : null;
|
|
63
|
+
const requested = Number(afterSequence);
|
|
64
|
+
const after = Number.isFinite(requested) && requested > 0 ? Math.floor(requested) : 0;
|
|
65
|
+
// Number.isFinite(null) is false, so counters absent on unstamped runners
|
|
66
|
+
// never masquerade as a zero high-water mark.
|
|
67
|
+
const highWater = session && Number.isFinite(session.eventSequence)
|
|
68
|
+
? session.eventSequence : NaN;
|
|
69
|
+
const buffer = session && Array.isArray(session.outputBuffer) ? session.outputBuffer : [];
|
|
70
|
+
let firstRetained = 0;
|
|
71
|
+
for (const item of buffer) {
|
|
72
|
+
const own = Number(item && item.sequence);
|
|
73
|
+
if (Number.isFinite(own) && own > 0) { firstRetained = own; break; }
|
|
74
|
+
}
|
|
75
|
+
// Three cursors cannot be served incrementally and must resync explicitly:
|
|
76
|
+
// one from a numbering era that no longer exists (beyond the high-water
|
|
77
|
+
// mark: a restart renumbered an unstamped legacy tail, or history was
|
|
78
|
+
// cleared), one on a session whose retained tail was renumbered from
|
|
79
|
+
// unstamped legacy records — there ANY non-zero cursor could predate the
|
|
80
|
+
// transition, including ones at or below the high-water mark, and no
|
|
81
|
+
// numeric test can prove otherwise — and one the retained tail can no
|
|
82
|
+
// longer serve continuously (compaction dropped the events between the
|
|
83
|
+
// cursor and the tail). All open with a sequence-less control marker so
|
|
84
|
+
// clients reset their dedupe cursor before the full retained replay
|
|
85
|
+
// instead of silently skipping live events until a fresh local count
|
|
86
|
+
// catches up.
|
|
87
|
+
const staleEra = Number.isFinite(highWater) && after > highWater;
|
|
88
|
+
const legacyTransition = session ? session.legacySequenceReset === true : false;
|
|
89
|
+
const legacyReset = !staleEra && legacyTransition && after > 0;
|
|
90
|
+
const historyGap = Number.isFinite(highWater) && after > 0
|
|
91
|
+
&& firstRetained > 0 && (after + 1 < firstRetained
|
|
92
|
+
// A retained row that carries compaction provenance subsumes every
|
|
93
|
+
// sequence from compactFrom to its own: a cursor inside that span
|
|
94
|
+
// already rendered part of the fragment, so serving the merged row as
|
|
95
|
+
// a delta would duplicate text — the client must resync instead.
|
|
96
|
+
|| buffer.some(item => {
|
|
97
|
+
const from = Number(item && item.compactFrom);
|
|
98
|
+
const own = Number(item && item.sequence);
|
|
99
|
+
return Number.isSafeInteger(from) && from > 0 && Number.isSafeInteger(own) && from <= after && after < own;
|
|
100
|
+
}));
|
|
101
|
+
const cursor = staleEra || legacyReset || historyGap ? 0 : after;
|
|
102
|
+
if (staleEra || legacyReset || historyGap) {
|
|
103
|
+
onEvent({
|
|
104
|
+
type: 'workbench/resync',
|
|
105
|
+
sessionId,
|
|
106
|
+
contextId: state.projectSlug,
|
|
107
|
+
at: new Date().toISOString(),
|
|
108
|
+
reason: staleEra ? 'stale-cursor' : legacyReset ? 'legacy-reset' : 'history-gap',
|
|
109
|
+
from: after,
|
|
110
|
+
highWater,
|
|
111
|
+
firstSequence: firstRetained,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
let legacySequence = 0;
|
|
115
|
+
return runner.subscribe(sessionId, raw => {
|
|
116
|
+
const own = Number(raw && raw.sequence);
|
|
117
|
+
if (Number.isFinite(own) && own > 0) {
|
|
118
|
+
if (own <= cursor) return;
|
|
119
|
+
onEvent(normalizeEvent(raw, sessionId, state.projectSlug, own, state.startedAt));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Unstamped runner events (in-memory fakes) keep the historical
|
|
123
|
+
// per-subscription counting contract.
|
|
124
|
+
legacySequence += 1;
|
|
125
|
+
if (legacySequence <= cursor) return;
|
|
126
|
+
onEvent(normalizeEvent(raw, sessionId, state.projectSlug, legacySequence, state.startedAt));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function snapshotEvents(runner, sessionId) {
|
|
131
|
+
const session = runner.getSession(sessionId);
|
|
132
|
+
const state = runner.getState(sessionId);
|
|
133
|
+
if (!session || !state) return [];
|
|
134
|
+
return (session.outputBuffer || []).map((event, index) =>
|
|
135
|
+
normalizeEvent(event, sessionId, state.projectSlug, index + 1, state.startedAt));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { normalizeEvent, subscribeNormalized, snapshotEvents, stampEvent, resequenceRestoredEvents };
|