@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.2

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 (156) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +306 -53
  4. package/bin/ai-app-bridge.js +51 -4504
  5. package/bin/android-permissions.js +152 -0
  6. package/bin/android-uia-xml.js +74 -0
  7. package/bin/artifact-paths.js +127 -1
  8. package/bin/bridge-forward.js +56 -0
  9. package/bin/command-discovery.js +86 -0
  10. package/bin/command-errors.js +60 -0
  11. package/bin/command-registry.js +504 -0
  12. package/bin/command-request.js +14 -0
  13. package/bin/command-router.js +80 -0
  14. package/bin/connection-cache.js +119 -0
  15. package/bin/device-provider.js +2934 -0
  16. package/bin/execution-host.js +596 -0
  17. package/bin/execution-runtime.js +101 -0
  18. package/bin/fact-codec.js +321 -0
  19. package/bin/fact-recorder.js +691 -0
  20. package/bin/fact-store.js +226 -0
  21. package/bin/feedback-probe.js +285 -0
  22. package/bin/intent/install-intent.js +267 -0
  23. package/bin/intent/intent-action-executor.js +129 -0
  24. package/bin/intent/intent-autonomous-adapter.js +46 -0
  25. package/bin/intent/intent-capture-port.js +64 -0
  26. package/bin/intent/intent-entry.js +226 -0
  27. package/bin/intent/intent-errors.js +28 -0
  28. package/bin/intent/intent-evidence-store.js +121 -0
  29. package/bin/intent/intent-lifetime.js +62 -0
  30. package/bin/intent/intent-observation-target.js +33 -0
  31. package/bin/intent/intent-observer.js +194 -0
  32. package/bin/intent/intent-production-adapter.js +334 -0
  33. package/bin/intent/intent-provider.js +27 -0
  34. package/bin/intent/intent-runtime.js +63 -0
  35. package/bin/intent/intent-worker.js +432 -0
  36. package/bin/intent/ios-intent-adapter.js +90 -0
  37. package/bin/intent/permission-intent.js +239 -0
  38. package/bin/intent/web-intent-adapter.js +31 -0
  39. package/bin/ios-device-outcome.js +47 -0
  40. package/bin/ios-execution.js +108 -0
  41. package/bin/ios-provider.js +497 -583
  42. package/bin/ios-runtime-binding.js +54 -0
  43. package/bin/ios-wda-execution.js +70 -0
  44. package/bin/ios-wda-port.js +98 -0
  45. package/bin/ios-wda-project.js +79 -0
  46. package/bin/mcp-server.js +83 -1040
  47. package/bin/mmap-scan-index.js +329 -0
  48. package/bin/observation-collector.js +862 -0
  49. package/bin/runtime-client.js +154 -0
  50. package/bin/runtime-directory.js +107 -0
  51. package/bin/runtime-protocol.js +36 -0
  52. package/bin/script/bounded-script-registry.js +103 -0
  53. package/bin/script/node-runtime-adapter.js +233 -0
  54. package/bin/script/progress-projector.js +63 -0
  55. package/bin/script/python-runtime-adapter.js +111 -0
  56. package/bin/script/rolling-summary.js +134 -0
  57. package/bin/script/script-agent-port.js +40 -0
  58. package/bin/script/script-assert.js +95 -0
  59. package/bin/script/script-capture-port.js +76 -0
  60. package/bin/script/script-catalog.js +85 -0
  61. package/bin/script/script-durable-restore.js +195 -0
  62. package/bin/script/script-entry-code.js +26 -0
  63. package/bin/script/script-entry-route.js +38 -0
  64. package/bin/script/script-entry.js +3 -0
  65. package/bin/script/script-errors.js +29 -0
  66. package/bin/script/script-evidence-store.js +22 -0
  67. package/bin/script/script-format-removed.js +26 -0
  68. package/bin/script/script-host-port.js +397 -0
  69. package/bin/script/script-ledger.js +64 -0
  70. package/bin/script/script-result.js +57 -0
  71. package/bin/script/script-sdk.js +152 -0
  72. package/bin/script/script-sdk.py +153 -0
  73. package/bin/script/script-session-channel.js +127 -0
  74. package/bin/script/script-spec.js +86 -0
  75. package/bin/script/script-supervisor.js +919 -0
  76. package/bin/script/templates/checkpoint-reentry.js +13 -0
  77. package/bin/segment-index.js +481 -0
  78. package/bin/segmented-fact-store.js +1571 -0
  79. package/bin/shared-kernel/android-h5-target.js +10 -0
  80. package/bin/shared-kernel/android-install-execution.js +176 -0
  81. package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
  82. package/bin/shared-kernel/android-shell-execution.js +195 -0
  83. package/bin/shared-kernel/argument-schema.js +117 -0
  84. package/bin/shared-kernel/canonical-path.js +17 -0
  85. package/bin/shared-kernel/device-acknowledgements.js +53 -0
  86. package/bin/shared-kernel/device-completion-history.js +52 -0
  87. package/bin/shared-kernel/device-mutation-lease.js +219 -0
  88. package/bin/shared-kernel/device-ownership-recovery.js +95 -0
  89. package/bin/shared-kernel/device-ownership-store.js +94 -0
  90. package/bin/shared-kernel/evidence-adapters.js +251 -0
  91. package/bin/shared-kernel/evidence-archive.js +329 -0
  92. package/bin/shared-kernel/evidence-recording.js +131 -0
  93. package/bin/shared-kernel/evidence-schema.js +194 -0
  94. package/bin/shared-kernel/evidence-store.js +190 -0
  95. package/bin/shared-kernel/execution-admission.js +22 -0
  96. package/bin/shared-kernel/execution-contracts.js +171 -0
  97. package/bin/shared-kernel/execution-io.js +106 -0
  98. package/bin/shared-kernel/execution-ledger.js +125 -0
  99. package/bin/shared-kernel/execution-scope.js +87 -0
  100. package/bin/shared-kernel/execution-target.js +115 -0
  101. package/bin/shared-kernel/flutter-execution.js +13 -0
  102. package/bin/shared-kernel/flutter-h5-port.js +60 -0
  103. package/bin/shared-kernel/flutter-h5-target.js +9 -0
  104. package/bin/shared-kernel/flutter-target.js +75 -0
  105. package/bin/shared-kernel/h5-execution.js +11 -0
  106. package/bin/shared-kernel/h5-target.js +31 -0
  107. package/bin/shared-kernel/host-fact-store.js +49 -0
  108. package/bin/shared-kernel/ios-h5-target.js +9 -0
  109. package/bin/shared-kernel/ios-native-target.js +71 -0
  110. package/bin/shared-kernel/live-capture-query.js +115 -0
  111. package/bin/shared-kernel/managed-sdk-execution.js +78 -0
  112. package/bin/shared-kernel/native-execution.js +13 -0
  113. package/bin/shared-kernel/native-target.js +156 -0
  114. package/bin/shared-kernel/provider-command-contracts.js +55 -0
  115. package/bin/shared-kernel/recorded-payload-archive.js +195 -0
  116. package/bin/shared-kernel/request-context.js +35 -0
  117. package/bin/shared-kernel/semantic-node.js +55 -0
  118. package/bin/shared-kernel/summary-transformer.js +352 -0
  119. package/bin/shared-kernel/target-lease-protocol.js +47 -0
  120. package/bin/shared-kernel/text-wait.js +111 -0
  121. package/bin/shared-kernel/uia-execution.js +96 -0
  122. package/bin/shared-kernel/uia-protocol.js +214 -0
  123. package/bin/shared-kernel/uia-runtime-port.js +377 -0
  124. package/bin/shared-kernel/uia-target.js +39 -0
  125. package/bin/shared-kernel/web-dom-target.js +44 -0
  126. package/bin/shared-kernel/xml-attributes.js +25 -0
  127. package/bin/target-execution.js +275 -0
  128. package/bin/web/command-schema.js +60 -0
  129. package/bin/web/session-store.js +157 -0
  130. package/bin/web-provider.js +334 -553
  131. package/docs/COMMAND_CONTRACT.md +1563 -0
  132. package/docs/EVIDENCE_ARCHIVE.md +214 -0
  133. package/docs/INTENT_FOREGROUND.md +71 -0
  134. package/docs/INTENT_NATIVE_EDITING.md +79 -0
  135. package/docs/RELEASE.md +59 -0
  136. package/docs/SCRIPT_AUTHORING.md +489 -0
  137. package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
  138. package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
  139. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
  140. package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
  141. package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
  142. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
  143. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
  144. package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
  145. package/package.json +60 -5
  146. package/runtime/ios-wda/AABWDABinding.h +19 -0
  147. package/runtime/ios-wda/AABWDABinding.m +97 -0
  148. package/runtime/ios-wda/AABWDAExecution.h +26 -0
  149. package/runtime/ios-wda/AABWDAExecution.m +172 -0
  150. package/runtime/ios-wda/AABWDAIntegration.h +71 -0
  151. package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
  152. package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
  153. package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
  154. package/runtime/uia/ai-app-bridge-uia.jar +0 -0
  155. package/runtime/uia/manifest.json +22 -0
  156. package/skills/ai-app-bridge-use/SKILL.md +23 -360
