@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,242 +1,242 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Native task runtime - the per-session coordinator that feeds verified SDK
|
|
4
|
-
// stream messages and sanitized native-trace hook events into the audited
|
|
5
|
-
// NativeTaskRegistry and projects single-task claude/native-task-state events
|
|
6
|
-
// through the session's existing emit path. It owns no transport and decides
|
|
7
|
-
// no ownership: the registry remains the only place facts are judged, and
|
|
8
|
-
// this module only observes it. The runtime is created once per workbench
|
|
9
|
-
// session (never per input), so a resident background query keeps its task
|
|
10
|
-
// owner while later inputs stream into the same session.
|
|
11
|
-
|
|
12
|
-
const { NativeTaskRegistry } = require('./native-task-registry');
|
|
13
|
-
|
|
14
|
-
// Only state a consumer can act on gates a new projection: observedEvents
|
|
15
|
-
// counters and diagnostic churn must not re-emit an unchanged task.
|
|
16
|
-
function semanticKey(task) {
|
|
17
|
-
return JSON.stringify([
|
|
18
|
-
task.association, task.status, task.statusSource, task.taskKind,
|
|
19
|
-
task.taskType, task.typeConflict, task.toolUseId, task.ownerRunId, task.taskRef,
|
|
20
|
-
]);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Minimal serializable per-task state for persistence.
|
|
24
|
-
function minimalTask(task) {
|
|
25
|
-
return {
|
|
26
|
-
sdkSessionId: task.sdkSessionId,
|
|
27
|
-
taskId: task.taskId,
|
|
28
|
-
toolUseId: task.toolUseId,
|
|
29
|
-
status: task.status,
|
|
30
|
-
statusSource: task.statusSource,
|
|
31
|
-
taskKind: task.taskKind,
|
|
32
|
-
taskType: task.taskType,
|
|
33
|
-
typeConflict: task.typeConflict === true,
|
|
34
|
-
association: task.association,
|
|
35
|
-
taskRef: task.taskRef,
|
|
36
|
-
ownerRunId: task.ownerRunId,
|
|
37
|
-
owner: task.owner,
|
|
38
|
-
mainToolUseId: task.mainToolUseId,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Fresh copies only: no consumer of scope(), snapshot() or an emitted event
|
|
43
|
-
// ever holds the internal fixed scope (or a task inside the persisted
|
|
44
|
-
// history) by reference, so external mutation cannot rewrite later output.
|
|
45
|
-
function copyScope(scope) {
|
|
46
|
-
return { storeScope: scope.storeScope, contextId: scope.contextId, parentSessionId: scope.parentSessionId };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function projection(scope, task, reason) {
|
|
50
|
-
return {
|
|
51
|
-
type: 'claude/native-task-state',
|
|
52
|
-
scope: copyScope(scope),
|
|
53
|
-
sdkSessionId: task.sdkSessionId,
|
|
54
|
-
taskId: task.taskId,
|
|
55
|
-
toolUseId: task.toolUseId,
|
|
56
|
-
taskKind: task.taskKind,
|
|
57
|
-
taskType: task.taskType,
|
|
58
|
-
status: task.status,
|
|
59
|
-
statusSource: task.statusSource,
|
|
60
|
-
association: task.association,
|
|
61
|
-
typeConflict: task.typeConflict === true,
|
|
62
|
-
taskRef: task.taskRef,
|
|
63
|
-
ownerRunId: task.ownerRunId,
|
|
64
|
-
owner: task.owner,
|
|
65
|
-
reason,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function clone(task) { return JSON.parse(JSON.stringify(task)); }
|
|
70
|
-
|
|
71
|
-
function createNativeTaskRuntime({ scope, emit, restoredTasks } = {}) {
|
|
72
|
-
if (typeof emit !== 'function') throw new Error('createNativeTaskRuntime requires an emit function');
|
|
73
|
-
// The registry constructor validates the scope; an invalid scope throws,
|
|
74
|
-
// which the runner reports as an honest attach failure instead of
|
|
75
|
-
// inventing a weaker fallback. The frozen internal copy is the only
|
|
76
|
-
// storage - the caller's object is never kept by reference.
|
|
77
|
-
const registry = new NativeTaskRegistry(scope);
|
|
78
|
-
const fixedScope = Object.freeze(copyScope(registry.scope));
|
|
79
|
-
// Restored native tasks are inert persistent history: deep-copied at
|
|
80
|
-
// attach, never fed into the live registry as running facts, never
|
|
81
|
-
// re-emitted as new live events, and never overwritten by the live view.
|
|
82
|
-
const restored = restoredTasks && Array.isArray(restoredTasks.tasks)
|
|
83
|
-
? restoredTasks.tasks.filter(task => task && typeof task === 'object').map(clone)
|
|
84
|
-
: [];
|
|
85
|
-
const restoredKeys = new Set(restored
|
|
86
|
-
.filter(task => typeof task.sdkSessionId === 'string' && typeof task.taskId === 'string')
|
|
87
|
-
.map(task => JSON.stringify([task.sdkSessionId, task.taskId])));
|
|
88
|
-
const lastProjected = new Map();
|
|
89
|
-
// Sticky unavailable state: once native history publishing has failed, no
|
|
90
|
-
// new projections happen and live tasks stop being controllable. A clean
|
|
91
|
-
// detach from the Service binding sets a NON-sticky unavailable that
|
|
92
|
-
// resumeFromDetach() clears when the session re-binds to a committer.
|
|
93
|
-
let unavailable = null;
|
|
94
|
-
// Delivered-projection state per task: the content of the last projection
|
|
95
|
-
// that cleared the durable gate, and whether the task's LATEST projection
|
|
96
|
-
// attempt was delivered. A refusal (emit returned false) keeps the older
|
|
97
|
-
// content but revokes the durable claim, so no unproven fact ever reaches
|
|
98
|
-
// a public view (snapshot, live lookup) - the registry stays a raw index,
|
|
99
|
-
// never the delivered truth.
|
|
100
|
-
const delivered = new Map();
|
|
101
|
-
|
|
102
|
-
const project = reason => {
|
|
103
|
-
if (unavailable) return;
|
|
104
|
-
for (const task of registry.list()) {
|
|
105
|
-
const key = JSON.stringify([task.sdkSessionId, task.taskId]);
|
|
106
|
-
// A task key already present in restored history stays historical: a
|
|
107
|
-
// late started for it must not surface as a running live fact.
|
|
108
|
-
if (restoredKeys.has(key)) continue;
|
|
109
|
-
const semantic = semanticKey(task);
|
|
110
|
-
if (lastProjected.get(key) === semantic) continue;
|
|
111
|
-
const event = projection(fixedScope, task, reason);
|
|
112
|
-
// Stage the delivered view BEFORE the emit: the emit path itself
|
|
113
|
-
// persists snapshots (the runner's per-event record), and that
|
|
114
|
-
// mid-emit snapshot must show exactly the fact being broadcast. The
|
|
115
|
-
// staged copy is deep and shares nothing with the event object, so a
|
|
116
|
-
// consumer mutating the event cannot rewrite it. A refusal or a throw
|
|
117
|
-
// rolls back to the prior content with the durable claim revoked - a
|
|
118
|
-
// durable-gate refusal is never recorded as delivered, and the same
|
|
119
|
-
// real fact retries next time.
|
|
120
|
-
const prior = delivered.get(key) || null;
|
|
121
|
-
delivered.set(key, { task: clone(minimalTask(task)), durable: true });
|
|
122
|
-
let ok = false;
|
|
123
|
-
try {
|
|
124
|
-
ok = emit(event);
|
|
125
|
-
} finally {
|
|
126
|
-
if (ok === false) {
|
|
127
|
-
if (prior) delivered.set(key, { task: prior.task, durable: false });
|
|
128
|
-
else delivered.delete(key);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (ok === false) return;
|
|
132
|
-
lastProjected.set(key, semantic);
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
scope: () => copyScope(fixedScope),
|
|
138
|
-
// Read-only live lookup by canonical NativeTaskRef for trusted
|
|
139
|
-
// coordinators (first-stage targeted stop): only the CURRENT registry
|
|
140
|
-
// participates and restored history never becomes live
|
|
141
|
-
// controllability - a historical key stays excluded even if live facts
|
|
142
|
-
// later re-bind it. Returns a fresh deep copy, or null for any invalid,
|
|
143
|
-
// foreign, unbound or unknown ref - never an error leaking internals.
|
|
144
|
-
getLiveTask(ref) {
|
|
145
|
-
if (unavailable) return null;
|
|
146
|
-
try {
|
|
147
|
-
const task = registry.getTask(ref);
|
|
148
|
-
if (!task) return null;
|
|
149
|
-
const key = JSON.stringify([task.sdkSessionId, task.taskId]);
|
|
150
|
-
if (restoredKeys.has(key)) return null;
|
|
151
|
-
const state = delivered.get(key);
|
|
152
|
-
return state ? clone(state.task) : null;
|
|
153
|
-
} catch (error) {
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
156
|
-
},
|
|
157
|
-
// Narrow real-commit-status query for trusted control coordinators:
|
|
158
|
-
// true only for a task whose latest semantic projection was actually
|
|
159
|
-
// delivered through a successful durable gate. No default true.
|
|
160
|
-
isTaskDurable(ref) {
|
|
161
|
-
if (unavailable) return false;
|
|
162
|
-
try {
|
|
163
|
-
const task = registry.getTask(ref);
|
|
164
|
-
if (!task) return false;
|
|
165
|
-
const state = delivered.get(JSON.stringify([task.sdkSessionId, task.taskId]));
|
|
166
|
-
return state ? state.durable === true : false;
|
|
167
|
-
} catch (error) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
// Marks native history publishing unavailable: sticky (default) after a
|
|
172
|
-
// real durable failure - projections stop, live tasks stop being
|
|
173
|
-
// controllable - or non-sticky for a clean Service detach so a rebind
|
|
174
|
-
// can resume live projection. A sticky failure is terminal for this
|
|
175
|
-
// runtime instance: a later clean detach must not downgrade it to a
|
|
176
|
-
// recoverable state, and the rebind's resumeFromDetach cannot clear it.
|
|
177
|
-
// The first sticky reason stays the honest record of what failed.
|
|
178
|
-
markUnavailable(reason, { sticky = true } = {}) {
|
|
179
|
-
if (unavailable && unavailable.sticky) return;
|
|
180
|
-
unavailable = { reason: String(reason || 'unknown'), sticky: sticky === true, at: new Date().toISOString() };
|
|
181
|
-
},
|
|
182
|
-
// Clears a non-sticky (detach) unavailable state after the runtime
|
|
183
|
-
// re-bound to a trusted committer; a sticky failure stays for good.
|
|
184
|
-
resumeFromDetach() {
|
|
185
|
-
if (unavailable && !unavailable.sticky) unavailable = null;
|
|
186
|
-
},
|
|
187
|
-
// Raw SDK stream message (the same object the product stream already
|
|
188
|
-
// normalized and emitted); recorded after that path, then projected.
|
|
189
|
-
// Returns false when unavailable so callers know facts were dropped.
|
|
190
|
-
observeSdkMessage(message) {
|
|
191
|
-
if (unavailable) return false;
|
|
192
|
-
registry.observeSdkMessage(message);
|
|
193
|
-
project('sdk-message');
|
|
194
|
-
return true;
|
|
195
|
-
},
|
|
196
|
-
// Sanitized claude/native-trace event (already emitted as-is); recorded
|
|
197
|
-
// after the trace itself, then projected. Returns false when
|
|
198
|
-
// unavailable so callers know facts were dropped.
|
|
199
|
-
observeTrace(trace) {
|
|
200
|
-
if (unavailable) return false;
|
|
201
|
-
registry.observeTrace(trace);
|
|
202
|
-
project('trace');
|
|
203
|
-
return true;
|
|
204
|
-
},
|
|
205
|
-
// Narrow trusted read-only binding query for the transcript collector:
|
|
206
|
-
// the registry's own bound agent identities, as fresh copies. Empty when
|
|
207
|
-
// unavailable - an unavailable runtime must not feed new reads.
|
|
208
|
-
agentTranscriptBindings() {
|
|
209
|
-
if (unavailable) return [];
|
|
210
|
-
return registry.agentBindings();
|
|
211
|
-
},
|
|
212
|
-
// Minimal serializable state for session persistence: restored history
|
|
213
|
-
// first (verbatim, terminal and restart statuses preserved), then live
|
|
214
|
-
// tasks whose key does not collide with history - both coexist and
|
|
215
|
-
// neither overwrites the other. Live rows carry a `durable` flag true
|
|
216
|
-
// only for delivered gated projections, and a sticky unavailable is
|
|
217
|
-
// persisted so a restored session never resumes as if history were
|
|
218
|
-
// healthy. Returned copies share nothing with the internal state.
|
|
219
|
-
snapshot() {
|
|
220
|
-
// Live rows serve the delivered view: a task whose latest projection
|
|
221
|
-
// was refused keeps its last proven content with a revoked durable
|
|
222
|
-
// flag; a task never delivered at all appears with durable false and
|
|
223
|
-
// never with unproven facts.
|
|
224
|
-
const live = registry.list()
|
|
225
|
-
.map(task => ({ task, key: JSON.stringify([task.sdkSessionId, task.taskId]) }))
|
|
226
|
-
.filter(({ key }) => !restoredKeys.has(key))
|
|
227
|
-
.map(({ task, key }) => {
|
|
228
|
-
const state = delivered.get(key);
|
|
229
|
-
return state
|
|
230
|
-
? Object.assign(clone(state.task), { durable: state.durable })
|
|
231
|
-
: Object.assign(minimalTask(task), { durable: false });
|
|
232
|
-
});
|
|
233
|
-
return Object.assign(
|
|
234
|
-
{ version: 1, scope: copyScope(fixedScope) },
|
|
235
|
-
unavailable ? { unavailable: { reason: unavailable.reason, sticky: unavailable.sticky, at: unavailable.at } } : {},
|
|
236
|
-
{ tasks: restored.map(clone).concat(live) }
|
|
237
|
-
);
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
module.exports = { createNativeTaskRuntime };
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Native task runtime - the per-session coordinator that feeds verified SDK
|
|
4
|
+
// stream messages and sanitized native-trace hook events into the audited
|
|
5
|
+
// NativeTaskRegistry and projects single-task claude/native-task-state events
|
|
6
|
+
// through the session's existing emit path. It owns no transport and decides
|
|
7
|
+
// no ownership: the registry remains the only place facts are judged, and
|
|
8
|
+
// this module only observes it. The runtime is created once per workbench
|
|
9
|
+
// session (never per input), so a resident background query keeps its task
|
|
10
|
+
// owner while later inputs stream into the same session.
|
|
11
|
+
|
|
12
|
+
const { NativeTaskRegistry } = require('./native-task-registry');
|
|
13
|
+
|
|
14
|
+
// Only state a consumer can act on gates a new projection: observedEvents
|
|
15
|
+
// counters and diagnostic churn must not re-emit an unchanged task.
|
|
16
|
+
function semanticKey(task) {
|
|
17
|
+
return JSON.stringify([
|
|
18
|
+
task.association, task.status, task.statusSource, task.taskKind,
|
|
19
|
+
task.taskType, task.typeConflict, task.toolUseId, task.ownerRunId, task.taskRef,
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Minimal serializable per-task state for persistence.
|
|
24
|
+
function minimalTask(task) {
|
|
25
|
+
return {
|
|
26
|
+
sdkSessionId: task.sdkSessionId,
|
|
27
|
+
taskId: task.taskId,
|
|
28
|
+
toolUseId: task.toolUseId,
|
|
29
|
+
status: task.status,
|
|
30
|
+
statusSource: task.statusSource,
|
|
31
|
+
taskKind: task.taskKind,
|
|
32
|
+
taskType: task.taskType,
|
|
33
|
+
typeConflict: task.typeConflict === true,
|
|
34
|
+
association: task.association,
|
|
35
|
+
taskRef: task.taskRef,
|
|
36
|
+
ownerRunId: task.ownerRunId,
|
|
37
|
+
owner: task.owner,
|
|
38
|
+
mainToolUseId: task.mainToolUseId,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Fresh copies only: no consumer of scope(), snapshot() or an emitted event
|
|
43
|
+
// ever holds the internal fixed scope (or a task inside the persisted
|
|
44
|
+
// history) by reference, so external mutation cannot rewrite later output.
|
|
45
|
+
function copyScope(scope) {
|
|
46
|
+
return { storeScope: scope.storeScope, contextId: scope.contextId, parentSessionId: scope.parentSessionId };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function projection(scope, task, reason) {
|
|
50
|
+
return {
|
|
51
|
+
type: 'claude/native-task-state',
|
|
52
|
+
scope: copyScope(scope),
|
|
53
|
+
sdkSessionId: task.sdkSessionId,
|
|
54
|
+
taskId: task.taskId,
|
|
55
|
+
toolUseId: task.toolUseId,
|
|
56
|
+
taskKind: task.taskKind,
|
|
57
|
+
taskType: task.taskType,
|
|
58
|
+
status: task.status,
|
|
59
|
+
statusSource: task.statusSource,
|
|
60
|
+
association: task.association,
|
|
61
|
+
typeConflict: task.typeConflict === true,
|
|
62
|
+
taskRef: task.taskRef,
|
|
63
|
+
ownerRunId: task.ownerRunId,
|
|
64
|
+
owner: task.owner,
|
|
65
|
+
reason,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clone(task) { return JSON.parse(JSON.stringify(task)); }
|
|
70
|
+
|
|
71
|
+
function createNativeTaskRuntime({ scope, emit, restoredTasks } = {}) {
|
|
72
|
+
if (typeof emit !== 'function') throw new Error('createNativeTaskRuntime requires an emit function');
|
|
73
|
+
// The registry constructor validates the scope; an invalid scope throws,
|
|
74
|
+
// which the runner reports as an honest attach failure instead of
|
|
75
|
+
// inventing a weaker fallback. The frozen internal copy is the only
|
|
76
|
+
// storage - the caller's object is never kept by reference.
|
|
77
|
+
const registry = new NativeTaskRegistry(scope);
|
|
78
|
+
const fixedScope = Object.freeze(copyScope(registry.scope));
|
|
79
|
+
// Restored native tasks are inert persistent history: deep-copied at
|
|
80
|
+
// attach, never fed into the live registry as running facts, never
|
|
81
|
+
// re-emitted as new live events, and never overwritten by the live view.
|
|
82
|
+
const restored = restoredTasks && Array.isArray(restoredTasks.tasks)
|
|
83
|
+
? restoredTasks.tasks.filter(task => task && typeof task === 'object').map(clone)
|
|
84
|
+
: [];
|
|
85
|
+
const restoredKeys = new Set(restored
|
|
86
|
+
.filter(task => typeof task.sdkSessionId === 'string' && typeof task.taskId === 'string')
|
|
87
|
+
.map(task => JSON.stringify([task.sdkSessionId, task.taskId])));
|
|
88
|
+
const lastProjected = new Map();
|
|
89
|
+
// Sticky unavailable state: once native history publishing has failed, no
|
|
90
|
+
// new projections happen and live tasks stop being controllable. A clean
|
|
91
|
+
// detach from the Service binding sets a NON-sticky unavailable that
|
|
92
|
+
// resumeFromDetach() clears when the session re-binds to a committer.
|
|
93
|
+
let unavailable = null;
|
|
94
|
+
// Delivered-projection state per task: the content of the last projection
|
|
95
|
+
// that cleared the durable gate, and whether the task's LATEST projection
|
|
96
|
+
// attempt was delivered. A refusal (emit returned false) keeps the older
|
|
97
|
+
// content but revokes the durable claim, so no unproven fact ever reaches
|
|
98
|
+
// a public view (snapshot, live lookup) - the registry stays a raw index,
|
|
99
|
+
// never the delivered truth.
|
|
100
|
+
const delivered = new Map();
|
|
101
|
+
|
|
102
|
+
const project = reason => {
|
|
103
|
+
if (unavailable) return;
|
|
104
|
+
for (const task of registry.list()) {
|
|
105
|
+
const key = JSON.stringify([task.sdkSessionId, task.taskId]);
|
|
106
|
+
// A task key already present in restored history stays historical: a
|
|
107
|
+
// late started for it must not surface as a running live fact.
|
|
108
|
+
if (restoredKeys.has(key)) continue;
|
|
109
|
+
const semantic = semanticKey(task);
|
|
110
|
+
if (lastProjected.get(key) === semantic) continue;
|
|
111
|
+
const event = projection(fixedScope, task, reason);
|
|
112
|
+
// Stage the delivered view BEFORE the emit: the emit path itself
|
|
113
|
+
// persists snapshots (the runner's per-event record), and that
|
|
114
|
+
// mid-emit snapshot must show exactly the fact being broadcast. The
|
|
115
|
+
// staged copy is deep and shares nothing with the event object, so a
|
|
116
|
+
// consumer mutating the event cannot rewrite it. A refusal or a throw
|
|
117
|
+
// rolls back to the prior content with the durable claim revoked - a
|
|
118
|
+
// durable-gate refusal is never recorded as delivered, and the same
|
|
119
|
+
// real fact retries next time.
|
|
120
|
+
const prior = delivered.get(key) || null;
|
|
121
|
+
delivered.set(key, { task: clone(minimalTask(task)), durable: true });
|
|
122
|
+
let ok = false;
|
|
123
|
+
try {
|
|
124
|
+
ok = emit(event);
|
|
125
|
+
} finally {
|
|
126
|
+
if (ok === false) {
|
|
127
|
+
if (prior) delivered.set(key, { task: prior.task, durable: false });
|
|
128
|
+
else delivered.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (ok === false) return;
|
|
132
|
+
lastProjected.set(key, semantic);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
scope: () => copyScope(fixedScope),
|
|
138
|
+
// Read-only live lookup by canonical NativeTaskRef for trusted
|
|
139
|
+
// coordinators (first-stage targeted stop): only the CURRENT registry
|
|
140
|
+
// participates and restored history never becomes live
|
|
141
|
+
// controllability - a historical key stays excluded even if live facts
|
|
142
|
+
// later re-bind it. Returns a fresh deep copy, or null for any invalid,
|
|
143
|
+
// foreign, unbound or unknown ref - never an error leaking internals.
|
|
144
|
+
getLiveTask(ref) {
|
|
145
|
+
if (unavailable) return null;
|
|
146
|
+
try {
|
|
147
|
+
const task = registry.getTask(ref);
|
|
148
|
+
if (!task) return null;
|
|
149
|
+
const key = JSON.stringify([task.sdkSessionId, task.taskId]);
|
|
150
|
+
if (restoredKeys.has(key)) return null;
|
|
151
|
+
const state = delivered.get(key);
|
|
152
|
+
return state ? clone(state.task) : null;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
// Narrow real-commit-status query for trusted control coordinators:
|
|
158
|
+
// true only for a task whose latest semantic projection was actually
|
|
159
|
+
// delivered through a successful durable gate. No default true.
|
|
160
|
+
isTaskDurable(ref) {
|
|
161
|
+
if (unavailable) return false;
|
|
162
|
+
try {
|
|
163
|
+
const task = registry.getTask(ref);
|
|
164
|
+
if (!task) return false;
|
|
165
|
+
const state = delivered.get(JSON.stringify([task.sdkSessionId, task.taskId]));
|
|
166
|
+
return state ? state.durable === true : false;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
// Marks native history publishing unavailable: sticky (default) after a
|
|
172
|
+
// real durable failure - projections stop, live tasks stop being
|
|
173
|
+
// controllable - or non-sticky for a clean Service detach so a rebind
|
|
174
|
+
// can resume live projection. A sticky failure is terminal for this
|
|
175
|
+
// runtime instance: a later clean detach must not downgrade it to a
|
|
176
|
+
// recoverable state, and the rebind's resumeFromDetach cannot clear it.
|
|
177
|
+
// The first sticky reason stays the honest record of what failed.
|
|
178
|
+
markUnavailable(reason, { sticky = true } = {}) {
|
|
179
|
+
if (unavailable && unavailable.sticky) return;
|
|
180
|
+
unavailable = { reason: String(reason || 'unknown'), sticky: sticky === true, at: new Date().toISOString() };
|
|
181
|
+
},
|
|
182
|
+
// Clears a non-sticky (detach) unavailable state after the runtime
|
|
183
|
+
// re-bound to a trusted committer; a sticky failure stays for good.
|
|
184
|
+
resumeFromDetach() {
|
|
185
|
+
if (unavailable && !unavailable.sticky) unavailable = null;
|
|
186
|
+
},
|
|
187
|
+
// Raw SDK stream message (the same object the product stream already
|
|
188
|
+
// normalized and emitted); recorded after that path, then projected.
|
|
189
|
+
// Returns false when unavailable so callers know facts were dropped.
|
|
190
|
+
observeSdkMessage(message) {
|
|
191
|
+
if (unavailable) return false;
|
|
192
|
+
registry.observeSdkMessage(message);
|
|
193
|
+
project('sdk-message');
|
|
194
|
+
return true;
|
|
195
|
+
},
|
|
196
|
+
// Sanitized claude/native-trace event (already emitted as-is); recorded
|
|
197
|
+
// after the trace itself, then projected. Returns false when
|
|
198
|
+
// unavailable so callers know facts were dropped.
|
|
199
|
+
observeTrace(trace) {
|
|
200
|
+
if (unavailable) return false;
|
|
201
|
+
registry.observeTrace(trace);
|
|
202
|
+
project('trace');
|
|
203
|
+
return true;
|
|
204
|
+
},
|
|
205
|
+
// Narrow trusted read-only binding query for the transcript collector:
|
|
206
|
+
// the registry's own bound agent identities, as fresh copies. Empty when
|
|
207
|
+
// unavailable - an unavailable runtime must not feed new reads.
|
|
208
|
+
agentTranscriptBindings() {
|
|
209
|
+
if (unavailable) return [];
|
|
210
|
+
return registry.agentBindings();
|
|
211
|
+
},
|
|
212
|
+
// Minimal serializable state for session persistence: restored history
|
|
213
|
+
// first (verbatim, terminal and restart statuses preserved), then live
|
|
214
|
+
// tasks whose key does not collide with history - both coexist and
|
|
215
|
+
// neither overwrites the other. Live rows carry a `durable` flag true
|
|
216
|
+
// only for delivered gated projections, and a sticky unavailable is
|
|
217
|
+
// persisted so a restored session never resumes as if history were
|
|
218
|
+
// healthy. Returned copies share nothing with the internal state.
|
|
219
|
+
snapshot() {
|
|
220
|
+
// Live rows serve the delivered view: a task whose latest projection
|
|
221
|
+
// was refused keeps its last proven content with a revoked durable
|
|
222
|
+
// flag; a task never delivered at all appears with durable false and
|
|
223
|
+
// never with unproven facts.
|
|
224
|
+
const live = registry.list()
|
|
225
|
+
.map(task => ({ task, key: JSON.stringify([task.sdkSessionId, task.taskId]) }))
|
|
226
|
+
.filter(({ key }) => !restoredKeys.has(key))
|
|
227
|
+
.map(({ task, key }) => {
|
|
228
|
+
const state = delivered.get(key);
|
|
229
|
+
return state
|
|
230
|
+
? Object.assign(clone(state.task), { durable: state.durable })
|
|
231
|
+
: Object.assign(minimalTask(task), { durable: false });
|
|
232
|
+
});
|
|
233
|
+
return Object.assign(
|
|
234
|
+
{ version: 1, scope: copyScope(fixedScope) },
|
|
235
|
+
unavailable ? { unavailable: { reason: unavailable.reason, sticky: unavailable.sticky, at: unavailable.at } } : {},
|
|
236
|
+
{ tasks: restored.map(clone).concat(live) }
|
|
237
|
+
);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = { createNativeTaskRuntime };
|