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