@@ -0,0 +1,919 @@
1
+ 'use strict';
2
+
3
+ const { bindCommandTarget } = require('../shared-kernel/execution-target');
4
+ const { authorizeCommand } = require('./script-catalog');
5
+
6
+ const { createBoundedEventLog, createBoundedScriptRegistry } = require('./bounded-script-registry');
7
+ const { createScriptAgentPort } = require('./script-agent-port');
8
+ const { createScriptHostPort, unavailableEnvelope } = require('./script-host-port');
9
+ const { checksumOf } = require('../shared-kernel/evidence-schema');
10
+ const { createNodeRuntimeAdapter } = require('./node-runtime-adapter');
11
+ const { createProgressProjector } = require('./progress-projector');
12
+ const { createPythonRuntimeAdapter, inspectPython } = require('./python-runtime-adapter');
13
+ const { createRollingSummary } = require('./rolling-summary');
14
+ const { catalogPayload } = require('./script-catalog');
15
+ const { isMutationCommand } = require('../command-registry');
16
+ const { runExecution } = require('../shared-kernel/execution-scope');
17
+ const { compileScriptSpec, isCodeScript } = require('./script-spec');
18
+ const { createScriptLedger, scriptEventTarget } = require('./script-ledger');
19
+ const {
20
+ codeRuntimeCheckpoint,
21
+ codeStartCheckpoint,
22
+ RESTORE_CONTEXT,
23
+ restartGate,
24
+ } = require('./script-durable-restore');
25
+ const { scriptError } = require('./script-errors');
26
+ const { createEvidenceRecording } = require('../shared-kernel/evidence-recording');
27
+ const { prepareScriptResult, resultReference, readScriptResult } = require('./script-result');
28
+
29
+ function createDefaultRuntime(opts = {}) {
30
+ if (opts.language === 'javascript') return createNodeRuntimeAdapter(opts);
31
+ if (opts.language === 'python') return createPythonRuntimeAdapter(opts);
32
+ throw new TypeError('runtime_unavailable');
33
+ }
34
+
35
+ function createScriptSupervisor({
36
+ registry = createBoundedScriptRegistry(),
37
+ createRuntime = createDefaultRuntime,
38
+ createHost = createScriptHostPort,
39
+ createAgent = createScriptAgentPort,
40
+ createLedger = createScriptLedger,
41
+ now = Date.now,
42
+ } = {}) {
43
+ let sequence = 0;
44
+ const scriptLedger = createLedger();
45
+
46
+ function handle(args = {}) {
47
+ const operation = args.operation || 'start';
48
+ if (operation === 'start') return start(args);
49
+ if (operation === 'result') return readScriptResult({ ...args, store: registry.get(args.operationId)?.store || args.store });
50
+ if (operation === 'status') {
51
+ const limitGate = rejectBadLimit(args);
52
+ if (limitGate) return limitGate;
53
+ return snapshot(args);
54
+ }
55
+ if (operation === 'wait') {
56
+ const limitGate = rejectBadLimit(args);
57
+ if (limitGate) return limitGate;
58
+ return wait(args);
59
+ }
60
+ if (operation === 'pause') return control(args, 'pause_requested');
61
+ if (operation === 'resume') return resume(args);
62
+ if (operation === 'decide') return decide(args);
63
+ if (operation === 'cancel') return control(args, 'cancelled');
64
+ if (operation === 'runtime-status') return { ok: true, command: 'script', runtimes: inspectRuntimes() };
65
+ return scriptError('invalid_operation', { operation });
66
+ }
67
+
68
+ function start(args) {
69
+ const compiled = args[RESTORE_CONTEXT]?.compiled || compileScriptSpec(args.script);
70
+ if (!compiled.ok) return compiled;
71
+ if (args.operationId && knownOperation(args.operationId)) {
72
+ return scriptError('operation_exists', { operationId: args.operationId });
73
+ }
74
+ sequence += 1;
75
+ const operationId = args.operationId || `script-${now()}-${sequence}`;
76
+ if (args.store?.latest?.(operationId, 'checkpoint') && !args[RESTORE_CONTEXT]) {
77
+ return scriptError('operation_exists', { operationId });
78
+ }
79
+ let recording = null;
80
+ if (args.recordingDir !== undefined) {
81
+ if (compiled.spec.policy.restartPolicy !== 'none') {
82
+ return scriptError('recording_restart_unsupported', { operationId });
83
+ }
84
+ try {
85
+ recording = createEvidenceRecording({ directory: args.recordingDir, namespace: 'script',
86
+ operationId, store: args.store, now });
87
+ } catch (error) { return scriptError(error.code || 'recording_failed', { operationId }); }
88
+ }
89
+ const events = createBoundedEventLog({
90
+ maxEvents: registry.maxEvents,
91
+ maxEventBytes: registry.maxEventBytes,
92
+ });
93
+ const agent = args.agent || createAgent();
94
+ let host;
95
+ try {
96
+ host = args.host || createHost({
97
+ handlers: args.handlers,
98
+ permissions: compiled.spec.permissions,
99
+ executionId: operationId,
100
+ runner: args.runner,
101
+ actions: args.actions,
102
+ query: args.query,
103
+ mutationLease: args.mutationLease,
104
+ target: compiled.spec.target,
105
+ });
106
+ } catch (error) {
107
+ return scriptError(error.message === 'actions' ? 'host_actions_required' : error.message, { operationId });
108
+ }
109
+ const runtime = args.runtime || createRuntime({
110
+ program: args.program,
111
+ language: compiled.spec.language,
112
+ spec: compiled.spec,
113
+ pythonPath: args.pythonPath,
114
+ });
115
+ if (runtime.unavailable) {
116
+ return scriptError('runtime_unavailable', { language: compiled.spec.language });
117
+ }
118
+ if (args.store && typeof args.store.persist === 'function' && !args[RESTORE_CONTEXT]) {
119
+ return persistStart(args, {
120
+ compiled,
121
+ operationId,
122
+ events,
123
+ agent,
124
+ host,
125
+ runtime,
126
+ recording,
127
+ });
128
+ }
129
+ return finishStart(args, {
130
+ compiled,
131
+ operationId,
132
+ events,
133
+ agent,
134
+ host,
135
+ runtime,
136
+ recording,
137
+ });
138
+ }
139
+
140
+ async function persistStart(args, parts) {
141
+ const persisted = await args.store.persist(
142
+ 'checkpoint',
143
+ { ...codeStartCheckpoint(parts.compiled, parts.operationId),
144
+ ...(parts.recording ? { recording: parts.recording.info() } : {}) },
145
+ );
146
+ if (!persisted.ok) {
147
+ return scriptError(persisted.error || 'checkpoint_not_persisted', { operationId: parts.operationId });
148
+ }
149
+ return finishStart(args, parts);
150
+ }
151
+
152
+ function finishStart(args, { compiled, operationId, events, agent, host, runtime, recording }) {
153
+ const startedAtMs = now();
154
+ const restoreContext = args[RESTORE_CONTEXT] || null;
155
+ const record = {
156
+ operationId,
157
+ spec: compiled.spec,
158
+ hash: compiled.hash,
159
+ status: 'running',
160
+ pauseReason: null,
161
+ error: null,
162
+ events,
163
+ agent,
164
+ host,
165
+ runtime,
166
+ startedAtMs,
167
+ finished: false,
168
+ projector: createProgressProjector(),
169
+ summary: createRollingSummary({ operationId, startedAtMs }),
170
+ lastMaterialAtMs: startedAtMs,
171
+ inFlightMutations: 0,
172
+ mutationUnknown: false,
173
+ decisionRevision: 0,
174
+ currentRequestId: null,
175
+ questionRevisions: new Map(),
176
+ decisions: new Map(),
177
+ ledger: scriptLedger,
178
+ store: args.store || null,
179
+ recording,
180
+ lastCheckpoint: restoreContext && restoreContext.checkpoint,
181
+ checkpointRevision: Number(restoreContext && restoreContext.revision) || 1,
182
+ actionSequence: Number(restoreContext && restoreContext.actionSequence) || 0,
183
+ checkpointActionSequence: Number(restoreContext && restoreContext.checkpointActionSequence) || 0,
184
+ actionsSinceCheckpoint: [],
185
+ durableError: null,
186
+ persistTail: Promise.resolve(),
187
+ pendingCalls: new Set(),
188
+ };
189
+ record.host = trackHost(host, record, now);
190
+ record.agent = trackAgent(agent, record, now);
191
+ const stored = registry.put(operationId, record);
192
+ if (!stored.ok) return scriptError(stored.error, { operationId });
193
+ emitRecord(record, 'script_started', { status: 'running' }, now);
194
+ startHeartbeat(record, now);
195
+ record.running = startRuntime(record, now);
196
+ return Promise.resolve(snapshotOf(record, args));
197
+ }
198
+
199
+ function startRuntime(record, now) {
200
+ record.controller = new AbortController();
201
+ record.deadlineMs = Date.now() + record.spec.policy.timeoutMs;
202
+ record.cancelling = false;
203
+ record.cancelPromise = null;
204
+ return Promise.resolve()
205
+ .then(() => record.runtime.start({
206
+ spec: record.spec,
207
+ host: record.host,
208
+ agent: record.agent,
209
+ emit: (type, extra) => {
210
+ const pending = emitRuntimeRecord(record, type, extra, now);
211
+ return type === 'checkpoint' ? trackPending(record, pending) : pending;
212
+ },
213
+ now,
214
+ control: () => ({
215
+ status: record.status,
216
+ pauseReason: record.pauseReason,
217
+ checkpoint: record.lastCheckpoint,
218
+ }),
219
+ }))
220
+ .then((result) => finishRuntime(record, result, now))
221
+ .catch(async () => {
222
+ if (typeof record.runtime.stop === 'function') await record.runtime.stop();
223
+ return finishRuntime(record, { ok: false, error: 'child_crashed' }, now);
224
+ });
225
+ }
226
+
227
+ async function finishRuntime(record, result, now) {
228
+ if (record.cancelling || record.status === 'cancelled') return { ok: false, error: 'cancelled' };
229
+ if (Date.now() >= record.deadlineMs) result = { ok: false, error: 'timeout' };
230
+ record.status = 'finishing';
231
+ record.controller.abort({ code: result?.error === 'timeout' ? 'deadline_exceeded' : 'runtime_stopped' });
232
+ await Promise.allSettled([...record.pendingCalls]);
233
+ const error = record.durableError || (record.mutationUnknown || record.inFlightMutations > 0 ? 'ambiguous' : result?.error || null);
234
+ const status = error || result?.ok === false ? 'failed' : 'completed';
235
+ return commitTerminal(record, status, error, now, result);
236
+ }
237
+
238
+ async function wait(args) {
239
+ if (args.isolatedTimeoutMs != null && args._scriptWaitHostTimeout !== true) {
240
+ return scriptError('unsupported_argument', { field: 'isolatedTimeoutMs' });
241
+ }
242
+ const record = registry.get(args.operationId);
243
+ if (!record) return ledgerSnapshot(args, { timedOut: false });
244
+ const resolved = resolveWaitMs(args.waitMs);
245
+ if (!resolved.ok) return resolved;
246
+ const waitMs = resolved.waitMs;
247
+ const after = Number(args.afterSequence) || 0;
248
+ if (waitMs === 0) {
249
+ return snapshotOf(record, args, {
250
+ timedOut: !hasWaitSignal(record, after),
251
+ waitMs,
252
+ });
253
+ }
254
+ return new Promise((resolve) => {
255
+ let settled = false;
256
+ let timer = null;
257
+ const finish = (timedOut) => {
258
+ if (settled) return;
259
+ settled = true;
260
+ unsubscribe();
261
+ if (timer) clearTimeout(timer);
262
+ resolve(snapshotOf(record, args, { timedOut, waitMs }));
263
+ };
264
+ const unsubscribe = record.events.subscribe(() => {
265
+ if (hasWaitSignal(record, after)) finish(false);
266
+ });
267
+ if (hasWaitSignal(record, after)) {
268
+ finish(false);
269
+ return;
270
+ }
271
+ timer = setTimeout(() => finish(true), waitMs);
272
+ });
273
+ }
274
+
275
+ function snapshot(args) {
276
+ const record = registry.get(args.operationId);
277
+ if (!record) return ledgerSnapshot(args);
278
+ return snapshotOf(record, args);
279
+ }
280
+
281
+ function ledgerSnapshot(args, extra = {}) {
282
+ const operationId = args.operationId || null;
283
+ const snap = scriptLedger.ledger.snapshot(operationId);
284
+ if (!snap.lastFact) {
285
+ return scriptError('unknown_operation', { operationId });
286
+ }
287
+ const terminalFact = ['script_completed', 'script_cancelled', 'script_failed']
288
+ .map((kind) => scriptLedger.ledger.latest(operationId, kind))
289
+ .filter(Boolean).sort((a, b) => b.sequence - a.sequence)[0];
290
+ const statusFact = terminalFact || snap.lastFact;
291
+ return {
292
+ ok: true,
293
+ command: 'script',
294
+ operationId,
295
+ status: statusFromFact(statusFact),
296
+ pauseReason: null,
297
+ error: statusFact.payloadSummary ? statusFact.payloadSummary.error || null : null,
298
+ hash: null,
299
+ resultRef: statusFact.payloadSummary?.resultRef ?? null,
300
+ eventSequence: snap.lastSequence,
301
+ events: [],
302
+ rollingSummary: null,
303
+ history: scriptLedger.ledger.query(
304
+ operationId,
305
+ args.afterSequence == null ? 0 : args.afterSequence,
306
+ args.limit,
307
+ ),
308
+ ...extra,
309
+ };
310
+ }
311
+
312
+ function control(args, status) {
313
+ const record = registry.get(args.operationId);
314
+ if (!record) return scriptError('unknown_operation', { operationId: args.operationId || null });
315
+ if (record.finished || record.status === 'finishing') return snapshotOf(record, args);
316
+ if (record.cancelling && status !== 'cancelled') return snapshotOf(record, args);
317
+ if (status === 'cancelled') {
318
+ if (record.cancelPromise) {
319
+ return record.cancelPromise.then(() => snapshotOf(record, args));
320
+ }
321
+ record.cancelling = true;
322
+ record.status = 'cancelling';
323
+ emitRecord(record, 'cancel_requested', { status: record.status }, now);
324
+ record.controller.abort({ code: 'cancelled' });
325
+ record.cancelPromise = Promise.resolve()
326
+ .then(() => (typeof record.runtime.stop === 'function' ? record.runtime.stop() : null))
327
+ .then(async () => {
328
+ await Promise.allSettled([...record.pendingCalls]);
329
+ await commitTerminal(record, 'cancelled', record.durableError || (record.mutationUnknown ? 'ambiguous' : null), now);
330
+ record.pauseReason = record.status === 'cancelled' ? 'cancelled' : null;
331
+ stopHeartbeat(record);
332
+ return snapshotOf(record, args);
333
+ });
334
+ return record.cancelPromise;
335
+ }
336
+ if (record.inFlightMutations > 0) {
337
+ record.status = 'pause_requested';
338
+ record.pauseReason = 'pause_requested';
339
+ emitRecord(record, 'pause_requested', { status: 'pause_requested' }, now);
340
+ pushControl(record);
341
+ return snapshotOf(record, args);
342
+ }
343
+ record.status = 'paused_manual';
344
+ record.pauseReason = status;
345
+ emitRecord(record, 'paused', { status: record.status }, now);
346
+ pushControl(record);
347
+ return snapshotOf(record, args);
348
+ }
349
+
350
+ function resume(args) {
351
+ const record = registry.get(args.operationId);
352
+ if (!record) return scriptError('unknown_operation', { operationId: args.operationId || null });
353
+ if (record.cancelling) return scriptError('runtime_stopped', { operationId: record.operationId });
354
+ if (record.status === 'finishing') return scriptError('not_resumable', { operationId: record.operationId });
355
+ if (record.mutationUnknown || record.inFlightMutations > 0) {
356
+ return scriptError('ambiguous', { operationId: record.operationId, status: 'ambiguous' });
357
+ }
358
+ if (record.finished) {
359
+ const gate = restartGate({
360
+ policy: record.spec.policy,
361
+ checkpoint: record.lastCheckpoint,
362
+ checkpointActionSequence: record.checkpointActionSequence,
363
+ status: record.status,
364
+ error: record.error,
365
+ }, record.actionsSinceCheckpoint);
366
+ if (!gate.ok) return { ...gate, operationId: record.operationId };
367
+ record.finished = false;
368
+ record.status = 'running';
369
+ record.error = null;
370
+ record.pauseReason = null;
371
+ record.mutationUnknown = false;
372
+ emitRecord(record, 'resumed', { status: 'running' }, now);
373
+ startHeartbeat(record, now);
374
+ record.running = startRuntime(record, now);
375
+ pushControl(record);
376
+ return snapshotOf(record, args);
377
+ }
378
+ record.status = 'running';
379
+ record.pauseReason = null;
380
+ record.mutationUnknown = false;
381
+ emitRecord(record, 'resumed', { status: 'running' }, now);
382
+ pushControl(record);
383
+ return snapshotOf(record, args);
384
+ }
385
+
386
+ function decide(args) {
387
+ const record = registry.get(args.operationId);
388
+ if (!record) return scriptError('unknown_operation', { operationId: args.operationId || null });
389
+ if (!Object.hasOwn(args, 'decision') || args.decision === undefined) {
390
+ return scriptError('invalid_argument', { field: 'decision' });
391
+ }
392
+ if (args.revision == null) {
393
+ return scriptError('stale_decide', { revision: record.decisionRevision });
394
+ }
395
+ if (!record.questionRevisions.has(args.requestId)) {
396
+ return scriptError('invalid_argument', { field: 'requestId' });
397
+ }
398
+ if (args.revision !== record.questionRevisions.get(args.requestId)) {
399
+ return scriptError('stale_decide', { revision: record.questionRevisions.get(args.requestId) });
400
+ }
401
+ if (record.decisions.has(args.requestId)) {
402
+ if (checksumOf(record.decisions.get(args.requestId)) !== checksumOf(args.decision)) {
403
+ return scriptError('invalid_argument', { field: 'decision' });
404
+ }
405
+ return snapshotOf(record, args);
406
+ }
407
+ if (record.cancelling || record.finished || record.status === 'finishing') return scriptError('runtime_stopped', { operationId: record.operationId });
408
+ const decided = record.agent.decide(args.requestId, args.decision);
409
+ if (!decided.ok) return scriptError(decided.error, { operationId: record.operationId });
410
+ record.decisions.set(args.requestId, decided.decision);
411
+ if (record.status === 'waiting_for_agent') record.status = 'running';
412
+ emitRecord(record, 'agent_decision', { requestId: args.requestId, decision: decided.decision }, now);
413
+ return snapshotOf(record, args);
414
+ }
415
+
416
+ function inspectRuntimes() {
417
+ return {
418
+ fake: { available: true },
419
+ javascript: { available: true, executable: process.execPath },
420
+ python: inspectPython(),
421
+ };
422
+ }
423
+
424
+ function snapshotOf(record, args = {}, extra = {}) {
425
+ maybeHeartbeat(record, now);
426
+ return {
427
+ ok: true,
428
+ command: 'script',
429
+ operationId: record.operationId,
430
+ status: record.status,
431
+ pauseReason: record.pauseReason,
432
+ error: record.error,
433
+ hash: record.hash,
434
+ resultRef: record.resultRef ?? null,
435
+ eventSequence: record.events.sequence,
436
+ events: record.events.after(args.afterSequence, args.eventLimit ?? args.limit),
437
+ rollingSummary: record.summary.snapshot(now()),
438
+ history: record.ledger.ledger.query(
439
+ record.operationId,
440
+ args.afterSequence == null ? 0 : args.afterSequence,
441
+ args.limit,
442
+ ),
443
+ ...(record.recording ? { recording: record.recording.info() } : {}),
444
+ catalog: args.includeCatalog ? catalogPayload() : undefined,
445
+ ...extra,
446
+ };
447
+ }
448
+
449
+ function knownOperation(operationId) {
450
+ if (!operationId) return false;
451
+ if (registry.get(operationId)) return true;
452
+ return scriptLedger.ledger.snapshot(operationId).lastFact != null;
453
+ }
454
+
455
+ async function cancelAll() {
456
+ await Promise.all(registry.values().filter(record => !record.finished).map(record =>
457
+ record.status === 'finishing' ? record.running : control({ operationId: record.operationId }, 'cancelled')));
458
+ }
459
+
460
+ return { handle, inspectRuntimes, registry, knownOperation, cancelAll };
461
+ }
462
+
463
+ function resolveWaitMs(value) {
464
+ if (value == null || value === '') return { ok: true, waitMs: 30_000 };
465
+ const waitMs = Number(value);
466
+ if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > 60_000) {
467
+ return scriptError('invalid_argument', { field: 'waitMs' });
468
+ }
469
+ return { ok: true, waitMs };
470
+ }
471
+
472
+ function hasWaitSignal(record, after) {
473
+ return record.events.sequence > after || isTerminal(record.status);
474
+ }
475
+
476
+ function isTerminal(status) {
477
+ return status === 'completed' || status === 'cancelled' || status === 'failed';
478
+ }
479
+
480
+ function statusFromFact(fact) {
481
+ if (fact.kind === 'script_completed') return 'completed';
482
+ if (fact.kind === 'script_cancelled') return 'cancelled';
483
+ if (fact.kind === 'script_failed') return 'failed';
484
+ if (fact.payloadSummary && fact.payloadSummary.status) return fact.payloadSummary.status;
485
+ return fact.kind;
486
+ }
487
+
488
+ function rejectBadLimit(args) {
489
+ if (args.limit == null) return null;
490
+ if (!Number.isInteger(args.limit) || args.limit < 1) {
491
+ return scriptError('unsupported_argument', { field: 'limit' });
492
+ }
493
+ return null;
494
+ }
495
+
496
+ function isLivePause(status) {
497
+ return status === 'paused_manual'
498
+ || status === 'paused_live'
499
+ || status === 'pause_requested'
500
+ || status === 'paused_ambiguous';
501
+ }
502
+
503
+ function persistRecord(record, kind, body) {
504
+ if (!record.store) return Promise.resolve({ ok: true, persisted: false });
505
+ const pending = record.persistTail.then(async () => {
506
+ try {
507
+ const result = await record.store.persist(kind, body);
508
+ return result?.ok ? result : { ok: false, error: result?.error || 'persist_failed' };
509
+ } catch (error) {
510
+ return { ok: false, error: error?.code || error?.message || 'persist_failed' };
511
+ }
512
+ });
513
+ record.persistTail = pending;
514
+ return pending;
515
+ }
516
+
517
+ async function commitTerminal(record, status, error, now, result) {
518
+ let revision = record.checkpointRevision + 1;
519
+ let resultRef = null;
520
+ if (status === 'completed' && record.store) {
521
+ const prepared = prepareScriptResult(result?.result, record.spec.policy.maxOutputBytes);
522
+ const saved = prepared.ok ? await persistRecord(record, 'result', {
523
+ operationId: record.operationId, revision, target: record.spec.target,
524
+ result: prepared.result, bytes: prepared.bytes, sha256: prepared.sha256,
525
+ originalSha256: prepared.originalSha256, representation: prepared.representation,
526
+ }) : prepared;
527
+ if (saved.ok && saved.persisted === true) resultRef = resultReference(saved, prepared);
528
+ else { status = 'failed'; error = prepared.ok ? 'result_not_persisted' : prepared.error; }
529
+ }
530
+ const checkpoint = {
531
+ ...codeStartCheckpoint({ spec: record.spec, hash: record.hash }, record.operationId),
532
+ revision,
533
+ stepId: record.lastCheckpoint?.name || 'start',
534
+ checkpoint: record.lastCheckpoint,
535
+ checkpointActionSequence: record.checkpointActionSequence,
536
+ actionSequence: record.actionSequence,
537
+ status,
538
+ error,
539
+ resultRef,
540
+ ...(record.recording ? { recording: record.recording.info() } : {}),
541
+ };
542
+ let persisted = await persistRecord(record, 'checkpoint', checkpoint);
543
+ // The terminal write shares a bounded partition with its result. A successful
544
+ // second write must not conceal eviction of the first one at a quota boundary.
545
+ if (persisted.ok && status === 'completed' && record.store) {
546
+ const verified = readScriptResult({ store: record.store, operationId: record.operationId });
547
+ if (!verified.ok) {
548
+ status = 'failed';
549
+ error = 'result_not_persisted';
550
+ resultRef = null;
551
+ revision += 1;
552
+ persisted = await persistRecord(record, 'checkpoint', {
553
+ ...checkpoint, revision, status, error, resultRef, resultError: verified.error,
554
+ });
555
+ }
556
+ }
557
+ record.checkpointRevision = revision;
558
+ record.finished = true;
559
+ record.status = persisted.ok ? status : 'failed';
560
+ record.error = persisted.ok ? error : 'terminal_not_persisted';
561
+ record.resultRef = persisted.ok ? resultRef : null;
562
+ const type = record.status === 'completed' ? 'script_completed'
563
+ : record.status === 'cancelled' ? 'script_cancelled' : 'script_failed';
564
+ emitRecord(record, type, {
565
+ resultRef: record.resultRef,
566
+ status: record.status,
567
+ error: record.error,
568
+ persisted: persisted.persisted === true,
569
+ }, now);
570
+ stopHeartbeat(record);
571
+ return record.status === 'completed' ? { ok: true, resultRef: record.resultRef }
572
+ : { ok: false, error: record.error || record.status };
573
+ }
574
+
575
+ function failedCall(command, error, actionId = null, ambiguous = false, dispatched = false) {
576
+ return { ok: false, command, error, result: null, ambiguous, dispatched, execution: { actionId } };
577
+ }
578
+
579
+ function trackPending(record, pending) {
580
+ record.pendingCalls.add(pending);
581
+ const settled = () => record.pendingCalls.delete(pending);
582
+ pending.then(settled, settled);
583
+ return pending;
584
+ }
585
+
586
+ function trackHost(host, record, now) {
587
+ function stopped() { return record.cancelling || record.finished || record.status === 'finishing'; }
588
+
589
+ function call(command, args = {}, options = {}) {
590
+ if (stopped()) return Promise.resolve(failedCall(command, 'runtime_stopped'));
591
+ const pending = runExecution({ signal: record.controller.signal, deadlineMs: record.deadlineMs,
592
+ timeoutMs: args.timeoutMs, mutation: isMutationCommand(command, args) }, () => callTracked(command, args, options))
593
+ .catch(error => failedCall(command, error.code || error.message, null, error.ambiguous === true, error.dispatched === true));
594
+ return trackPending(record, pending);
595
+ }
596
+
597
+ async function callTracked(command, args, options) {
598
+ if (isLivePause(record.status)) return failedCall(command, 'paused');
599
+ if (record.durableError || record.mutationUnknown) {
600
+ return failedCall(command, record.durableError || 'ambiguous', null, record.mutationUnknown);
601
+ }
602
+ const gate = authorizeCommand(command, record.spec.permissions);
603
+ if (!gate.ok) return rejectCall(gate.error);
604
+ let boundTarget = null;
605
+ if (command !== 'page-summary') {
606
+ try {
607
+ const binding = bindCommandTarget(command, args, record.spec.target, options.target);
608
+ boundTarget = binding.target;
609
+ args = binding.args;
610
+ } catch (error) {
611
+ return rejectCall(error.code || 'invalid_target', { field: error.field, message: error.message });
612
+ }
613
+ }
614
+ const mutation = isMutationCommand(command, args);
615
+ const actionSequence = mutation ? ++record.actionSequence : null;
616
+ const actionId = mutation ? `${record.operationId}:action-${actionSequence}` : null;
617
+ const startedAtMs = now();
618
+ if (mutation) record.inFlightMutations += 1;
619
+ emitRecord(record, 'call_started', { command, args, target: boundTarget, actionId }, now);
620
+ try {
621
+ if (mutation) {
622
+ const prepared = await persistRecord(record, 'dispatch-marker', {
623
+ operationId: record.operationId,
624
+ revision: record.checkpointRevision,
625
+ actionSequence,
626
+ actionId,
627
+ target: boundTarget,
628
+ actionSpecHash: checksumOf({ command, args }),
629
+ planStepId: actionId,
630
+ state: 'prepared',
631
+ startedAtMs,
632
+ });
633
+ if (!prepared.ok) {
634
+ record.durableError = 'dispatch_marker_not_persisted';
635
+ emitRecord(record, 'call_failed', { command, args, target: boundTarget, actionId, error: record.durableError }, now);
636
+ return failedCall(command, record.durableError);
637
+ }
638
+ }
639
+ let result;
640
+ try {
641
+ // The host uses this ID for its provider request and returned receipt.
642
+ // Caller-supplied dispatch IDs never become authoritative.
643
+ result = stopped() || isLivePause(record.status)
644
+ ? failedCall(command, isLivePause(record.status) ? 'paused' : 'runtime_stopped')
645
+ : await host.call(command, args, { ...options, dispatchActionId: actionId });
646
+ } catch (error) {
647
+ result = failedCall(command, error?.code || error?.message || String(error), actionId,
648
+ error?.ambiguous ?? mutation, error?.dispatched ?? null);
649
+ }
650
+ if (mutation) {
651
+ record.mutationUnknown ||= result.ambiguous === true;
652
+ const receipt = {
653
+ operationId: record.operationId,
654
+ target: boundTarget,
655
+ revision: record.checkpointRevision,
656
+ actionSequence,
657
+ actionId,
658
+ startedAtMs,
659
+ completedAtMs: now(),
660
+ mechanicalStatus: result.ok === false ? 'failed' : 'ok',
661
+ error: result.error || null,
662
+ ambiguous: result.ambiguous === true,
663
+ dispatched: result.dispatched ?? null,
664
+ executionReceipt: result.executionReceipt ?? null,
665
+ ...(result.executionReceipts === undefined ? {} : { executionReceipts: result.executionReceipts }),
666
+ evidenceRefs: result.evidence?.refs || [],
667
+ };
668
+ const persisted = await persistRecord(record, 'action-receipt', receipt);
669
+ record.actionsSinceCheckpoint.push({ kind: 'action-receipt', ...receipt });
670
+ if (!persisted.ok) {
671
+ record.durableError = 'action_receipt_not_persisted';
672
+ record.mutationUnknown = true;
673
+ return failedCall(command, record.durableError, actionId, true);
674
+ }
675
+ emitRecord(record, 'action_receipt', {
676
+ actionId,
677
+ target: boundTarget,
678
+ payloadSummary: {
679
+ mechanicalStatus: receipt.mechanicalStatus,
680
+ error: receipt.error,
681
+ ambiguous: receipt.ambiguous,
682
+ dispatched: receipt.dispatched,
683
+ args,
684
+ resolved: result.resolved,
685
+ matched: result.matched,
686
+ },
687
+ evidenceRefs: result.evidence?.refs,
688
+ timings: result.timings,
689
+ }, now);
690
+ }
691
+ if (record.recording) {
692
+ const saved = await record.recording.record({ kind: 'script-call', revision: record.checkpointRevision,
693
+ target: boundTarget,
694
+ data: { command, args, options, startedAtMs, completedAtMs: now(), envelope: result } });
695
+ if (!saved.ok) {
696
+ record.durableError = saved.error;
697
+ return failedCall(command, saved.error, actionId, result.ambiguous === true);
698
+ }
699
+ }
700
+ emitRecord(record, result.ok === false ? 'call_failed' : 'call_completed', {
701
+ command, args, target: boundTarget, actionId,
702
+ callId: result.execution?.callId,
703
+ error: result.error || null,
704
+ evidenceRefs: result.evidence?.refs,
705
+ observationId: result.evidence?.observationId,
706
+ source: result.evidence?.source,
707
+ window: result.evidence?.window,
708
+ coverage: result.evidence?.coverage,
709
+ }, now);
710
+ return result;
711
+ } finally {
712
+ if (mutation) {
713
+ record.inFlightMutations -= 1;
714
+ if (record.status === 'pause_requested' && record.inFlightMutations === 0) {
715
+ record.status = record.mutationUnknown ? 'paused_ambiguous' : 'paused_live';
716
+ emitRecord(record, 'paused', { status: record.status }, now);
717
+ pushControl(record);
718
+ }
719
+ }
720
+ }
721
+ async function rejectCall(error, detail = {}) {
722
+ const result = { ...unavailableEnvelope({ command, error, executionId: record.operationId, callId: null }),
723
+ dispatched: false, ambiguous: false, ...detail };
724
+ emitRecord(record, 'call_failed', { command, args, target: null, actionId: null, callId: null, error,
725
+ evidenceRefs: result.evidence.refs, window: result.evidence.window, coverage: result.evidence.coverage }, now);
726
+ if (record.recording) {
727
+ const saved = await record.recording.record({ kind: 'script-call', revision: record.checkpointRevision,
728
+ target: null, data: { command, args, options, startedAtMs: now(), completedAtMs: now(), envelope: result } });
729
+ if (!saved.ok) { record.durableError = saved.error; return failedCall(command, saved.error); }
730
+ }
731
+ return result;
732
+ }
733
+ }
734
+ async function assertTracked(assertion) {
735
+ const result = await host.assert(assertion);
736
+ if (record.recording) {
737
+ const saved = await record.recording.record({ kind: 'script-assertion', revision: record.checkpointRevision,
738
+ target: record.spec.target, data: { assertion, result } });
739
+ if (!saved.ok) {
740
+ record.durableError = saved.error;
741
+ return { ok: false, name: result.name, scope: result.scope, verdict: 'inconclusive', reason: saved.error };
742
+ }
743
+ }
744
+ emitRecord(record, `assertion_${result.verdict}`, {
745
+ name: result.name,
746
+ verdict: result.verdict,
747
+ scope: result.scope,
748
+ reason: result.reason,
749
+ predicateSummary: assertion.predicateSummary,
750
+ condition: assertion.condition,
751
+ requiredEvidence: assertion.requiredEvidence,
752
+ requireCoverage: assertion.requireCoverage,
753
+ refs: assertion.evidence && assertion.evidence.refs,
754
+ coverage: assertion.evidence && assertion.evidence.coverage,
755
+ observationId: assertion.evidence?.observationId,
756
+ window: assertion.evidence?.window,
757
+ source: assertion.evidence?.source,
758
+ }, now);
759
+ return result;
760
+ }
761
+
762
+ return {
763
+ call,
764
+ assert(assertion) {
765
+ if (stopped()) return Promise.resolve({ ok: false, verdict: 'inconclusive', reason: 'runtime_stopped' });
766
+ return trackPending(record, assertTracked(assertion));
767
+ },
768
+ };
769
+ }
770
+
771
+ function trackAgent(agent, record, now) {
772
+ return {
773
+ askAgent: (request) => {
774
+ record.decisionRevision += 1;
775
+ const requestId = `ask-${record.decisionRevision}`;
776
+ record.questionRevisions.set(requestId, record.decisionRevision);
777
+ record.currentRequestId = requestId;
778
+ record.status = 'waiting_for_agent';
779
+ emitRecord(record, 'agent_question_created', {
780
+ request,
781
+ revision: record.decisionRevision,
782
+ requestId,
783
+ }, now);
784
+ return agent.askAgent({ ...request, requestId });
785
+ },
786
+ decide: (requestId, decision) => agent.decide(requestId, decision),
787
+ };
788
+ }
789
+
790
+ function pushControl(record) {
791
+ if (typeof record.runtime.setControl !== 'function') return;
792
+ record.runtime.setControl({
793
+ status: record.status,
794
+ pauseReason: record.pauseReason,
795
+ checkpoint: record.lastCheckpoint,
796
+ });
797
+ }
798
+
799
+ function emitRecord(record, type, extra, now) {
800
+ const event = record.events.emit(type, extra, now);
801
+ projectEvent(record, type, extra, event.atMs);
802
+ if (type !== 'heartbeat') {
803
+ record.lastMaterialAtMs = event.atMs;
804
+ record.ledger.append(record, type, { ...extra, sequence: event.sequence }, event.atMs);
805
+ }
806
+ return event;
807
+ }
808
+
809
+ async function emitRuntimeRecord(record, type, extra, now) {
810
+ if (record.cancelling || record.finished || record.status === 'finishing') return { ok: false, error: 'runtime_stopped', persisted: false };
811
+ if (type === 'script_completed' || type === 'script_failed') {
812
+ return null;
813
+ }
814
+ if (type !== 'checkpoint') return emitRecord(record, type, extra, now);
815
+ if (record.mutationUnknown || record.inFlightMutations > 0) return { ok: false, error: 'ambiguous', persisted: false };
816
+ const checkpoint = { name: extra.name, state: extra.state };
817
+ const revision = record.checkpointRevision + 1;
818
+ let persisted = null;
819
+ if (record.store && typeof record.store.persist === 'function') {
820
+ persisted = await persistRecord(record,
821
+ 'checkpoint',
822
+ codeRuntimeCheckpoint(record, checkpoint, revision),
823
+ );
824
+ if (!persisted.ok) {
825
+ return {
826
+ ok: false,
827
+ persisted: false,
828
+ error: persisted.error || 'checkpoint_not_persisted',
829
+ };
830
+ }
831
+ }
832
+ record.checkpointRevision = revision;
833
+ record.lastCheckpoint = checkpoint;
834
+ record.checkpointActionSequence = record.actionSequence;
835
+ record.actionsSinceCheckpoint = [];
836
+ if (record.finished || record.status === 'finishing' || record.cancelling) return { ok: false, error: 'runtime_stopped', persisted: Boolean(persisted) };
837
+ emitRecord(record, type, extra, now);
838
+ return {
839
+ ok: true,
840
+ committed: true,
841
+ persisted: Boolean(persisted),
842
+ evidenceId: persisted && persisted.evidenceId,
843
+ };
844
+ }
845
+
846
+ function projectEvent(record, type, extra, timestampMs) {
847
+ record.projector.accept({
848
+ executionId: record.operationId,
849
+ revision: extra.revision,
850
+ kind: type,
851
+ target: scriptEventTarget(record, type, extra),
852
+ timestampMs,
853
+ actionId: extra.actionId,
854
+ parentFactId: extra.parentFactId,
855
+ payloadSummary: extra.payloadSummary,
856
+ evidenceRefs: extra.evidenceRefs,
857
+ timings: extra.timings,
858
+ });
859
+ if (type === 'progress') {
860
+ record.summary.apply({
861
+ kind: 'progress',
862
+ timestampMs,
863
+ stage: extra.stage,
864
+ message: extra.message,
865
+ });
866
+ return;
867
+ }
868
+ if (type === 'checkpoint') {
869
+ record.summary.apply({ kind: 'checkpoint_committed', timestampMs, name: extra.name });
870
+ return;
871
+ }
872
+ if (type === 'heartbeat') {
873
+ return;
874
+ }
875
+ record.summary.apply({
876
+ kind: type,
877
+ timestampMs,
878
+ command: extra.command,
879
+ evidenceRefs: extra.evidenceRefs,
880
+ name: extra.name,
881
+ scope: extra.scope,
882
+ request: extra.request,
883
+ });
884
+ }
885
+
886
+ function startHeartbeat(record, now) {
887
+ stopHeartbeat(record);
888
+ armHeartbeat(record, now);
889
+ }
890
+
891
+ function stopHeartbeat(record) {
892
+ if (!record.heartbeatTimer) return;
893
+ clearTimeout(record.heartbeatTimer);
894
+ clearInterval(record.heartbeatTimer);
895
+ record.heartbeatTimer = null;
896
+ }
897
+
898
+ function armHeartbeat(record, now) {
899
+ const idleMs = now() - record.lastMaterialAtMs;
900
+ const delayMs = idleMs >= 2000 ? 2000 : Math.max(0, 2000 - idleMs);
901
+ record.heartbeatTimer = setTimeout(() => {
902
+ maybeHeartbeat(record, now);
903
+ if (isTerminal(record.status) || !record.heartbeatTimer) return;
904
+ armHeartbeat(record, now);
905
+ }, delayMs);
906
+ if (typeof record.heartbeatTimer.unref === 'function') record.heartbeatTimer.unref();
907
+ }
908
+
909
+ function maybeHeartbeat(record, now) {
910
+ if (isTerminal(record.status)) {
911
+ stopHeartbeat(record);
912
+ return;
913
+ }
914
+ const atMs = now();
915
+ if (atMs - record.lastMaterialAtMs < 2000) return;
916
+ emitRecord(record, 'heartbeat', record.summary.heartbeat(atMs, record.finished ? record.status : 'alive'), now);
917
+ }
918
+
919
+ module.exports = { createScriptSupervisor, createDefaultRuntime, isCodeScript };