@tomflow/proflow-execution-runtime 0.1.19 → 0.1.20

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @tomflow/proflow-execution-runtime
2
2
 
3
+ ## 0.1.20
4
+
5
+ ### Patch Changes
6
+
7
+ - fix(execution-runtime): unify formal effect identity
8
+
3
9
  ## 0.1.19
4
10
 
5
11
  ### Patch Changes
@@ -6,7 +6,7 @@ export declare const behaviorAdapter: {
6
6
  ok: true;
7
7
  status: "SUCCEEDED";
8
8
  moduleRef: "execution-runtime";
9
- moduleVersion: "0.1.19";
9
+ moduleVersion: "0.1.20";
10
10
  data: {
11
11
  endpoint: string;
12
12
  transportCredentialFile: string;
@@ -23,7 +23,7 @@ export declare const behaviorAdapter: {
23
23
  readonly ok: true;
24
24
  readonly status: "SUCCEEDED";
25
25
  readonly moduleRef: "execution-runtime";
26
- readonly moduleVersion: "0.1.19";
26
+ readonly moduleVersion: "0.1.20";
27
27
  };
28
28
  observedEffects: ("Persists execution state, output references and evidence in WAL SQLite" | "Runs the single backend Execution Runtime service")[];
29
29
  }>;
@@ -33,7 +33,7 @@ export declare const behaviorAdapter: {
33
33
  ok: true;
34
34
  status: "SUCCEEDED";
35
35
  moduleRef: "execution-runtime";
36
- moduleVersion: "0.1.19";
36
+ moduleVersion: "0.1.20";
37
37
  data: {
38
38
  setupStatus: "READY";
39
39
  runtimeStatus: "RUNNING" | "STOPPED";
@@ -54,7 +54,7 @@ export declare const behaviorAdapter: {
54
54
  ok: true;
55
55
  status: "SUCCEEDED";
56
56
  moduleRef: "execution-runtime";
57
- moduleVersion: "0.1.19";
57
+ moduleVersion: "0.1.20";
58
58
  data: {
59
59
  endpoint: string;
60
60
  transportCredentialFile: string;
@@ -71,7 +71,7 @@ export declare const behaviorAdapter: {
71
71
  ok: true;
72
72
  status: "SUCCEEDED";
73
73
  moduleRef: "execution-runtime";
74
- moduleVersion: "0.1.19";
74
+ moduleVersion: "0.1.20";
75
75
  data: {
76
76
  docs: string;
77
77
  };
@@ -84,7 +84,7 @@ export declare const behaviorAdapter: {
84
84
  ok: true;
85
85
  status: "SUCCEEDED";
86
86
  moduleRef: "execution-runtime";
87
- moduleVersion: "0.1.19";
87
+ moduleVersion: "0.1.20";
88
88
  data: unknown;
89
89
  };
90
90
  observedEffects: ("Persists execution state, output references and evidence in WAL SQLite" | "Runs the single backend Execution Runtime service")[];
@@ -92,7 +92,7 @@ export declare const behaviorAdapter: {
92
92
  result: {
93
93
  contract: "deployment.result.v1";
94
94
  moduleRef: "execution-runtime";
95
- moduleVersion: "0.1.19";
95
+ moduleVersion: "0.1.20";
96
96
  ok: false;
97
97
  status: "FAILED";
98
98
  error: {
@@ -109,7 +109,7 @@ export declare const behaviorAdapter: {
109
109
  readonly ok: true;
110
110
  readonly status: "SUCCEEDED";
111
111
  readonly moduleRef: "execution-runtime";
112
- readonly moduleVersion: "0.1.19";
112
+ readonly moduleVersion: "0.1.20";
113
113
  };
114
114
  observedEffects: ("Persists execution state, output references and evidence in WAL SQLite" | "Runs the single backend Execution Runtime service")[];
115
115
  } | {
@@ -118,11 +118,11 @@ export declare const behaviorAdapter: {
118
118
  readonly ok: true;
119
119
  readonly status: "SUCCEEDED";
120
120
  readonly moduleRef: "execution-runtime";
121
- readonly moduleVersion: "0.1.19";
121
+ readonly moduleVersion: "0.1.20";
122
122
  } | {
123
123
  contract: "deployment.result.v1";
124
124
  moduleRef: "execution-runtime";
125
- moduleVersion: "0.1.19";
125
+ moduleVersion: "0.1.20";
126
126
  ok: false;
127
127
  status: "FAILED";
128
128
  error: {
@@ -3,7 +3,7 @@ export declare const descriptor: {
3
3
  readonly contractVersion: "1.0.0";
4
4
  readonly moduleRef: "execution-runtime";
5
5
  readonly packageName: "@tomflow/proflow-execution-runtime";
6
- readonly moduleVersion: "0.1.19";
6
+ readonly moduleVersion: "0.1.20";
7
7
  readonly kind: "service";
8
8
  readonly templateVersion: "1.0.0";
9
9
  readonly platformCompatibility: ">=1.0.0 <2.0.0";
@@ -3,7 +3,7 @@ export const descriptor = {
3
3
  contractVersion: "1.0.0",
4
4
  moduleRef: "execution-runtime",
5
5
  packageName: "@tomflow/proflow-execution-runtime",
6
- moduleVersion: "0.1.19",
6
+ moduleVersion: "0.1.20",
7
7
  kind: "service",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -1,4 +1,4 @@
1
- import { type ExecuteCapabilityRequest, type ExecutionArtifactRecord, type ExecutionErrorCode, type ExecutionRecord, type ReadExecutionOutputResponse } from "@tomflow/proflow-execution-contracts";
1
+ import { type ExecuteCapabilityRequest, type ExecutionArtifactRecord, type ExecutionErrorCode, type ExecutionRecord, type MonitorEffectReceipt, type ReadExecutionOutputResponse } from "@tomflow/proflow-execution-contracts";
2
2
  import type { ExecutionExecutorPort, ExecutorPrecondition } from "./executor-port.ts";
3
3
  export declare const moduleRef: "execution-runtime";
4
4
  type MaybePromise<Value> = Value | Promise<Value>;
@@ -62,6 +62,7 @@ export declare class ExecutionRuntimeError extends Error {
62
62
  export declare function executionInputFingerprint(input: unknown): string;
63
63
  export declare function createExecutionRuntime(options: ExecutionRuntimeOptions): Promise<Readonly<{
64
64
  executeCapability: (input: unknown) => Promise<ExecutionRecord>;
65
+ executeEffect: (input: unknown) => Promise<MonitorEffectReceipt>;
65
66
  lookupExecutionIntent: (input: unknown) => ExecutionRecord;
66
67
  listExecutionObserverSignals: (limit?: number) => {
67
68
  signalRef: string;
package/dist/src/index.js CHANGED
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { appendFile, mkdir } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { DatabaseSync } from "node:sqlite";
5
- import { browserCapabilityIds, EXECUTION_CONTRACT_VERSION, executionErrorCodes, parseCancelExecutionRequest, parseExecuteCapabilityRequest, parseExecutionArtifactRecord, parseExecutionRecord, parseExecutionRef, parseReadExecutionOutputRequest, } from "@tomflow/proflow-execution-contracts";
5
+ import { browserCapabilityIds, EXECUTION_CONTRACT_VERSION, executionErrorCodes, parseCancelExecutionRequest, parseExecuteCapabilityRequest, parseExecutionArtifactRecord, parseExecutionRecord, parseMonitorEffectRequest, parseMonitorEffectReceipt, parseExecutionRef, parseReadExecutionOutputRequest, } from "@tomflow/proflow-execution-contracts";
6
6
  import { createExecutionApprovalOwner } from "./approval-owner.js";
7
7
  export const moduleRef = "execution-runtime";
8
8
  export class ExecutionRuntimeError extends Error {
@@ -17,6 +17,12 @@ export class ExecutionRuntimeError extends Error {
17
17
  }
18
18
  const sensitive = /(?:authorization|bearer|api[_-]?key|token|password|cookie|private[_-]?key|secret)/i;
19
19
  const browserIds = new Set(browserCapabilityIds);
20
+ const formalEffectCapabilities = new Set([
21
+ "monitor.chat.create",
22
+ "monitor.chat.submit",
23
+ "browser.action",
24
+ ]);
25
+ const isFormalEffectCapability = (capability) => formalEffectCapabilities.has(capability);
20
26
  function canonical(value) {
21
27
  if (Array.isArray(value))
22
28
  return value.map(canonical);
@@ -39,7 +45,7 @@ function scrubEmbeddedSecrets(value) {
39
45
  .replace(/((?:authorization|api[_-]?key|token|password|cookie|secret|private[_-]?key)\s*[:=]\s*)([^\s,;"'}]+)/gi, (_match, prefix, secretValue) => typeof prefix === "string" && typeof secretValue === "string"
40
46
  ? `${prefix}[SECRET_REF:${sha(secretValue)}]`
41
47
  : _match)
42
- .replace(/("(?:password|token|secret|api[_-]?key|authorization|cookie|private[_-]?key)"\s*:\s*")((?:[^"\\]|\\.)*)(")/gi, (_match, prefix, secretValue, suffix) => typeof prefix === "string" &&
48
+ .replace(/("[A-Za-z0-9_-]*(?:password|token|secret|api[_-]?key|authorization|cookie|private[_-]?key)[A-Za-z0-9_-]*"\s*:\s*")((?:[^"\\]|\\.)*)(")/gi, (_match, prefix, secretValue, suffix) => typeof prefix === "string" &&
43
49
  typeof secretValue === "string" &&
44
50
  typeof suffix === "string"
45
51
  ? `${prefix}[SECRET_REF:${sha(secretValue)}]${suffix}`
@@ -62,13 +68,11 @@ function protectedValue(value, key = "") {
62
68
  continue;
63
69
  }
64
70
  if (flag[1] !== undefined && flag[1].length > 0) {
65
- // `--token=value` — keep the flag, hash the value.
66
71
  items.push(`${item.slice(0, item.length - flag[1].length)}[SECRET_REF:${sha(flag[1])}]`);
67
72
  }
68
73
  else {
69
74
  items.push(item);
70
75
  if (i + 1 < value.length) {
71
- // `--token value` — the following positional arg is the secret.
72
76
  items.push(`[SECRET_REF:${sha(value[i + 1])}]`);
73
77
  i++;
74
78
  }
@@ -84,11 +88,20 @@ function protectedValue(value, key = "") {
84
88
  protectedValue(item, childKey),
85
89
  ]));
86
90
  }
91
+ function stableEffectInput(request) {
92
+ if (request.capability !== "monitor.chat.submit" && request.capability !== "monitor.chat.create")
93
+ return request.input;
94
+ const effect = { ...request.input };
95
+ // Legacy revision fences are admission facts, not physical-effect identity.
96
+ delete effect.expectedRunVersion;
97
+ delete effect.configRevision;
98
+ return effect;
99
+ }
87
100
  export function executionInputFingerprint(input) {
88
101
  const request = parseExecuteCapabilityRequest(input);
89
102
  return sha({
90
103
  capability: request.capability,
91
- input: protectedValue(request.input),
104
+ input: protectedValue(stableEffectInput(request)),
92
105
  callerRef: request.callerRef,
93
106
  taskId: request.taskId,
94
107
  nodeId: request.nodeId,
@@ -118,6 +131,20 @@ function compatibleDurableInputFingerprint(row, request, currentFingerprint) {
118
131
  try {
119
132
  const durableRequest = parseExecuteCapabilityRequest(JSON.parse(String(row.request_json)));
120
133
  const durableRecord = parseExecutionRecord(JSON.parse(String(row.record_json)));
134
+ if ((request.capability === "monitor.chat.submit" || request.capability === "monitor.chat.create") &&
135
+ durableRequest.capability === request.capability &&
136
+ sha({
137
+ capability: durableRequest.capability,
138
+ input: stableEffectInput(durableRequest),
139
+ callerRef: durableRequest.callerRef,
140
+ taskId: durableRequest.taskId,
141
+ nodeId: durableRequest.nodeId,
142
+ runNo: durableRequest.runNo,
143
+ roleRef: durableRequest.roleRef,
144
+ workerRef: durableRequest.workerRef,
145
+ projectRoot: durableRequest.projectRoot,
146
+ }) === currentFingerprint)
147
+ return durableFingerprint;
121
148
  if (durableFingerprint === legacyExecutionInputFingerprint(request) &&
122
149
  durableRequest.runNo === request.runNo &&
123
150
  durableRecord.runNo === request.runNo)
@@ -153,6 +180,10 @@ function runtimeError(error) {
153
180
  }
154
181
  const defaultPolicy = {
155
182
  decide(request) {
183
+ const trustedSystemEffect = request.capability === "product.discussion.bootstrap.deliver" ||
184
+ request.capability === "product.review.deliver" ||
185
+ request.capability === "monitor.chat.submit" ||
186
+ request.capability === "monitor.chat.create" || request.capability === "browser.action";
156
187
  const readOnly = request.capability === "file.read" ||
157
188
  request.capability === "file.searchText" ||
158
189
  request.capability === "artifact.external-file.materialize" ||
@@ -169,14 +200,12 @@ const defaultPolicy = {
169
200
  request.capability === "browser.verify" ||
170
201
  (request.capability === "network.request" &&
171
202
  (request.input.method === "GET" || request.input.method === "HEAD"));
172
- if (readOnly)
203
+ if (readOnly || trustedSystemEffect)
173
204
  return {
174
205
  decision: "ALLOW",
175
206
  decisionPath: "deterministic",
176
207
  approvalRequired: false,
177
208
  };
178
- // Frozen policy: every ordinary real side effect is semantically judged by
179
- // FAST first. REASON/Human are escalation paths, never a blanket gate.
180
209
  return {
181
210
  decision: "REVIEW",
182
211
  decisionPath: "fast",
@@ -220,6 +249,9 @@ export async function createExecutionRuntime(options) {
220
249
  record_json TEXT NOT NULL, precondition_json TEXT, created_at TEXT NOT NULL,
221
250
  UNIQUE(caller_ref, capability, idempotency_key)
222
251
  );
252
+ CREATE UNIQUE INDEX IF NOT EXISTS formal_effect_identity
253
+ ON executions(idempotency_key)
254
+ WHERE capability IN ('monitor.chat.create', 'monitor.chat.submit', 'browser.action');
223
255
  CREATE TABLE IF NOT EXISTS execution_artifacts (
224
256
  execution_ref TEXT NOT NULL, artifact_ref TEXT NOT NULL, stream TEXT NOT NULL, path TEXT NOT NULL,
225
257
  PRIMARY KEY(execution_ref, artifact_ref), FOREIGN KEY(execution_ref) REFERENCES executions(execution_ref)
@@ -324,6 +356,12 @@ export async function createExecutionRuntime(options) {
324
356
  updatedAt: now().toISOString(),
325
357
  });
326
358
  async function admit(request, inputFingerprint, executionRef) {
359
+ const trustedSystemEffect = request.capability === "product.discussion.bootstrap.deliver" ||
360
+ request.capability === "product.review.deliver" ||
361
+ request.capability === "monitor.chat.submit" ||
362
+ request.capability === "monitor.chat.create" || request.capability === "browser.action";
363
+ if (trustedSystemEffect && !options.identity)
364
+ throw new ExecutionRuntimeError("IDENTITY_INVALID", "trusted system effect requires an authoritative identity port");
327
365
  if (options.identity && !(await options.identity.authorize(request)))
328
366
  throw new ExecutionRuntimeError("IDENTITY_INVALID", "caller, Task, Agent, workspace, or Browser identity is not authoritative");
329
367
  const policy = options.policy ?? defaultPolicy;
@@ -532,10 +570,96 @@ export async function createExecutionRuntime(options) {
532
570
  throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "execution intent lookup input does not match durable input");
533
571
  return fromRow(row);
534
572
  }
573
+ async function reconcileUnknownReplay(request, row, prior) {
574
+ const executor = browserIds.has(request.capability)
575
+ ? options.browserExecutor
576
+ : options.localExecutor;
577
+ const preconditionRaw = row.precondition_json;
578
+ if (!executor || !preconditionRaw) {
579
+ await log(prior, "UNKNOWN_REPLAY_UNRESOLVED", prior.error?.code);
580
+ return prior;
581
+ }
582
+ try {
583
+ const reconciliation = await executor.reconcile(request, JSON.parse(String(preconditionRaw)));
584
+ let reconciled;
585
+ if (reconciliation.state === "NOT_APPLIED") {
586
+ const failed = failRecord(prior, new ExecutionRuntimeError("EXECUTION_FAILED", "replay reconciliation verified no effect"));
587
+ reconciled = parseExecutionRecord({
588
+ ...failed,
589
+ evidence: reconciliation.evidence,
590
+ evidenceRefs: reconciliation.evidence.map((item) => item.evidenceRef),
591
+ });
592
+ }
593
+ else if (reconciliation.state === "APPLIED" && reconciliation.result) {
594
+ const { error: _previousError, ...recoverable } = prior;
595
+ reconciled = parseExecutionRecord({
596
+ ...recoverable,
597
+ status: "SUCCEEDED",
598
+ sideEffectState: "APPLIED",
599
+ retryable: false,
600
+ result: reconciliation.result,
601
+ evidence: reconciliation.evidence,
602
+ evidenceRefs: reconciliation.evidence.map((item) => item.evidenceRef),
603
+ finishedAt: now().toISOString(),
604
+ updatedAt: now().toISOString(),
605
+ });
606
+ }
607
+ else {
608
+ await log(prior, "UNKNOWN_REPLAY_UNRESOLVED", prior.error?.code);
609
+ return prior;
610
+ }
611
+ const updated = database.prepare("UPDATE executions SET record_json=? WHERE execution_ref=? AND record_json=?").run(JSON.stringify(reconciled), prior.executionRef, String(row.record_json));
612
+ if (updated.changes !== 1) {
613
+ const current = getRow(prior.executionRef);
614
+ return current ? fromRow(current) : prior;
615
+ }
616
+ await log(reconciled, "UNKNOWN_REPLAY_RECONCILED", reconciled.error?.code);
617
+ emitObserverSignal(reconciled, "RECOVERY_RESUME");
618
+ return reconciled;
619
+ }
620
+ catch {
621
+ await log(prior, "UNKNOWN_REPLAY_UNRESOLVED", prior.error?.code);
622
+ return prior;
623
+ }
624
+ }
625
+ async function executeEffect(input) {
626
+ const { effectId, action, request } = parseMonitorEffectRequest(input);
627
+ const existingEffect = database
628
+ .prepare("SELECT * FROM executions WHERE idempotency_key=? AND capability IN ('monitor.chat.create', 'monitor.chat.submit', 'browser.action') ORDER BY created_at, execution_ref LIMIT 1")
629
+ .get(effectId);
630
+ let ownerRequest = request;
631
+ if (existingEffect) {
632
+ if (String(existingEffect.capability) !== action)
633
+ throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "effectId already belongs to another action");
634
+ if (String(existingEffect.caller_ref) !== request.callerRef)
635
+ ownerRequest = parseExecuteCapabilityRequest({
636
+ ...request,
637
+ callerRef: String(existingEffect.caller_ref),
638
+ });
639
+ if (compatibleDurableInputFingerprint(existingEffect, ownerRequest, executionInputFingerprint(ownerRequest)) === undefined)
640
+ throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "effectId already belongs to different critical input");
641
+ }
642
+ const record = await executeCapability(ownerRequest);
643
+ const state = record.sideEffectState === "APPLIED"
644
+ ? "APPLIED"
645
+ : record.sideEffectState === "NOT_APPLIED"
646
+ ? "NOT_APPLIED"
647
+ : "PENDING";
648
+ return parseMonitorEffectReceipt({
649
+ effectId,
650
+ action,
651
+ state,
652
+ executionRef: record.executionRef,
653
+ ...(state === "APPLIED" && record.result
654
+ ? { result: record.result }
655
+ : {}),
656
+ });
657
+ }
535
658
  async function executeCapability(input) {
536
659
  if (closed)
537
660
  throw new ExecutionRuntimeError("EXECUTOR_UNAVAILABLE", "execution runtime is closed");
538
661
  const request = parseExecuteCapabilityRequest(input);
662
+ const monitorEffect = isFormalEffectCapability(request.capability);
539
663
  assertPatchArtifactScope(request);
540
664
  let inputFingerprint = executionInputFingerprint(request);
541
665
  const existing = database
@@ -548,8 +672,16 @@ export async function createExecutionRuntime(options) {
548
672
  throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "idempotency key was already used with different critical input");
549
673
  inputFingerprint = durableFingerprint;
550
674
  const prior = fromRow(existing);
551
- const safelyResumable = (prior.status === "PENDING" &&
552
- prior.sideEffectState === "NOT_STARTED") ||
675
+ // Existing PENDING and APPLIED are read-only. Only proven absence permits retry.
676
+ if (monitorEffect && prior.sideEffectState !== "NOT_APPLIED")
677
+ return prior;
678
+ if (prior.status === "UNKNOWN" && prior.sideEffectState === "UNKNOWN" &&
679
+ (request.capability === "product.discussion.bootstrap.deliver" ||
680
+ request.capability === "product.review.deliver"))
681
+ return reconcileUnknownReplay(request, existing, prior);
682
+ const safelyResumable = (monitorEffect && prior.sideEffectState === "NOT_APPLIED") ||
683
+ (prior.status === "PENDING" &&
684
+ prior.sideEffectState === "NOT_STARTED") ||
553
685
  (prior.status === "FAILED" && prior.sideEffectState === "NOT_APPLIED");
554
686
  if (!safelyResumable)
555
687
  return prior;
@@ -561,9 +693,11 @@ export async function createExecutionRuntime(options) {
561
693
  retryable: false,
562
694
  updatedAt: now().toISOString(),
563
695
  });
564
- database
565
- .prepare("UPDATE executions SET request_json=?, record_json=?, precondition_json=NULL WHERE execution_ref=?")
566
- .run(JSON.stringify(protectedValue(request)), JSON.stringify(record), record.executionRef);
696
+ const claimed = database
697
+ .prepare("UPDATE executions SET request_json=?, record_json=?, precondition_json=NULL WHERE execution_ref=? AND record_json=?")
698
+ .run(JSON.stringify(protectedValue(request)), JSON.stringify(record), record.executionRef, String(existing.record_json));
699
+ if (claimed.changes === 0)
700
+ return fromRow(getRow(record.executionRef));
567
701
  await log(record, "EXECUTION_REDECISION_REQUESTED");
568
702
  }
569
703
  else {
@@ -658,9 +792,6 @@ export async function createExecutionRuntime(options) {
658
792
  throw new ExecutionRuntimeError(timedOut ? "TIMEOUT" : "CANCELLED", timedOut
659
793
  ? "execution runtime timeout elapsed during admission"
660
794
  : "execution was cancelled during admission");
661
- // CAS: only transition to RUNNING if the record is still PENDING.
662
- // A concurrent cancel/timeout finalizes PENDING records, so a
663
- // stale RUNNING write would silently resurrect a cancelled intent.
664
795
  const casRow = getRow(record.executionRef);
665
796
  if (casRow) {
666
797
  const casRecord = fromRow(casRow);
@@ -812,7 +943,12 @@ export async function createExecutionRuntime(options) {
812
943
  const row = getRow(record.executionRef);
813
944
  if (row)
814
945
  record = fromRow(row);
815
- if (record.sideEffectState === "STARTED") {
946
+ if (monitorEffect && record.sideEffectState === "APPLIED")
947
+ return record;
948
+ if (monitorEffect && record.sideEffectState === "STARTED") {
949
+ record = failRecord(record, failure, "UNKNOWN");
950
+ }
951
+ else if (record.sideEffectState === "STARTED") {
816
952
  const executor = browserIds.has(request.capability)
817
953
  ? options.browserExecutor
818
954
  : options.localExecutor;
@@ -914,6 +1050,10 @@ export async function createExecutionRuntime(options) {
914
1050
  let record = getExecutionRecord(request.executionRef);
915
1051
  if (record.callerRef !== request.callerRef)
916
1052
  throw new ExecutionRuntimeError("IDENTITY_INVALID", "only the bound caller may cancel execution");
1053
+ // Admission may still be in flight. Cancellation cannot prove it has stopped,
1054
+ // so it must not expose NOT_APPLIED and let another caller claim this row.
1055
+ if (isFormalEffectCapability(record.capability) && record.status === "PENDING")
1056
+ throw new ExecutionRuntimeError("INVALID_REQUEST", "Monitor PENDING must settle through effect.execute");
917
1057
  controllers.get(request.executionRef)?.abort();
918
1058
  if (record.status === "PENDING") {
919
1059
  record = failRecord(record, new ExecutionRuntimeError("CANCELLED", request.reason));
@@ -926,6 +1066,8 @@ export async function createExecutionRuntime(options) {
926
1066
  const rows = database.prepare("SELECT * FROM executions").all();
927
1067
  for (const row of rows) {
928
1068
  let record = fromRow(row);
1069
+ if (isFormalEffectCapability(record.capability))
1070
+ continue;
929
1071
  if (record.status !== "PENDING" &&
930
1072
  record.status !== "RUNNING" &&
931
1073
  record.status !== "UNKNOWN")
@@ -986,6 +1128,7 @@ export async function createExecutionRuntime(options) {
986
1128
  await recoverIncomplete();
987
1129
  return Object.freeze({
988
1130
  executeCapability,
1131
+ executeEffect,
989
1132
  lookupExecutionIntent,
990
1133
  listExecutionObserverSignals,
991
1134
  acknowledgeExecutionObserverSignal,
@@ -441,6 +441,8 @@ export async function createExecutionRuntimeProcess(input) {
441
441
  if (request.method === "GET" &&
442
442
  url.pathname.startsWith("/approvals/"))
443
443
  return respond(response, 200, runtime.getExecutionApproval(decodeURIComponent(url.pathname.slice(11))));
444
+ if (request.method === "POST" && url.pathname === "/effects/execute")
445
+ return respond(response, 200, await runtime.executeEffect(body));
444
446
  if (request.method === "POST" && url.pathname === "/executions")
445
447
  return respond(response, 200, await runtime.executeCapability(body));
446
448
  if (request.method === "POST" &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-execution-runtime",
3
- "version": "0.1.19",
3
+ "version": "0.1.20",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -24,11 +24,11 @@
24
24
  "SETUP.md"
25
25
  ],
26
26
  "dependencies": {
27
- "@tomflow/proflow-model-contracts": "^0.1.10",
28
- "@tomflow/proflow-execution-browser-extension": "^0.1.51",
29
- "@tomflow/proflow-module-contract": "^0.1.13",
27
+ "@tomflow/proflow-execution-contracts": "^0.1.12",
30
28
  "@tomflow/proflow-execution-local": "^0.1.16",
31
- "@tomflow/proflow-execution-contracts": "^0.1.11"
29
+ "@tomflow/proflow-module-contract": "^0.1.13",
30
+ "@tomflow/proflow-model-contracts": "^0.1.10",
31
+ "@tomflow/proflow-execution-browser-extension": "^0.1.74"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@tomflow/proflow-deployment-conformance": "^0.1.13"
@@ -3,7 +3,7 @@
3
3
  "contractVersion": "1.0.0",
4
4
  "moduleRef": "execution-runtime",
5
5
  "packageName": "@tomflow/proflow-execution-runtime",
6
- "moduleVersion": "0.1.19",
6
+ "moduleVersion": "0.1.20",
7
7
  "kind": "service",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",