@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,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 };