@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,217 +1,217 @@
1
- 'use strict';
2
-
3
- // Native task control - the trusted first-stage coordinator deciding
4
- // whether ONE native background task may receive a targeted stop through a
5
- // CURRENT SDK query lease. It owns no transport and invents no facts: the
6
- // runner supplies three explicit trusted synchronous callbacks (live task
7
- // lookup, SDK query lease lookup, durability verdict) and this module only
8
- // combines their answers into one short receipt per stop request. Every
9
- // non-satisfiable case is an explicit rejection code - there is
10
- // deliberately NO fallback to session.abort, transport.close or stopping
11
- // the parent session. Receipts carry only fixed safe codes, short detail
12
- // tags and identity fields: upstream exception text is never copied into
13
- // a receipt (truncation is not redaction), a callback that throws or
14
- // answers asynchronously resolves an explicit unavailable receipt instead
15
- // of rejecting the stop() promise, and the task a lookup returns is
16
- // verified against the requested ref (taskId, ownerRunId, taskRef, SDK
17
- // session, kind/type) before any SDK call, so one broken lookup can never
18
- // widen into stopping a different task. This is not yet a product stop
19
- // API: runner, Service and API wiring is a later stage; unit tests use
20
- // fixtures and are not a real-SDK proof.
21
-
22
- const { createTaskScope, createNativeTaskRef, sameTaskScope } = require('./task-identity');
23
-
24
- const CONTROLLED_KINDS = new Set(['agent', 'local_bash']);
25
- // Mirrors TASK_KIND_BY_TYPE in the registry: a bound task's kind is
26
- // derived from its type there, so an inconsistent kind/type pair here can
27
- // only come from a broken lookup and must never reach the SDK.
28
- const KIND_BY_TYPE = new Map([['local_agent', 'agent'], ['local_bash', 'local_bash']]);
29
- const STOPPABLE_STATUSES = new Set(['pending', 'running']);
30
- const TERMINAL_STATUSES = new Set(['completed', 'failed', 'stopped']);
31
- const MAX_REQUEST_ID_LENGTH = 200;
32
-
33
- function id(value) { return typeof value === 'string' && value.trim() !== '' ? value : null; }
34
- function freeze(receipt) { return Object.freeze(receipt); }
35
- function own(object, key) { return Object.prototype.hasOwnProperty.call(object, key); }
36
- function isThenable(value) {
37
- return !!value && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function';
38
- }
39
-
40
- // The canonical owner tuple embedded in an ownerRunId. Anything that does
41
- // not parse back to ['sdk-launch', 1, sdkSessionId, parent, toolUseId]
42
- // cannot be identity-verified against a live task.
43
- function ownerTuple(ownerRunId) {
44
- let tuple = null;
45
- try { tuple = JSON.parse(ownerRunId); } catch (error) { return null; }
46
- if (!Array.isArray(tuple) || tuple.length !== 5) return null;
47
- if (tuple[0] !== 'sdk-launch' || tuple[1] !== 1) return null;
48
- if (!id(tuple[2])) return null;
49
- return tuple;
50
- }
51
-
52
- // Verifies that the object a trusted lookup returned really IS the task
53
- // the caller asked to stop. Own (not inherited) identity fields, matching
54
- // taskId / ownerRunId / taskRef, the SDK session embedded in the owner
55
- // tuple and kind/type compatibility are all required; any doubt becomes a
56
- // fixed detail tag and the stop is refused before the SDK is touched.
57
- function targetMismatch(ref, task) {
58
- for (const key of ['taskId', 'sdkSessionId', 'ownerRunId', 'taskRef', 'taskKind', 'taskType']) {
59
- if (!own(task, key)) return 'task-shape';
60
- }
61
- if (task.taskId !== ref.taskId) return 'task-id';
62
- if (task.ownerRunId !== ref.ownerRunId) return 'owner-run';
63
- const tuple = ownerTuple(ref.ownerRunId);
64
- if (!tuple) return 'owner-run';
65
- if (task.sdkSessionId !== tuple[2]) return 'sdk-session';
66
- const taskRef = task.taskRef;
67
- if (!taskRef || typeof taskRef !== 'object' || Array.isArray(taskRef)) return 'task-ref';
68
- for (const key of ['kind', 'storeScope', 'contextId', 'parentSessionId', 'ownerRunId', 'taskId']) {
69
- if (!own(taskRef, key)) return 'task-ref';
70
- }
71
- if (taskRef.kind !== ref.kind || taskRef.taskId !== ref.taskId || taskRef.ownerRunId !== ref.ownerRunId) return 'task-ref';
72
- if (taskRef.storeScope !== ref.storeScope || taskRef.contextId !== ref.contextId || taskRef.parentSessionId !== ref.parentSessionId) return 'task-ref';
73
- if (KIND_BY_TYPE.get(task.taskType) !== task.taskKind) return 'kind-type';
74
- if (id(task.toolUseId) && tuple[4] !== task.toolUseId) return 'tool-use';
75
- return null;
76
- }
77
-
78
- function createNativeTaskControl({ scope, runtimeEpoch, getLiveTask, getLease, isDurable } = {}) {
79
- const fixedScope = createTaskScope(scope);
80
- if (!fixedScope) throw new Error('createNativeTaskControl requires a fixed TaskScope');
81
- if (!id(runtimeEpoch)) throw new Error('createNativeTaskControl requires a runtimeEpoch string');
82
- // No default allow: every trusted callback must be supplied explicitly.
83
- if (typeof getLiveTask !== 'function' || typeof getLease !== 'function' || typeof isDurable !== 'function') {
84
- throw new Error('createNativeTaskControl requires explicit getLiveTask, getLease and isDurable functions');
85
- }
86
- const epoch = runtimeEpoch;
87
- const frozenScope = Object.freeze({ storeScope: fixedScope.storeScope, contextId: fixedScope.contextId, parentSessionId: fixedScope.parentSessionId });
88
- // requestId -> { payload, promise }: an id is bound to the full request
89
- // payload (fixed scope + canonical ref + epoch). The same id replays the
90
- // same result - in flight or settled, including deterministic failure
91
- // receipts - and a different payload under the same id is a diagnosed
92
- // conflict, never a second dispatch.
93
- const requests = new Map();
94
- // (epoch, scope, ref, leaseId) -> send receipt promise: the same lease
95
- // and ref receive at most ONE SDK stopTask call whatever the requestIds;
96
- // both 'requested' and 'unknown' outcomes stay cached. A later real
97
- // terminal is only ever produced by independent SDK facts.
98
- const sentStops = new Map();
99
-
100
- function rejected(code, extra) {
101
- return freeze(Object.assign({ ok: false, code }, extra || {}));
102
- }
103
-
104
- // Runs one trusted synchronous callback under the protocol contract. A
105
- // throw, or a thenable answer that breaks the synchronous contract,
106
- // becomes a fixed unavailable receipt: the exception text is dropped,
107
- // stop() still resolves, the SDK is never called, and the answer is
108
- // cached per requestId like any deterministic result - never swallowed
109
- // into ok or stopped.
110
- function syncAnswer(invoke, code, identity) {
111
- let value;
112
- try {
113
- value = invoke();
114
- } catch (error) {
115
- return { failed: rejected(code, identity) };
116
- }
117
- if (isThenable(value)) return { failed: rejected(code, identity) };
118
- return { value };
119
- }
120
-
121
- async function execute(ref, requestId) {
122
- const lookedUp = syncAnswer(() => getLiveTask(ref), 'live-lookup-failed', { requestId });
123
- if (lookedUp.failed) return lookedUp.failed;
124
- const task = lookedUp.value;
125
- if (!task || typeof task !== 'object' || Array.isArray(task)) return rejected('task-not-live', { requestId });
126
- const identity = { requestId, taskId: task.taskId, sdkSessionId: task.sdkSessionId };
127
- if (task.association === 'main-command') return rejected('task-main-command', identity);
128
- if (task.association !== 'bound') return rejected('task-unassigned', identity);
129
- if (task.typeConflict === true) return rejected('task-type-conflict', identity);
130
- if (!CONTROLLED_KINDS.has(task.taskKind)) return rejected('task-kind-unknown', identity);
131
- const mismatch = targetMismatch(ref, task);
132
- if (mismatch) return rejected('task-target-mismatch', { requestId, taskId: ref.taskId, detail: mismatch });
133
- if (TERMINAL_STATUSES.has(task.status)) {
134
- // A terminal established by real SDK facts needs no SDK call at all.
135
- return freeze(Object.assign({ ok: true, outcome: 'already-terminal', status: task.status, statusSource: task.statusSource || null, stopTaskSent: false }, identity));
136
- }
137
- if (!STOPPABLE_STATUSES.has(task.status)) return rejected('task-status-unknown', identity);
138
- const durability = syncAnswer(() => isDurable(ref, task), 'durability-unavailable', identity);
139
- if (durability.failed) return durability.failed;
140
- if (durability.value !== true) return rejected('durability-insufficient', identity);
141
- const leased = syncAnswer(() => getLease(task.sdkSessionId), 'lease-unavailable', identity);
142
- if (leased.failed) return leased.failed;
143
- const lease = leased.value;
144
- if (!lease || typeof lease !== 'object' || Array.isArray(lease)) return rejected('lease-not-found', identity);
145
- const leaseId = id(lease.leaseId);
146
- if (lease.active !== true) return rejected('lease-inactive', Object.assign({ leaseId }, identity));
147
- if (id(lease.sdkSessionId) !== task.sdkSessionId) return rejected('lease-session-mismatch', Object.assign({ leaseId }, identity));
148
- if (!leaseId) return rejected('lease-invalid', identity);
149
- // Lease identity fields must be the lease's own data: an object that
150
- // only inherits sdkSessionId/leaseId/active/query from a prototype (or
151
- // smuggles them through a promise-like shape) is never a real lease.
152
- for (const key of ['leaseId', 'active', 'sdkSessionId', 'query']) {
153
- if (!own(lease, key)) return rejected('lease-invalid', Object.assign({ leaseId }, identity));
154
- }
155
- if (!lease.query || typeof lease.query !== 'object' || typeof lease.query.stopTask !== 'function') {
156
- return rejected('lease-handle-invalid', Object.assign({ leaseId }, identity));
157
- }
158
- const stopKey = JSON.stringify([epoch, frozenScope.storeScope, frozenScope.contextId, frozenScope.parentSessionId, ref.ownerRunId, ref.taskId, leaseId]);
159
- let entry = sentStops.get(stopKey);
160
- if (!entry) {
161
- // Exactly one SDK call per lease+ref, invoked as a method so `this`
162
- // is the real query object. The RPC resolving only proves the
163
- // request left: the task status itself is never rewritten here, and
164
- // a rejection is kept as an unknown outcome with NO upstream error
165
- // text - receipts expose only this module's fixed codes.
166
- const send = (async () => {
167
- try {
168
- await lease.query.stopTask(task.taskId);
169
- return freeze({ ok: true, outcome: 'requested', stopTaskSent: true });
170
- } catch (error) {
171
- return freeze({ ok: false, outcome: 'unknown', code: 'stop-rejected', stopTaskSent: true });
172
- }
173
- })();
174
- entry = { promise: send };
175
- sentStops.set(stopKey, entry);
176
- const base = await entry.promise;
177
- return freeze(Object.assign({ leaseId, deduped: false }, base, identity));
178
- }
179
- const base = await entry.promise;
180
- return freeze(Object.assign({ leaseId, deduped: true }, base, identity));
181
- }
182
-
183
- // Receipts always resolve (never throw): every unsatisfiable case is a
184
- // short receipt with an explicit code and no upstream text, and even
185
- // hostile property getters on the stop arguments cannot turn stop() into
186
- // a rejected promise. Callers get a fresh copy per call, so mutating a
187
- // returned receipt cannot rewrite the stored one.
188
- function stop(args) {
189
- let ref = null;
190
- let requestId = null;
191
- try {
192
- if (!args || typeof args !== 'object') return Promise.resolve(rejected('request-invalid', { detail: 'stop-args' }));
193
- ref = createNativeTaskRef(args.taskRef);
194
- if (!ref) return Promise.resolve(rejected('request-invalid', { detail: 'taskRef' }));
195
- requestId = args.requestId;
196
- if (!id(requestId) || requestId.length > MAX_REQUEST_ID_LENGTH) return Promise.resolve(rejected('request-invalid', { detail: 'requestId' }));
197
- if (!sameTaskScope(ref, frozenScope)) return Promise.resolve(rejected('ref-scope-mismatch', { requestId }));
198
- if (!id(args.runtimeEpoch)) return Promise.resolve(rejected('request-invalid', { requestId, detail: 'runtimeEpoch' }));
199
- if (args.runtimeEpoch !== epoch) return Promise.resolve(rejected('epoch-mismatch', { requestId }));
200
- } catch (error) {
201
- return Promise.resolve(rejected('request-invalid', { detail: 'stop-args' }));
202
- }
203
- const payload = JSON.stringify([frozenScope.storeScope, frozenScope.contextId, frozenScope.parentSessionId, ref.ownerRunId, ref.taskId, epoch]);
204
- const existing = requests.get(requestId);
205
- if (existing) {
206
- if (existing.payload !== payload) return Promise.resolve(rejected('request-conflict', { requestId }));
207
- return existing.promise.then(receipt => ({ ...receipt }));
208
- }
209
- const promise = execute(ref, requestId);
210
- requests.set(requestId, { payload, promise });
211
- return promise.then(receipt => ({ ...receipt }));
212
- }
213
-
214
- return { stop };
215
- }
216
-
217
- module.exports = { createNativeTaskControl };
1
+ 'use strict';
2
+
3
+ // Native task control - the trusted first-stage coordinator deciding
4
+ // whether ONE native background task may receive a targeted stop through a
5
+ // CURRENT SDK query lease. It owns no transport and invents no facts: the
6
+ // runner supplies three explicit trusted synchronous callbacks (live task
7
+ // lookup, SDK query lease lookup, durability verdict) and this module only
8
+ // combines their answers into one short receipt per stop request. Every
9
+ // non-satisfiable case is an explicit rejection code - there is
10
+ // deliberately NO fallback to session.abort, transport.close or stopping
11
+ // the parent session. Receipts carry only fixed safe codes, short detail
12
+ // tags and identity fields: upstream exception text is never copied into
13
+ // a receipt (truncation is not redaction), a callback that throws or
14
+ // answers asynchronously resolves an explicit unavailable receipt instead
15
+ // of rejecting the stop() promise, and the task a lookup returns is
16
+ // verified against the requested ref (taskId, ownerRunId, taskRef, SDK
17
+ // session, kind/type) before any SDK call, so one broken lookup can never
18
+ // widen into stopping a different task. This is not yet a product stop
19
+ // API: runner, Service and API wiring is a later stage; unit tests use
20
+ // fixtures and are not a real-SDK proof.
21
+
22
+ const { createTaskScope, createNativeTaskRef, sameTaskScope } = require('./task-identity');
23
+
24
+ const CONTROLLED_KINDS = new Set(['agent', 'local_bash']);
25
+ // Mirrors TASK_KIND_BY_TYPE in the registry: a bound task's kind is
26
+ // derived from its type there, so an inconsistent kind/type pair here can
27
+ // only come from a broken lookup and must never reach the SDK.
28
+ const KIND_BY_TYPE = new Map([['local_agent', 'agent'], ['local_bash', 'local_bash']]);
29
+ const STOPPABLE_STATUSES = new Set(['pending', 'running']);
30
+ const TERMINAL_STATUSES = new Set(['completed', 'failed', 'stopped']);
31
+ const MAX_REQUEST_ID_LENGTH = 200;
32
+
33
+ function id(value) { return typeof value === 'string' && value.trim() !== '' ? value : null; }
34
+ function freeze(receipt) { return Object.freeze(receipt); }
35
+ function own(object, key) { return Object.prototype.hasOwnProperty.call(object, key); }
36
+ function isThenable(value) {
37
+ return !!value && (typeof value === 'object' || typeof value === 'function') && typeof value.then === 'function';
38
+ }
39
+
40
+ // The canonical owner tuple embedded in an ownerRunId. Anything that does
41
+ // not parse back to ['sdk-launch', 1, sdkSessionId, parent, toolUseId]
42
+ // cannot be identity-verified against a live task.
43
+ function ownerTuple(ownerRunId) {
44
+ let tuple = null;
45
+ try { tuple = JSON.parse(ownerRunId); } catch (error) { return null; }
46
+ if (!Array.isArray(tuple) || tuple.length !== 5) return null;
47
+ if (tuple[0] !== 'sdk-launch' || tuple[1] !== 1) return null;
48
+ if (!id(tuple[2])) return null;
49
+ return tuple;
50
+ }
51
+
52
+ // Verifies that the object a trusted lookup returned really IS the task
53
+ // the caller asked to stop. Own (not inherited) identity fields, matching
54
+ // taskId / ownerRunId / taskRef, the SDK session embedded in the owner
55
+ // tuple and kind/type compatibility are all required; any doubt becomes a
56
+ // fixed detail tag and the stop is refused before the SDK is touched.
57
+ function targetMismatch(ref, task) {
58
+ for (const key of ['taskId', 'sdkSessionId', 'ownerRunId', 'taskRef', 'taskKind', 'taskType']) {
59
+ if (!own(task, key)) return 'task-shape';
60
+ }
61
+ if (task.taskId !== ref.taskId) return 'task-id';
62
+ if (task.ownerRunId !== ref.ownerRunId) return 'owner-run';
63
+ const tuple = ownerTuple(ref.ownerRunId);
64
+ if (!tuple) return 'owner-run';
65
+ if (task.sdkSessionId !== tuple[2]) return 'sdk-session';
66
+ const taskRef = task.taskRef;
67
+ if (!taskRef || typeof taskRef !== 'object' || Array.isArray(taskRef)) return 'task-ref';
68
+ for (const key of ['kind', 'storeScope', 'contextId', 'parentSessionId', 'ownerRunId', 'taskId']) {
69
+ if (!own(taskRef, key)) return 'task-ref';
70
+ }
71
+ if (taskRef.kind !== ref.kind || taskRef.taskId !== ref.taskId || taskRef.ownerRunId !== ref.ownerRunId) return 'task-ref';
72
+ if (taskRef.storeScope !== ref.storeScope || taskRef.contextId !== ref.contextId || taskRef.parentSessionId !== ref.parentSessionId) return 'task-ref';
73
+ if (KIND_BY_TYPE.get(task.taskType) !== task.taskKind) return 'kind-type';
74
+ if (id(task.toolUseId) && tuple[4] !== task.toolUseId) return 'tool-use';
75
+ return null;
76
+ }
77
+
78
+ function createNativeTaskControl({ scope, runtimeEpoch, getLiveTask, getLease, isDurable } = {}) {
79
+ const fixedScope = createTaskScope(scope);
80
+ if (!fixedScope) throw new Error('createNativeTaskControl requires a fixed TaskScope');
81
+ if (!id(runtimeEpoch)) throw new Error('createNativeTaskControl requires a runtimeEpoch string');
82
+ // No default allow: every trusted callback must be supplied explicitly.
83
+ if (typeof getLiveTask !== 'function' || typeof getLease !== 'function' || typeof isDurable !== 'function') {
84
+ throw new Error('createNativeTaskControl requires explicit getLiveTask, getLease and isDurable functions');
85
+ }
86
+ const epoch = runtimeEpoch;
87
+ const frozenScope = Object.freeze({ storeScope: fixedScope.storeScope, contextId: fixedScope.contextId, parentSessionId: fixedScope.parentSessionId });
88
+ // requestId -> { payload, promise }: an id is bound to the full request
89
+ // payload (fixed scope + canonical ref + epoch). The same id replays the
90
+ // same result - in flight or settled, including deterministic failure
91
+ // receipts - and a different payload under the same id is a diagnosed
92
+ // conflict, never a second dispatch.
93
+ const requests = new Map();
94
+ // (epoch, scope, ref, leaseId) -> send receipt promise: the same lease
95
+ // and ref receive at most ONE SDK stopTask call whatever the requestIds;
96
+ // both 'requested' and 'unknown' outcomes stay cached. A later real
97
+ // terminal is only ever produced by independent SDK facts.
98
+ const sentStops = new Map();
99
+
100
+ function rejected(code, extra) {
101
+ return freeze(Object.assign({ ok: false, code }, extra || {}));
102
+ }
103
+
104
+ // Runs one trusted synchronous callback under the protocol contract. A
105
+ // throw, or a thenable answer that breaks the synchronous contract,
106
+ // becomes a fixed unavailable receipt: the exception text is dropped,
107
+ // stop() still resolves, the SDK is never called, and the answer is
108
+ // cached per requestId like any deterministic result - never swallowed
109
+ // into ok or stopped.
110
+ function syncAnswer(invoke, code, identity) {
111
+ let value;
112
+ try {
113
+ value = invoke();
114
+ } catch (error) {
115
+ return { failed: rejected(code, identity) };
116
+ }
117
+ if (isThenable(value)) return { failed: rejected(code, identity) };
118
+ return { value };
119
+ }
120
+
121
+ async function execute(ref, requestId) {
122
+ const lookedUp = syncAnswer(() => getLiveTask(ref), 'live-lookup-failed', { requestId });
123
+ if (lookedUp.failed) return lookedUp.failed;
124
+ const task = lookedUp.value;
125
+ if (!task || typeof task !== 'object' || Array.isArray(task)) return rejected('task-not-live', { requestId });
126
+ const identity = { requestId, taskId: task.taskId, sdkSessionId: task.sdkSessionId };
127
+ if (task.association === 'main-command') return rejected('task-main-command', identity);
128
+ if (task.association !== 'bound') return rejected('task-unassigned', identity);
129
+ if (task.typeConflict === true) return rejected('task-type-conflict', identity);
130
+ if (!CONTROLLED_KINDS.has(task.taskKind)) return rejected('task-kind-unknown', identity);
131
+ const mismatch = targetMismatch(ref, task);
132
+ if (mismatch) return rejected('task-target-mismatch', { requestId, taskId: ref.taskId, detail: mismatch });
133
+ if (TERMINAL_STATUSES.has(task.status)) {
134
+ // A terminal established by real SDK facts needs no SDK call at all.
135
+ return freeze(Object.assign({ ok: true, outcome: 'already-terminal', status: task.status, statusSource: task.statusSource || null, stopTaskSent: false }, identity));
136
+ }
137
+ if (!STOPPABLE_STATUSES.has(task.status)) return rejected('task-status-unknown', identity);
138
+ const durability = syncAnswer(() => isDurable(ref, task), 'durability-unavailable', identity);
139
+ if (durability.failed) return durability.failed;
140
+ if (durability.value !== true) return rejected('durability-insufficient', identity);
141
+ const leased = syncAnswer(() => getLease(task.sdkSessionId), 'lease-unavailable', identity);
142
+ if (leased.failed) return leased.failed;
143
+ const lease = leased.value;
144
+ if (!lease || typeof lease !== 'object' || Array.isArray(lease)) return rejected('lease-not-found', identity);
145
+ const leaseId = id(lease.leaseId);
146
+ if (lease.active !== true) return rejected('lease-inactive', Object.assign({ leaseId }, identity));
147
+ if (id(lease.sdkSessionId) !== task.sdkSessionId) return rejected('lease-session-mismatch', Object.assign({ leaseId }, identity));
148
+ if (!leaseId) return rejected('lease-invalid', identity);
149
+ // Lease identity fields must be the lease's own data: an object that
150
+ // only inherits sdkSessionId/leaseId/active/query from a prototype (or
151
+ // smuggles them through a promise-like shape) is never a real lease.
152
+ for (const key of ['leaseId', 'active', 'sdkSessionId', 'query']) {
153
+ if (!own(lease, key)) return rejected('lease-invalid', Object.assign({ leaseId }, identity));
154
+ }
155
+ if (!lease.query || typeof lease.query !== 'object' || typeof lease.query.stopTask !== 'function') {
156
+ return rejected('lease-handle-invalid', Object.assign({ leaseId }, identity));
157
+ }
158
+ const stopKey = JSON.stringify([epoch, frozenScope.storeScope, frozenScope.contextId, frozenScope.parentSessionId, ref.ownerRunId, ref.taskId, leaseId]);
159
+ let entry = sentStops.get(stopKey);
160
+ if (!entry) {
161
+ // Exactly one SDK call per lease+ref, invoked as a method so `this`
162
+ // is the real query object. The RPC resolving only proves the
163
+ // request left: the task status itself is never rewritten here, and
164
+ // a rejection is kept as an unknown outcome with NO upstream error
165
+ // text - receipts expose only this module's fixed codes.
166
+ const send = (async () => {
167
+ try {
168
+ await lease.query.stopTask(task.taskId);
169
+ return freeze({ ok: true, outcome: 'requested', stopTaskSent: true });
170
+ } catch (error) {
171
+ return freeze({ ok: false, outcome: 'unknown', code: 'stop-rejected', stopTaskSent: true });
172
+ }
173
+ })();
174
+ entry = { promise: send };
175
+ sentStops.set(stopKey, entry);
176
+ const base = await entry.promise;
177
+ return freeze(Object.assign({ leaseId, deduped: false }, base, identity));
178
+ }
179
+ const base = await entry.promise;
180
+ return freeze(Object.assign({ leaseId, deduped: true }, base, identity));
181
+ }
182
+
183
+ // Receipts always resolve (never throw): every unsatisfiable case is a
184
+ // short receipt with an explicit code and no upstream text, and even
185
+ // hostile property getters on the stop arguments cannot turn stop() into
186
+ // a rejected promise. Callers get a fresh copy per call, so mutating a
187
+ // returned receipt cannot rewrite the stored one.
188
+ function stop(args) {
189
+ let ref = null;
190
+ let requestId = null;
191
+ try {
192
+ if (!args || typeof args !== 'object') return Promise.resolve(rejected('request-invalid', { detail: 'stop-args' }));
193
+ ref = createNativeTaskRef(args.taskRef);
194
+ if (!ref) return Promise.resolve(rejected('request-invalid', { detail: 'taskRef' }));
195
+ requestId = args.requestId;
196
+ if (!id(requestId) || requestId.length > MAX_REQUEST_ID_LENGTH) return Promise.resolve(rejected('request-invalid', { detail: 'requestId' }));
197
+ if (!sameTaskScope(ref, frozenScope)) return Promise.resolve(rejected('ref-scope-mismatch', { requestId }));
198
+ if (!id(args.runtimeEpoch)) return Promise.resolve(rejected('request-invalid', { requestId, detail: 'runtimeEpoch' }));
199
+ if (args.runtimeEpoch !== epoch) return Promise.resolve(rejected('epoch-mismatch', { requestId }));
200
+ } catch (error) {
201
+ return Promise.resolve(rejected('request-invalid', { detail: 'stop-args' }));
202
+ }
203
+ const payload = JSON.stringify([frozenScope.storeScope, frozenScope.contextId, frozenScope.parentSessionId, ref.ownerRunId, ref.taskId, epoch]);
204
+ const existing = requests.get(requestId);
205
+ if (existing) {
206
+ if (existing.payload !== payload) return Promise.resolve(rejected('request-conflict', { requestId }));
207
+ return existing.promise.then(receipt => ({ ...receipt }));
208
+ }
209
+ const promise = execute(ref, requestId);
210
+ requests.set(requestId, { payload, promise });
211
+ return promise.then(receipt => ({ ...receipt }));
212
+ }
213
+
214
+ return { stop };
215
+ }
216
+
217
+ module.exports = { createNativeTaskControl };