@sanqianx/claude-ai-workbench 0.13.2 → 0.13.3

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