@margay/ccl-core 1.5.0-alpha.4 → 1.5.1-alpha.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 (65) hide show
  1. package/dist/cli.js +108954 -103254
  2. package/dist/sdk/background.d.ts +48 -0
  3. package/dist/sdk/background.js +87 -0
  4. package/dist/sdk/backgroundRunTypes.d.ts +101 -0
  5. package/dist/sdk/backgroundRunTypes.js +1 -0
  6. package/dist/sdk/cancellation.d.ts +15 -0
  7. package/dist/sdk/cancellation.js +18 -0
  8. package/dist/sdk/commandAdapter.d.ts +77 -0
  9. package/dist/sdk/commandAdapter.js +180 -0
  10. package/dist/sdk/commandProcess.d.ts +27 -0
  11. package/dist/sdk/commandProcess.js +130 -0
  12. package/dist/sdk/executionGuidance.d.ts +7 -0
  13. package/dist/sdk/executionGuidance.js +37 -0
  14. package/dist/sdk/index.d.ts +14 -0
  15. package/dist/sdk/index.js +9 -0
  16. package/dist/sdk/mcpServer.d.ts +9 -5
  17. package/dist/sdk/mcpServer.js +51 -9
  18. package/dist/sdk/operations/command.d.ts +24 -0
  19. package/dist/sdk/operations/command.js +48 -0
  20. package/dist/sdk/operations/host.d.ts +3 -0
  21. package/dist/sdk/operations/host.js +376 -0
  22. package/dist/sdk/operations/json.d.ts +6 -0
  23. package/dist/sdk/operations/json.js +24 -0
  24. package/dist/sdk/operations/reference.d.ts +15 -0
  25. package/dist/sdk/operations/reference.js +19 -0
  26. package/dist/sdk/operations/store.d.ts +26 -0
  27. package/dist/sdk/operations/store.js +300 -0
  28. package/dist/sdk/operations/types.d.ts +171 -0
  29. package/dist/sdk/operations/types.js +8 -0
  30. package/dist/sdk/pendingApprovalIdentity.d.ts +35 -0
  31. package/dist/sdk/pendingApprovalIdentity.js +27 -0
  32. package/dist/sdk/permissionDelegation.d.ts +19 -0
  33. package/dist/sdk/permissionDelegation.js +11 -0
  34. package/dist/sdk/query.d.ts +3 -1
  35. package/dist/sdk/query.js +142 -30
  36. package/dist/sdk/queryHandle.js +28 -8
  37. package/dist/sdk/queryLifecycle.d.ts +19 -0
  38. package/dist/sdk/queryLifecycle.js +93 -0
  39. package/dist/sdk/queryLifecycleTypes.d.ts +160 -0
  40. package/dist/sdk/queryLifecycleTypes.js +47 -0
  41. package/dist/sdk/scheduledOccurrenceTypes.d.ts +103 -0
  42. package/dist/sdk/scheduledOccurrenceTypes.js +12 -0
  43. package/dist/sdk/types.d.ts +21 -3
  44. package/docs/background-runs.md +229 -0
  45. package/docs/command-tool-recovery.md +62 -0
  46. package/docs/dev-quickstart.md +29 -7
  47. package/docs/error-recovery-usage.md +18 -0
  48. package/docs/hook-input-provenance.md +77 -0
  49. package/docs/index.md +2 -0
  50. package/docs/mcp-tool-recovery.md +82 -0
  51. package/docs/releases/1.5.0-alpha.5.md +26 -0
  52. package/docs/releases/1.5.0-alpha.6.md +15 -0
  53. package/docs/releases/1.5.1-alpha.1.md +11 -0
  54. package/docs/releases/1.5.1-alpha.2.md +17 -0
  55. package/docs/releases/1.5.1-alpha.3.md +23 -0
  56. package/docs/releases/1.5.1-alpha.md +22 -0
  57. package/docs/runtime-resources.md +6 -3
  58. package/examples/background-runs/agents/background-reviewer.md +6 -0
  59. package/examples/background-runs/agents/background-worker.md +6 -0
  60. package/examples/background-runs/worker-reviewer.mjs +42 -0
  61. package/examples/command-recovery/durable-server.mjs +44 -0
  62. package/examples/command-recovery/receipt-command.mjs +40 -0
  63. package/package.json +17 -2
  64. package/sdk/API.md +206 -1
  65. package/sdk/README.md +6 -0
@@ -0,0 +1,48 @@
1
+ import type { BackgroundRun, BackgroundRunAction, BackgroundRunApproval, BackgroundRunEvents, BackgroundRunLookup, BackgroundRunSelector, BackgroundRunSpec, BackgroundRunSource } from './backgroundRunTypes.js';
2
+ import type { OccurrenceCancelRequest, OccurrenceApprovalRequest, OccurrenceApprovalReceipt, OccurrenceLookup, OccurrenceEvents, ScheduledOccurrence } from './scheduledOccurrenceTypes.js';
3
+ export type BackgroundClientOptions = {
4
+ cwd: string;
5
+ cliPath: string;
6
+ execPath?: string;
7
+ launch?: {
8
+ model?: string;
9
+ settings?: string;
10
+ extraArgs?: string[];
11
+ };
12
+ env?: NodeJS.ProcessEnv;
13
+ timeoutMs?: number;
14
+ };
15
+ export declare class BackgroundClientError extends Error {
16
+ readonly code: string;
17
+ constructor(code: string, message: string);
18
+ }
19
+ /** Durable runs do not retain JS callbacks or in-process SDK MCP servers. Use
20
+ * persistent stdio/HTTP tools configured through launch instead. */
21
+ export declare function createBackgroundClient(options: BackgroundClientOptions): {
22
+ getOccurrence: (request: {
23
+ occurrenceId: string;
24
+ }) => Promise<OccurrenceLookup>;
25
+ listOccurrences: (request?: {
26
+ taskId?: string;
27
+ sourceSessionId?: string;
28
+ }) => Promise<ScheduledOccurrence[]>;
29
+ readOccurrenceEvents: (request: {
30
+ occurrenceId: string;
31
+ afterSequence?: number;
32
+ limit?: number;
33
+ }) => Promise<OccurrenceEvents>;
34
+ cancelOccurrence: (request: OccurrenceCancelRequest) => Promise<ScheduledOccurrence>;
35
+ approveOccurrence: (request: OccurrenceApprovalRequest) => Promise<OccurrenceApprovalReceipt>;
36
+ submitRun: (request: {
37
+ requestKey: string;
38
+ spec: BackgroundRunSpec;
39
+ source?: BackgroundRunSource;
40
+ }) => Promise<BackgroundRun>;
41
+ getRun: (request: BackgroundRunSelector) => Promise<BackgroundRunLookup>;
42
+ readRunEvents: (request: BackgroundRunSelector & {
43
+ cursor?: string;
44
+ limit?: number;
45
+ }) => Promise<BackgroundRunEvents>;
46
+ cancelRun: (request: BackgroundRunAction) => Promise<BackgroundRun>;
47
+ approveRun: (request: BackgroundRunApproval) => Promise<BackgroundRun>;
48
+ };
@@ -0,0 +1,87 @@
1
+ import { spawn } from 'node:child_process';
2
+ export class BackgroundClientError extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = 'BackgroundClientError';
8
+ }
9
+ }
10
+ /** Durable runs do not retain JS callbacks or in-process SDK MCP servers. Use
11
+ * persistent stdio/HTTP tools configured through launch instead. */
12
+ export function createBackgroundClient(options) {
13
+ for (const key of Object.keys(options)) {
14
+ if (!['cwd', 'cliPath', 'execPath', 'launch', 'env', 'timeoutMs'].includes(key))
15
+ throw new BackgroundClientError('unsupported_option', `Background execution cannot preserve option ${key}; use durable service configuration`);
16
+ }
17
+ async function invoke(command, request) {
18
+ // Serialization can throw (for example for a cyclic input); do it before
19
+ // allocating a subprocess that would otherwise wait forever for stdin.
20
+ const input = JSON.stringify(request);
21
+ const retryAdvice = command.includes('occurrence') ? 'Read the original occurrence and approvalDecisions. Retry approval only with the same actionId and payload.' : 'Query the original requestKey; do not submit a new key.';
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(options.execPath ?? process.execPath, [options.cliPath, 'daemon', command, '--cwd', options.cwd, '--request-stdin'], {
24
+ cwd: options.cwd, env: options.env ?? process.env, stdio: ['pipe', 'pipe', 'pipe'],
25
+ });
26
+ // A UTF-8 character may straddle pipe chunks. Decode incrementally so
27
+ // saved event text is not silently replaced with U+FFFD on readback.
28
+ child.stdout.setEncoding('utf8');
29
+ child.stderr.setEncoding('utf8');
30
+ let stdout = '', stderr = '', settled = false;
31
+ let forceStop;
32
+ const finish = (error, value) => {
33
+ if (settled)
34
+ return;
35
+ settled = true;
36
+ clearTimeout(timer);
37
+ if (error && child.exitCode === null && child.signalCode === null) {
38
+ child.kill('SIGTERM');
39
+ forceStop = setTimeout(() => child.kill('SIGKILL'), 1000);
40
+ forceStop.unref();
41
+ }
42
+ if (error)
43
+ reject(error);
44
+ else
45
+ resolve(value);
46
+ };
47
+ const timer = setTimeout(() => {
48
+ child.kill('SIGTERM');
49
+ finish(new BackgroundClientError('ack_unknown', `Background acknowledgement timed out. ${retryAdvice}`));
50
+ }, options.timeoutMs ?? 45_000);
51
+ child.on('error', error => finish(error));
52
+ child.stdin.on('error', error => finish(error));
53
+ child.stdout.on('data', chunk => { stdout += chunk; if (Buffer.byteLength(stdout) > 32 * 1024 * 1024) {
54
+ child.kill('SIGTERM');
55
+ finish(new BackgroundClientError('response_too_large', `Response exceeds 32 MiB. ${command === 'events' ? 'Use limit: 1; if one complete event still exceeds 32 MiB, ' : ''}read the original result using the CLI daemon ${command === 'events' ? 'events' : 'get'} command redirected to a task-owned file. Do not resubmit with a new key.`));
56
+ } });
57
+ child.stderr.on('data', chunk => { stderr = (stderr + chunk).slice(-4096); });
58
+ child.on('close', () => {
59
+ if (forceStop)
60
+ clearTimeout(forceStop);
61
+ try {
62
+ const reply = JSON.parse(stdout);
63
+ if (reply.ok !== true)
64
+ finish(new BackgroundClientError(reply.code ?? 'request_failed', reply.error ?? 'Background request failed'));
65
+ else
66
+ finish(undefined, reply.result);
67
+ }
68
+ catch {
69
+ finish(new BackgroundClientError('invalid_response', `No complete background response. ${retryAdvice} ${stderr}`));
70
+ }
71
+ });
72
+ child.stdin.end(input);
73
+ });
74
+ }
75
+ return {
76
+ getOccurrence: (request) => invoke('get-occurrence', request),
77
+ listOccurrences: (request = {}) => invoke('list-occurrences', request),
78
+ readOccurrenceEvents: (request) => invoke('occurrence-events', request),
79
+ cancelOccurrence: (request) => invoke('cancel-occurrence', request),
80
+ approveOccurrence: (request) => invoke('approve-occurrence', request),
81
+ submitRun: (request) => invoke('submit', { ...request, launch: options.launch }),
82
+ getRun: (request) => invoke('get', request),
83
+ readRunEvents: (request) => invoke('events', request),
84
+ cancelRun: (request) => invoke('cancel-run', request),
85
+ approveRun: (request) => invoke('approve-run', request),
86
+ };
87
+ }
@@ -0,0 +1,101 @@
1
+ /** Local daemon submissions. These records describe execution, not business acceptance. */
2
+ export type BackgroundRunSpec = {
3
+ prompt: string;
4
+ systemPrompt?: string;
5
+ model?: string;
6
+ maxTurns?: number;
7
+ agent?: string;
8
+ /** Optional restriction; omission preserves the existing default permission mode. */
9
+ permissionMode?: 'plan';
10
+ };
11
+ export type BackgroundRun = {
12
+ runId: string;
13
+ requestKey: string;
14
+ scope: string;
15
+ spec: BackgroundRunSpec;
16
+ specHash: string;
17
+ configId: string;
18
+ source?: BackgroundRunSource;
19
+ launchRecovery?: BackgroundLaunchRecovery;
20
+ dispatchBlockedReason?: 'context_unavailable';
21
+ acceptedAt: number;
22
+ updatedAt: number;
23
+ state: 'queued' | 'starting' | 'running' | 'waiting_permission' | 'succeeded' | 'completed_with_issues' | 'failed' | 'interrupted' | 'cancelled';
24
+ sessionId: string;
25
+ generation: string;
26
+ revision: number;
27
+ cancellation: 'none' | 'requested';
28
+ permissions: BackgroundPermission[];
29
+ operationReferences: import('./operations/reference.js').OperationReference[];
30
+ result?: string;
31
+ error?: string;
32
+ toolErrors: unknown[];
33
+ deniedToolUseIds: string[];
34
+ /** Latest captured request diagnostic, not the current run state or proof that
35
+ * earlier tools did not execute. Inspect state/events/operationReferences too. */
36
+ latestDiagnostic?: import('./errorDiagnostics.js').RecoveryDiagnostic;
37
+ };
38
+ /** Source descriptors only. Credential values never belong in durable facts. */
39
+ export type BackgroundLaunchRecovery = {
40
+ version: 1;
41
+ launchDigest: string;
42
+ configurationFiles: string[];
43
+ credentialEnvKeys: string[];
44
+ inlineCredentialPaths: string[];
45
+ };
46
+ export type BackgroundRunSource = {
47
+ sessionId: string;
48
+ assistantId?: string;
49
+ toolUseId?: string;
50
+ parentRunId?: string;
51
+ };
52
+ export type BackgroundPermission = {
53
+ toolUseId: string;
54
+ toolName: string;
55
+ input: unknown;
56
+ assistantId: string;
57
+ decision?: 'allow' | 'deny';
58
+ };
59
+ export type BackgroundRunEvent = {
60
+ runId: string;
61
+ generation: string;
62
+ sequence: number;
63
+ at: number;
64
+ kind: string;
65
+ snapshot: BackgroundRun;
66
+ data?: unknown;
67
+ };
68
+ export type BackgroundRunEvents = {
69
+ events: BackgroundRunEvent[];
70
+ nextCursor: string;
71
+ hasMore: boolean;
72
+ };
73
+ export type BackgroundRunAction = {
74
+ runId: string;
75
+ expectedRevision: number;
76
+ actionId: string;
77
+ };
78
+ export type BackgroundRunApproval = BackgroundRunAction & {
79
+ toolUseId: string;
80
+ decision: 'allow' | 'deny';
81
+ };
82
+ export type BackgroundRunLookup = {
83
+ status: 'not_found';
84
+ ownerAvailable: boolean;
85
+ } | {
86
+ status: 'found';
87
+ snapshot: BackgroundRun;
88
+ observation: {
89
+ checkedAt: number;
90
+ ownerAvailable: boolean;
91
+ executorLock: 'held' | 'free' | 'unknown';
92
+ externalLiveness: 'unknown';
93
+ };
94
+ allowedActions: Array<'get' | 'events' | 'cancel' | 'approve'>;
95
+ dispatchBlockedReason?: 'configuration_changed' | 'owner_unavailable' | 'context_unavailable';
96
+ };
97
+ export type BackgroundRunSelector = {
98
+ requestKey: string;
99
+ } | {
100
+ runId: string;
101
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod/v4';
2
+ /** Control-channel provenance, not the identity or intent of a person. */
3
+ export declare const hostCancellationSchema: z.ZodObject<{
4
+ origin: z.ZodLiteral<"host">;
5
+ reason: z.ZodEnum<{
6
+ cancelled: "cancelled";
7
+ timeout: "timeout";
8
+ connection_closed: "connection_closed";
9
+ callback_error: "callback_error";
10
+ permission_denied: "permission_denied";
11
+ }>;
12
+ }, z.core.$strict>;
13
+ export type HostCancellation = z.infer<typeof hostCancellationSchema>;
14
+ export declare function readHostCancellation(value: unknown): HostCancellation | undefined;
15
+ export declare function hostCancellationMessage(cancellation: HostCancellation, execution?: 'not_started' | 'unknown'): string;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod/v4';
2
+ /** Control-channel provenance, not the identity or intent of a person. */
3
+ export const hostCancellationSchema = z.object({
4
+ origin: z.literal('host'),
5
+ reason: z.enum(['cancelled', 'timeout', 'connection_closed', 'callback_error', 'permission_denied']),
6
+ }).strict();
7
+ export function readHostCancellation(value) {
8
+ const parsed = hostCancellationSchema.safeParse(value);
9
+ return parsed.success ? parsed.data : undefined;
10
+ }
11
+ export function hostCancellationMessage(cancellation, execution = 'unknown') {
12
+ return `Host control channel ended this turn (${JSON.stringify(cancellation)}). ` +
13
+ (cancellation.reason === 'permission_denied'
14
+ ? 'The host ended the turn after a permission denial; preserve the recorded decision without inferring who made it. '
15
+ : 'This lifecycle cancellation is not a user permission refusal or a user preference. ') +
16
+ (execution === 'not_started' ? 'This tool call was not executed. ' : 'A missing result does not establish whether an in-flight operation had effects. ') +
17
+ 'Stop this turn. On a later authorized continuation, retain completed results and check existing execution evidence before repeating side effects. This cancellation grants no permission and does not revoke unrelated existing authorization.';
18
+ }
@@ -0,0 +1,77 @@
1
+ import { type CommandProcessResult } from './commandProcess.js';
2
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export type CommandOperation = Readonly<{
6
+ operationId: string;
7
+ requestKey: string;
8
+ adapterId: string;
9
+ adapterVersion: string;
10
+ targetIdentity: string;
11
+ bindingDigest: string;
12
+ inputDigest: string;
13
+ input: JsonValue;
14
+ createdAt: number;
15
+ }>;
16
+ export type CommandOutcome = {
17
+ status: 'applied';
18
+ receipt: JsonValue;
19
+ result?: JsonValue;
20
+ } | {
21
+ status: 'in_progress';
22
+ handle: string;
23
+ } | {
24
+ status: 'unknown';
25
+ reason: string;
26
+ } | {
27
+ status: 'unsupported';
28
+ reason: string;
29
+ };
30
+ export type CommandExecution = Readonly<{
31
+ operation: CommandOperation;
32
+ attemptId: string;
33
+ process: CommandProcessResult | null;
34
+ outcome: CommandOutcome;
35
+ }>;
36
+ export type CommandDefinition = {
37
+ cmd: string;
38
+ args?: readonly string[];
39
+ cwd?: string;
40
+ env?: Record<string, string>;
41
+ timeoutMs: number;
42
+ maxOutputBytes?: number;
43
+ inheritEnv?: boolean;
44
+ };
45
+ export type CommandAdapterSpec = {
46
+ adapterId: string;
47
+ adapterVersion: string;
48
+ targetIdentity: string;
49
+ execute: CommandDefinition;
50
+ query?: CommandDefinition;
51
+ /** Trusted external service guarantee, never a model-supplied capability. */
52
+ idempotency?: {
53
+ scope: string;
54
+ retentionMs: number;
55
+ };
56
+ /** Check the real account/version/target before recovery, not merely config strings. */
57
+ verifyIdentity?: (operation: CommandOperation) => Promise<boolean>;
58
+ };
59
+ export interface CommandAdapter {
60
+ prepare(input: JsonValue, options?: {
61
+ requestKey?: string;
62
+ }): CommandOperation;
63
+ execute(operation: CommandOperation, options?: {
64
+ signal?: AbortSignal;
65
+ }): Promise<CommandExecution>;
66
+ queryOutcome(operation: CommandOperation, options?: {
67
+ signal?: AbortSignal;
68
+ }): Promise<CommandExecution>;
69
+ retry(operation: CommandOperation, options: {
70
+ signal?: AbortSignal;
71
+ authorize: (operation: CommandOperation) => Promise<boolean>;
72
+ }): Promise<CommandExecution>;
73
+ }
74
+ /** Internal host bridge. Not exported by sdk/index: only verified durable records may enter. */
75
+ export declare function restoreCommandOperation(adapter: CommandAdapter, operation: CommandOperation, previouslyDispatched: boolean): CommandOperation;
76
+ /** Live command adapter. Durable ownership and rehydration are provided separately by the host. */
77
+ export declare function createCommandAdapter(spec: CommandAdapterSpec): CommandAdapter;
@@ -0,0 +1,180 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { runCommandProcess } from './commandProcess.js';
3
+ function canonical(value) {
4
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
5
+ return JSON.stringify(value);
6
+ if (typeof value === 'number' && Number.isFinite(value))
7
+ return JSON.stringify(value);
8
+ if (Array.isArray(value))
9
+ return `[${value.map(canonical).join(',')}]`;
10
+ if (value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype) {
11
+ return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonical(value[k])}`).join(',')}}`;
12
+ }
13
+ throw new Error('Expected a finite JSON value');
14
+ }
15
+ function freeze(value) {
16
+ if (value && typeof value === 'object') {
17
+ for (const v of Object.values(value))
18
+ freeze(v);
19
+ Object.freeze(value);
20
+ }
21
+ return value;
22
+ }
23
+ function digest(value) { return createHash('sha256').update(canonical(value)).digest('hex'); }
24
+ const restoreHandlers = new WeakMap();
25
+ /** Internal host bridge. Not exported by sdk/index: only verified durable records may enter. */
26
+ export function restoreCommandOperation(adapter, operation, previouslyDispatched) {
27
+ const restore = restoreHandlers.get(adapter);
28
+ if (!restore)
29
+ throw new Error('Unknown command adapter instance');
30
+ return restore(operation, previouslyDispatched);
31
+ }
32
+ /** Live command adapter. Durable ownership and rehydration are provided separately by the host. */
33
+ export function createCommandAdapter(spec) {
34
+ for (const s of [spec.adapterId, spec.adapterVersion, spec.targetIdentity]) {
35
+ if (!s)
36
+ throw new Error('Adapter identity must be nonempty');
37
+ }
38
+ const verifyIdentity = spec.verifyIdentity;
39
+ const snapshot = (command) => {
40
+ if (!command.cmd || !Number.isFinite(command.timeoutMs) || command.timeoutMs <= 0 || command.timeoutMs > 2_147_483_647)
41
+ throw new Error('Invalid command configuration');
42
+ if (command.maxOutputBytes !== undefined && (!Number.isSafeInteger(command.maxOutputBytes) || command.maxOutputBytes < 1))
43
+ throw new Error('Invalid output limit');
44
+ return freeze({ cmd: command.cmd, args: [...(command.args ?? [])], cwd: command.cwd ?? process.cwd(),
45
+ env: { ...command.env }, inheritEnv: command.inheritEnv !== false, timeoutMs: command.timeoutMs, maxOutputBytes: command.maxOutputBytes ?? 8 * 1024 * 1024 });
46
+ };
47
+ const execute = snapshot(spec.execute), query = spec.query ? snapshot(spec.query) : undefined;
48
+ const idempotency = spec.idempotency ? freeze({ ...spec.idempotency }) : undefined;
49
+ if (idempotency && (!idempotency.scope || !Number.isFinite(idempotency.retentionMs) || idempotency.retentionMs <= 0))
50
+ throw new Error('Invalid idempotency contract');
51
+ const identity = { adapterId: spec.adapterId, adapterVersion: spec.adapterVersion, targetIdentity: spec.targetIdentity };
52
+ const binding = () => digest({ ...identity, execute: { ...execute, env: execute.inheritEnv ? { ...process.env, ...execute.env } : execute.env },
53
+ query: query ? { ...query, env: query.inheritEnv ? { ...process.env, ...query.env } : query.env } : null, idempotency: idempotency ?? null });
54
+ const bindingDigest = binding();
55
+ const states = new WeakMap();
56
+ const keys = new Map();
57
+ function stateFor(operation) {
58
+ const state = states.get(operation);
59
+ if (!state)
60
+ throw new Error('Operation is not owned by this adapter instance');
61
+ if (binding() !== state.bindingDigest)
62
+ throw new Error('Command environment binding changed; verify identity before recovery');
63
+ return state;
64
+ }
65
+ function result(operation, outcome, process = null, attemptId = randomUUID()) {
66
+ return freeze({ operation, attemptId, process, outcome });
67
+ }
68
+ async function run(operation, command, signal) {
69
+ const state = stateFor(operation);
70
+ const attemptId = randomUUID();
71
+ const envelope = { schemaVersion: 1, ...operation, attemptId };
72
+ const processResult = await runCommandProcess({ ...command, input: JSON.stringify(envelope), signal });
73
+ let outcome = { status: 'unknown', reason: 'Command did not return a valid bound receipt' };
74
+ if (processResult.exit_code === 0 && !processResult.error && !processResult.cancelled && !processResult.timed_out) {
75
+ try {
76
+ const reply = JSON.parse(processResult.output);
77
+ const matches = reply.schemaVersion === 1 && ['operationId', 'requestKey', 'adapterId', 'adapterVersion', 'targetIdentity', 'inputDigest', 'bindingDigest'].every(k => reply[k] === envelope[k]);
78
+ if (matches) {
79
+ if (reply.status === 'applied' && reply.receipt !== undefined && reply.receipt !== null) {
80
+ canonical(reply.receipt);
81
+ if (reply.result !== undefined)
82
+ canonical(reply.result);
83
+ outcome = { status: 'applied', receipt: reply.receipt, ...(reply.result !== undefined ? { result: reply.result } : {}) };
84
+ }
85
+ else if (reply.status === 'in_progress' && typeof reply.handle === 'string' && reply.handle)
86
+ outcome = { status: 'in_progress', handle: reply.handle };
87
+ else if (reply.status === 'unknown' && typeof reply.reason === 'string')
88
+ outcome = { status: 'unknown', reason: reply.reason };
89
+ }
90
+ }
91
+ catch { /* Invalid/truncated output never proves an effect. */ }
92
+ }
93
+ const execution = result(operation, outcome, processResult, attemptId);
94
+ if (outcome.status === 'applied')
95
+ state.accepted ??= execution;
96
+ return state.accepted ?? execution;
97
+ }
98
+ const adapter = {
99
+ prepare(input, options = {}) {
100
+ const text = canonical(input), inputDigest = digest(input);
101
+ const requestKey = options.requestKey ?? randomUUID();
102
+ if (!requestKey)
103
+ throw new Error('requestKey must be nonempty');
104
+ const existing = keys.get(requestKey);
105
+ if (existing) {
106
+ if (existing.inputDigest !== inputDigest)
107
+ throw new Error('requestKey is already bound to different input');
108
+ stateFor(existing);
109
+ return existing;
110
+ }
111
+ if (binding() !== bindingDigest)
112
+ throw new Error('Command environment binding changed');
113
+ const operation = freeze({ operationId: randomUUID(), requestKey, ...identity, bindingDigest,
114
+ inputDigest, input: JSON.parse(text), createdAt: Date.now() });
115
+ states.set(operation, { active: false, bindingDigest });
116
+ keys.set(requestKey, operation);
117
+ return operation;
118
+ },
119
+ execute(operation, options = {}) {
120
+ const state = stateFor(operation);
121
+ if (state.first)
122
+ return state.first;
123
+ state.active = true;
124
+ state.first = run(operation, execute, options.signal).finally(() => { state.active = false; });
125
+ return state.first;
126
+ },
127
+ async queryOutcome(operation, options = {}) {
128
+ const state = stateFor(operation);
129
+ if (!state.first)
130
+ throw new Error('Operation has not been submitted');
131
+ if (state.accepted)
132
+ return state.accepted;
133
+ if (!query)
134
+ return result(operation, { status: 'unsupported', reason: 'No query command configured' });
135
+ if (verifyIdentity && !await verifyIdentity(operation))
136
+ return result(operation, { status: 'unknown', reason: 'External target identity could not be verified' });
137
+ return run(operation, query, options.signal);
138
+ },
139
+ async retry(operation, options) {
140
+ const state = stateFor(operation);
141
+ if (!state.first)
142
+ throw new Error('Use execute for an operation that has not been submitted');
143
+ if (state.accepted)
144
+ return state.accepted;
145
+ if (state.active)
146
+ throw new Error('Operation execution or recovery is already active');
147
+ const eligible = () => idempotency && Date.now() - operation.createdAt < idempotency.retentionMs;
148
+ if (!eligible() || !verifyIdentity)
149
+ throw new Error('Retry requires valid idempotency and external identity verification');
150
+ state.active = true;
151
+ try {
152
+ if (!await options.authorize(operation))
153
+ throw new Error('Retry permission denied');
154
+ if (!await verifyIdentity(operation))
155
+ throw new Error('External target identity changed');
156
+ stateFor(operation);
157
+ if (state.accepted)
158
+ return state.accepted;
159
+ if (!eligible())
160
+ throw new Error('Idempotency retention expired during authorization');
161
+ return await run(operation, execute, options.signal);
162
+ }
163
+ finally {
164
+ state.active = false;
165
+ }
166
+ },
167
+ };
168
+ restoreHandlers.set(adapter, (record, previouslyDispatched) => {
169
+ if (record.adapterId !== identity.adapterId || record.adapterVersion !== identity.adapterVersion || record.targetIdentity !== identity.targetIdentity || digest(record.input) !== record.inputDigest || !record.operationId || !record.requestKey || !Number.isFinite(record.createdAt))
170
+ throw new Error('Invalid restored command operation binding');
171
+ const operation = freeze(JSON.parse(canonical(record)));
172
+ const state = { active: false, bindingDigest: binding() };
173
+ if (previouslyDispatched)
174
+ state.first = Promise.resolve(result(operation, { status: 'unknown', reason: 'Original command was dispatched; recovering its durable history' }));
175
+ states.set(operation, state);
176
+ keys.set(operation.requestKey, operation);
177
+ return operation;
178
+ });
179
+ return adapter;
180
+ }
@@ -0,0 +1,27 @@
1
+ export type CommandProcessInput = {
2
+ cmd: string;
3
+ args: readonly string[];
4
+ timeoutMs: number;
5
+ input?: string;
6
+ env?: NodeJS.ProcessEnv;
7
+ inheritEnv?: boolean;
8
+ cwd?: string;
9
+ signal?: AbortSignal;
10
+ maxOutputBytes?: number;
11
+ };
12
+ export type CommandProcessResult = {
13
+ output: string;
14
+ stderr: string;
15
+ exit_code: number;
16
+ duration_ms: number;
17
+ timed_out: boolean;
18
+ cancelled: boolean;
19
+ started: boolean;
20
+ pid?: number;
21
+ error?: string;
22
+ dispatch: 'not_dispatched' | 'dispatched';
23
+ effect: 'not_dispatched' | 'unknown';
24
+ cancellation: 'not_requested' | 'requested';
25
+ };
26
+ /** Run exactly once, without a shell. Local termination never proves remote effects stopped. */
27
+ export declare function runCommandProcess(opts: CommandProcessInput): Promise<CommandProcessResult>;