@tomflow/proflow-execution-runtime 0.1.18 → 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,17 @@
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
+
9
+ ## 0.1.19
10
+
11
+ ### Patch Changes
12
+
13
+ - Close the Real-3 runtime reconciliation, health, browser effect-result boundary, observer pagination, and standalone execution-runtime configuration gaps for final human acceptance.
14
+
3
15
  ## 0.1.18
4
16
 
5
17
  ### Patch Changes
package/DOCS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 模块定位与作用
4
4
 
5
- 接收标准 Execution Intent,选择 Local 或 Browser Executor,管理执行、恢复和 Evidence,并向其他领域提供唯一真实副作用入口。
5
+ 接收内部 durable Execution Intent,选择受控 Local/Browser Executor,管理执行、恢复和 Evidence;GPT-facing Repomix / Local Dev / CodeGraph Direct Tools 不进入该 lifecycle。
6
6
 
7
7
  ## 主要能力
8
8
 
@@ -17,8 +17,8 @@
17
17
 
18
18
  ## 依赖的 Module、Contract 和外部资源
19
19
 
20
- - 依赖 `execution-local`,兼容版本 `>=1.0.0 <2.0.0`。
21
- - Browser Effect 还需要 execution-browser-extension 发布的 Executor shared fact。
20
+ - 依赖 `execution-browser-executor` 与 `execution-local`,兼容版本均为 `>=1.0.0 <2.0.0`。
21
+ - `execution-browser-executor` 由 execution-browser-extension 发布;Execution Runtime 只消费该 Browser lane client/readiness,不拥有或关闭 Extension bridge listener。
22
22
 
23
23
  ## 运行形态与生命周期
24
24
 
@@ -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.18";
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.18";
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.18";
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.18";
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.18";
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.18";
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.18";
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.18";
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.18";
121
+ readonly moduleVersion: "0.1.20";
122
122
  } | {
123
123
  contract: "deployment.result.v1";
124
124
  moduleRef: "execution-runtime";
125
- moduleVersion: "0.1.18";
125
+ moduleVersion: "0.1.20";
126
126
  ok: false;
127
127
  status: "FAILED";
128
128
  error: {
@@ -1,5 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { mkdir, readFile } from "node:fs/promises";
2
+ import { mkdir } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
4
  import { deterministicLoopbackPort, ensureModuleSecretFile, moduleWorkspaceStateDirectory, readModuleSharedFacts, writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
5
5
  import { descriptor } from "./descriptor.js";
@@ -48,45 +48,18 @@ async function readOwnFacts(context) {
48
48
  }
49
49
  : undefined;
50
50
  }
51
- async function baseDependencies(context) {
52
- const host = await readModuleSharedFacts(context, "platform-host");
53
- const model = await readModuleSharedFacts(context, "model-runtime");
54
- const identityEndpoint = factString(host, "endpoint");
55
- const identityTokenFile = factString(host, "identityTokenFile");
56
- const modelEndpoint = factString(model, "endpoint");
57
- const modelCredentialFile = factString(model, "transportCredentialFile");
58
- return identityEndpoint &&
59
- identityTokenFile &&
60
- modelEndpoint &&
61
- modelCredentialFile
62
- ? {
63
- identityEndpoint,
64
- identityTokenFile,
65
- modelEndpoint,
66
- modelCredentialFile,
67
- }
68
- : undefined;
69
- }
70
- async function dependencies(context) {
71
- const base = await baseDependencies(context);
51
+ async function startupDependencies(context) {
72
52
  const browser = await readModuleSharedFacts(context, "execution-browser-extension");
73
53
  const browserExecutorConfigPath = factString(browser, "browserExecutorConfigPath");
74
- return base && browserExecutorConfigPath
75
- ? {
76
- ...base,
77
- browserExecutorConfigPath,
78
- }
79
- : undefined;
54
+ return browserExecutorConfigPath ? { browserExecutorConfigPath } : undefined;
80
55
  }
81
56
  async function running(context) {
82
57
  const own = await readOwnFacts(context);
83
58
  if (!own)
84
59
  return false;
85
60
  try {
86
- const token = (await readFile(own.transportCredentialFile, "utf8")).trim();
87
- return (await fetch(`${own.endpoint}/ready`, {
88
- headers: { authorization: `Bearer ${token}` },
89
- signal: AbortSignal.timeout(500),
61
+ return (await fetch(`${own.endpoint}/health`, {
62
+ signal: AbortSignal.timeout(2_000),
90
63
  })).ok;
91
64
  }
92
65
  catch {
@@ -95,31 +68,39 @@ async function running(context) {
95
68
  }
96
69
  async function compose(context) {
97
70
  const own = await ownFacts(context);
98
- const deps = await dependencies(context);
99
- if (!deps)
100
- throw new Error("required Platform Host, Model Runtime, or Browser Executor shared facts are unavailable");
71
+ const browser = await readModuleSharedFacts(context, "execution-browser-extension");
72
+ const browserExecutorConfigPath = factString(browser, "browserExecutorConfigPath");
73
+ if (!browserExecutorConfigPath)
74
+ throw new Error("Browser Executor shared facts are unavailable");
101
75
  const [{ createFormalExecutionRuntimeLifecycle }, { parseExecutionRuntimeProcessConfig },] = await Promise.all([
102
76
  import("../src/formal-process.js"),
103
77
  import("../src/service.js"),
104
78
  ]);
105
79
  const url = new URL(own.endpoint);
106
80
  return createFormalExecutionRuntimeLifecycle({
81
+ resolveDependencies: async () => {
82
+ const host = await readModuleSharedFacts(context, "platform-host");
83
+ const model = await readModuleSharedFacts(context, "model-runtime");
84
+ const endpoint = factString(host, "endpoint"), tokenFile = factString(host, "identityTokenFile"), taskToken = factString(host, "taskApplicationTokenFile");
85
+ const modelEndpoint = factString(model, "endpoint"), credentialFile = factString(model, "transportCredentialFile");
86
+ return {
87
+ ...(endpoint && tokenFile ? { identity: { endpoint, tokenFile } } : {}),
88
+ ...(endpoint && taskToken
89
+ ? { platformHost: { endpoint, tokenFile: taskToken } }
90
+ : {}),
91
+ ...(modelEndpoint && credentialFile
92
+ ? { modelDecision: { endpoint: modelEndpoint, credentialFile } }
93
+ : {}),
94
+ };
95
+ },
107
96
  config: parseExecutionRuntimeProcessConfig({
108
97
  host: url.hostname,
109
98
  port: Number(url.port),
110
99
  databasePath: own.databasePath,
111
100
  projectRoot: own.projectRoot,
112
101
  artifactRoot: own.artifactRoot,
113
- browserExecutorConfigPath: deps.browserExecutorConfigPath,
102
+ browserExecutorConfigPath,
114
103
  transportCredentialFile: own.transportCredentialFile,
115
- identity: {
116
- endpoint: deps.identityEndpoint,
117
- tokenFile: deps.identityTokenFile,
118
- },
119
- modelDecision: {
120
- endpoint: deps.modelEndpoint,
121
- credentialFile: deps.modelCredentialFile,
122
- },
123
104
  exactNetworkTargets: [],
124
105
  }),
125
106
  });
@@ -149,7 +130,7 @@ export const behaviorAdapter = {
149
130
  };
150
131
  },
151
132
  status: async (context) => {
152
- const missingRuntimeDependencies = await dependencies(context);
133
+ const startup = await startupDependencies(context);
153
134
  return {
154
135
  result: {
155
136
  ...base,
@@ -158,18 +139,14 @@ export const behaviorAdapter = {
158
139
  runtimeStatus: (await running(context))
159
140
  ? "RUNNING"
160
141
  : "STOPPED",
161
- ...(!missingRuntimeDependencies
142
+ ...(!startup
162
143
  ? {
163
144
  issues: [
164
145
  {
165
146
  scope: "RUNTIME",
166
147
  code: "UPSTREAM_NOT_READY",
167
- message: "等待 platform-host、model-runtime 与 execution-browser-extension 发布运行所需信息",
168
- relatedModuleRefs: [
169
- "platform-host",
170
- "model-runtime",
171
- "execution-browser-extension",
172
- ],
148
+ message: "等待 execution-browser-extension 发布 Browser Executor client 配置",
149
+ relatedModuleRefs: ["execution-browser-extension"],
173
150
  nextCommand: "platform setup",
174
151
  },
175
152
  ],
@@ -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.18";
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";
@@ -16,6 +16,9 @@ export declare const descriptor: {
16
16
  readonly version: "1.0.0";
17
17
  }];
18
18
  readonly requires: readonly [{
19
+ readonly contractRef: "execution-browser-executor";
20
+ readonly versionRange: ">=1.0.0 <2.0.0";
21
+ }, {
19
22
  readonly contractRef: "execution-local";
20
23
  readonly versionRange: ">=1.0.0 <2.0.0";
21
24
  }];
@@ -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.18",
6
+ moduleVersion: "0.1.20",
7
7
  kind: "service",
8
8
  templateVersion: "1.0.0",
9
9
  platformCompatibility: ">=1.0.0 <2.0.0",
@@ -18,6 +18,10 @@ export const descriptor = {
18
18
  },
19
19
  ],
20
20
  requires: [
21
+ {
22
+ contractRef: "execution-browser-executor",
23
+ versionRange: ">=1.0.0 <2.0.0",
24
+ },
21
25
  {
22
26
  contractRef: "execution-local",
23
27
  versionRange: ">=1.0.0 <2.0.0",
@@ -2,6 +2,11 @@ import { type ExecutionRuntimeProcessConfig } from "./service.ts";
2
2
  export declare function createFormalExecutionRuntimeLifecycle(input: {
3
3
  config: ExecutionRuntimeProcessConfig;
4
4
  log?: (entry: Record<string, unknown>) => void;
5
+ resolveDependencies?: () => Promise<{
6
+ identity?: ExecutionRuntimeProcessConfig["identity"];
7
+ modelDecision?: ExecutionRuntimeProcessConfig["modelDecision"];
8
+ platformHost?: ExecutionRuntimeProcessConfig["platformHost"];
9
+ }>;
5
10
  }): Readonly<{
6
11
  status: () => import("./service.ts").ExecutionRuntimeProcessStatus | {
7
12
  readiness: "NOT_READY";
@@ -1,5 +1,5 @@
1
1
  import { readFile, stat } from "node:fs/promises";
2
- import { createBrowserExecutorComposition, loadBrowserExecutorCompositionConfig, } from "@tomflow/proflow-execution-browser-extension/runtime-composition";
2
+ import { createBrowserExecutorClientComposition } from "@tomflow/proflow-execution-browser-extension/runtime-composition";
3
3
  import { createExecutionBrowserVisionClient } from "./browser-vision-client.js";
4
4
  import { createExecutionModelDecisionClient } from "./model-decision-client.js";
5
5
  import { createExecutionRuntimeProcess, } from "./service.js";
@@ -28,7 +28,6 @@ async function createIdentityClient(config) {
28
28
  }
29
29
  return ready;
30
30
  };
31
- await probe();
32
31
  return {
33
32
  port: {
34
33
  async authorize(request) {
@@ -56,45 +55,81 @@ async function createIdentityClient(config) {
56
55
  };
57
56
  }
58
57
  function assertFormalConfig(config) {
59
- if (!config.identity)
60
- throw new Error("formal execution-runtime requires identity configuration");
61
58
  if (!config.transportCredentialFile)
62
59
  throw new Error("formal execution-runtime requires transportCredentialFile");
63
60
  if (!config.browserExecutorConfigPath)
64
61
  throw new Error("formal execution-runtime requires browserExecutorConfigPath");
65
- if (!config.modelDecision)
66
- throw new Error("formal execution-runtime requires modelDecision configuration");
67
- if (!config.modelDecision.credentialFile)
68
- throw new Error("formal execution-runtime requires modelDecision.credentialFile");
69
62
  return {
70
63
  identity: config.identity,
71
64
  transportCredentialFile: config.transportCredentialFile,
72
65
  browserExecutorConfigPath: config.browserExecutorConfigPath,
73
- modelDecision: {
74
- ...config.modelDecision,
75
- credentialFile: config.modelDecision.credentialFile,
76
- },
77
66
  };
78
67
  }
79
68
  export function createFormalExecutionRuntimeLifecycle(input) {
80
69
  const required = assertFormalConfig(input.config);
70
+ if (!input.resolveDependencies && !input.config.platformHost)
71
+ throw new Error("formal execution-runtime requires platformHost or resolveDependencies");
81
72
  let current;
82
73
  const build = async () => {
83
- const identityClient = await createIdentityClient(required.identity);
74
+ const dependencies = input.resolveDependencies ??
75
+ (async () => ({
76
+ identity: input.config.identity,
77
+ modelDecision: input.config.modelDecision,
78
+ platformHost: input.config.platformHost,
79
+ }));
80
+ let identityReady = false;
81
+ let modelReady = false;
82
+ const resolveIdentity = async () => {
83
+ const config = (await dependencies()).identity;
84
+ if (!config)
85
+ throw new Error("EXECUTION_IDENTITY_UNAVAILABLE");
86
+ return createIdentityClient(config);
87
+ };
88
+ const modelConfig = async () => {
89
+ const config = (await dependencies()).modelDecision;
90
+ if (!config?.credentialFile)
91
+ throw new Error("MODEL_DECISION_UNAVAILABLE");
92
+ return {
93
+ ...config,
94
+ credential: await readSecret(config.credentialFile, "model credential"),
95
+ };
96
+ };
84
97
  const transportCredential = await readSecret(required.transportCredentialFile, "execution transport credential");
85
- const modelDecisionCredential = await readSecret(required.modelDecision.credentialFile, "model decision transport credential");
86
- const modelDecisionClient = createExecutionModelDecisionClient({
87
- endpoint: required.modelDecision.endpoint,
88
- ...(required.modelDecision.timeoutMs === undefined
89
- ? {}
90
- : { timeoutMs: required.modelDecision.timeoutMs }),
91
- credential: modelDecisionCredential,
98
+ const browserComposition = await createBrowserExecutorClientComposition({
99
+ configPath: required.browserExecutorConfigPath,
100
+ platformHost: async () => {
101
+ const config = (await dependencies()).platformHost;
102
+ if (!config)
103
+ throw new Error("PLATFORM_HOST_APPLICATION_UNAVAILABLE");
104
+ return {
105
+ endpoint: config.endpoint,
106
+ token: await readSecret(config.tokenFile, "task application credential"),
107
+ };
108
+ },
109
+ vision: {
110
+ inspect: async (request) => createExecutionBrowserVisionClient(await modelConfig()).port.inspect(request),
111
+ },
92
112
  });
93
113
  let dependencyRefresh;
94
114
  const refreshDependencies = () => {
95
115
  dependencyRefresh ??= Promise.allSettled([
96
- identityClient.probe(),
97
- modelDecisionClient.probe(),
116
+ resolveIdentity()
117
+ .then((client) => client.probe())
118
+ .then((ready) => {
119
+ identityReady = ready;
120
+ })
121
+ .catch(() => {
122
+ identityReady = false;
123
+ }),
124
+ modelConfig()
125
+ .then((config) => createExecutionModelDecisionClient(config).probe())
126
+ .then((ready) => {
127
+ modelReady = ready;
128
+ })
129
+ .catch(() => {
130
+ modelReady = false;
131
+ }),
132
+ browserComposition.refreshStatus(),
98
133
  ])
99
134
  .then(() => undefined)
100
135
  .finally(() => {
@@ -102,27 +137,35 @@ export function createFormalExecutionRuntimeLifecycle(input) {
102
137
  });
103
138
  return dependencyRefresh;
104
139
  };
105
- await refreshDependencies();
106
- const browserVisionClient = createExecutionBrowserVisionClient({
107
- endpoint: required.modelDecision.endpoint,
108
- ...(required.modelDecision.timeoutMs === undefined
109
- ? {}
110
- : { timeoutMs: required.modelDecision.timeoutMs }),
111
- credential: modelDecisionCredential,
112
- });
113
- const browserComposition = await createBrowserExecutorComposition({
114
- ...(await loadBrowserExecutorCompositionConfig(required.browserExecutorConfigPath)),
115
- vision: browserVisionClient.port,
116
- });
117
140
  try {
118
141
  const service = await createExecutionRuntimeProcess({
119
142
  config: input.config,
120
- identity: identityClient.port,
121
- identityReadiness: identityClient.readiness,
143
+ identity: {
144
+ authorize: async (request) => {
145
+ try {
146
+ const client = await resolveIdentity();
147
+ const allowed = await client.port.authorize(request);
148
+ identityReady = client.readiness();
149
+ return allowed;
150
+ }
151
+ catch {
152
+ identityReady = false;
153
+ return false;
154
+ }
155
+ },
156
+ },
157
+ identityReadiness: () => identityReady,
122
158
  transportCredential,
123
159
  requireModelDecision: true,
124
- modelDecision: modelDecisionClient.port,
125
- modelDecisionReadiness: modelDecisionClient.readiness,
160
+ modelDecision: {
161
+ decide: async (request, context) => {
162
+ const client = createExecutionModelDecisionClient(await modelConfig());
163
+ await client.probe();
164
+ modelReady = client.readiness();
165
+ return client.port.decide(request, context);
166
+ },
167
+ },
168
+ modelDecisionReadiness: () => modelReady,
126
169
  refreshDependencies,
127
170
  browserExecutor: browserComposition.browserExecutor,
128
171
  browserReadiness: () => browserComposition.bridgeStatus().online,
@@ -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,
@@ -12,6 +12,10 @@ export type ExecutionRuntimeProcessConfig = {
12
12
  endpoint: string;
13
13
  tokenFile: string;
14
14
  };
15
+ platformHost?: {
16
+ endpoint: string;
17
+ tokenFile: string;
18
+ };
15
19
  modelDecision?: {
16
20
  endpoint: string;
17
21
  timeoutMs?: number;
@@ -2,6 +2,7 @@ import { timingSafeEqual } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { createServer } from "node:http";
4
4
  import { dirname, isAbsolute, resolve } from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
5
6
  import { artifactMaterializationResponseSchema, EXECUTION_CONTRACT_VERSION, materializeContextPackRequestSchema, materializeExternalFilesRequestSchema, materializeExternalFilesResponseSchema, materializePatchProposalRequestSchema, parseExecutionRef, } from "@tomflow/proflow-execution-contracts";
6
7
  import { createLocalExecutorPort } from "./executors/local-adapter.js";
7
8
  import { createExecutionRuntime, } from "./index.js";
@@ -15,6 +16,22 @@ function string(value, name) {
15
16
  throw new TypeError(`${name} must be a non-empty string`);
16
17
  return value;
17
18
  }
19
+ function parseLoopbackOwner(value, name) {
20
+ const input = object(value, name);
21
+ const endpoint = new URL(string(input.endpoint, `${name}.endpoint`));
22
+ if (endpoint.protocol !== "http:" ||
23
+ !new Set(["127.0.0.1", "localhost", "::1", "[::1]"]).has(endpoint.hostname) ||
24
+ endpoint.pathname !== "/" ||
25
+ endpoint.username ||
26
+ endpoint.password ||
27
+ endpoint.search ||
28
+ endpoint.hash)
29
+ throw new TypeError(`${name}.endpoint must be loopback HTTP root`);
30
+ const tokenFile = string(input.tokenFile, `${name}.tokenFile`);
31
+ if (!isAbsolute(tokenFile))
32
+ throw new TypeError(`${name}.tokenFile must be absolute`);
33
+ return { endpoint: endpoint.origin, tokenFile };
34
+ }
18
35
  export function parseExecutionRuntimeProcessConfig(value) {
19
36
  const input = object(value, "execution-runtime config");
20
37
  const databasePath = resolve(string(input.databasePath, "databasePath"));
@@ -63,21 +80,12 @@ export function parseExecutionRuntimeProcessConfig(value) {
63
80
  ...(credentialFile === undefined ? {} : { credentialFile }),
64
81
  };
65
82
  }
66
- let identity;
67
- if (input.identity !== undefined) {
68
- const value = object(input.identity, "identity");
69
- const endpoint = new URL(string(value.endpoint, "identity.endpoint"));
70
- if (endpoint.protocol !== "http:" ||
71
- !new Set(["127.0.0.1", "localhost", "::1", "[::1]"]).has(endpoint.hostname) ||
72
- endpoint.pathname !== "/" ||
73
- endpoint.search ||
74
- endpoint.hash)
75
- throw new TypeError("identity.endpoint must be loopback HTTP root");
76
- const tokenFile = string(value.tokenFile, "identity.tokenFile");
77
- if (!isAbsolute(tokenFile))
78
- throw new TypeError("identity.tokenFile must be absolute");
79
- identity = { endpoint: endpoint.origin, tokenFile };
80
- }
83
+ const identity = input.identity === undefined
84
+ ? undefined
85
+ : parseLoopbackOwner(input.identity, "identity");
86
+ const platformHost = input.platformHost === undefined
87
+ ? undefined
88
+ : parseLoopbackOwner(input.platformHost, "platformHost");
81
89
  if (!Array.isArray(exactNetworkTargets) ||
82
90
  exactNetworkTargets.some((item) => typeof item !== "string"))
83
91
  throw new TypeError("exactNetworkTargets must be a string array");
@@ -91,12 +99,57 @@ export function parseExecutionRuntimeProcessConfig(value) {
91
99
  ...(browserExecutorConfigPath ? { browserExecutorConfigPath } : {}),
92
100
  ...(transportCredentialFile ? { transportCredentialFile } : {}),
93
101
  ...(identity ? { identity } : {}),
102
+ ...(platformHost ? { platformHost } : {}),
94
103
  ...(modelDecision ? { modelDecision } : {}),
95
104
  };
96
105
  }
97
106
  export async function loadExecutionRuntimeProcessConfig(path) {
98
107
  return parseExecutionRuntimeProcessConfig(JSON.parse(await readFile(resolve(path), "utf8")));
99
108
  }
109
+ function createObserverSignalPager(databasePath) {
110
+ const database = new DatabaseSync(databasePath, { readOnly: true });
111
+ let cursor;
112
+ let upperBound;
113
+ const page = (limit, after) => {
114
+ if (!upperBound)
115
+ return [];
116
+ return database.prepare(`SELECT record_json, created_at, signal_ref FROM execution_observer_signals
117
+ WHERE acknowledged_at IS NULL
118
+ AND (created_at, signal_ref) <= (?, ?)
119
+ ${after ? "AND (created_at, signal_ref) > (?, ?)" : ""}
120
+ ORDER BY created_at, signal_ref LIMIT ?`).all(upperBound.createdAt, upperBound.signalRef, ...(after ? [after.createdAt, after.signalRef] : []), limit);
121
+ };
122
+ const beginCycle = () => {
123
+ cursor = undefined;
124
+ const last = database.prepare("SELECT created_at, signal_ref FROM execution_observer_signals WHERE acknowledged_at IS NULL ORDER BY created_at DESC, signal_ref DESC LIMIT 1").get();
125
+ upperBound = last ? {
126
+ createdAt: String(last.created_at), signalRef: String(last.signal_ref),
127
+ } : undefined;
128
+ };
129
+ return Object.freeze({
130
+ list(limit = 50) {
131
+ const normalized = Number.isFinite(limit) ? Math.trunc(limit) : 50;
132
+ const bounded = Math.max(1, Math.min(100, normalized));
133
+ if (!upperBound)
134
+ beginCycle();
135
+ let rows = page(bounded, cursor);
136
+ if (rows.length === 0) {
137
+ beginCycle();
138
+ rows = page(bounded);
139
+ }
140
+ const last = rows.at(-1);
141
+ if (last)
142
+ cursor = {
143
+ createdAt: String(last.created_at),
144
+ signalRef: String(last.signal_ref),
145
+ };
146
+ return rows.map((row) => JSON.parse(String(row.record_json)));
147
+ },
148
+ close() {
149
+ database.close();
150
+ },
151
+ });
152
+ }
100
153
  export async function createExecutionRuntimeProcess(input) {
101
154
  if (input.transportCredential && input.transportCredential.length < 32)
102
155
  throw new TypeError("transport credential must contain at least 32 characters");
@@ -104,6 +157,7 @@ export async function createExecutionRuntimeProcess(input) {
104
157
  let accepting = false;
105
158
  let server;
106
159
  let runtime;
160
+ let observerSignals;
107
161
  const active = new Set();
108
162
  const log = (event, fields = {}) => input.log?.({
109
163
  timestamp: new Date().toISOString(),
@@ -117,29 +171,29 @@ export async function createExecutionRuntimeProcess(input) {
117
171
  ? "UNAVAILABLE"
118
172
  : "READY"
119
173
  : "UNAVAILABLE";
120
- const browserRequirementSatisfied = input.config.browserExecutorConfigPath === undefined ||
121
- browserExecutor === "READY";
122
174
  const identity = input.identity && input.identityReadiness?.() !== false
123
175
  ? "READY"
124
176
  : "UNAVAILABLE";
125
- const identityRequirementSatisfied = input.config.identity === undefined || identity === "READY";
126
177
  const transportAuth = input.transportCredential ? "READY" : "UNAVAILABLE";
127
178
  const transportRequirementSatisfied = input.config.transportCredentialFile === undefined ||
128
179
  transportAuth === "READY";
129
180
  const modelDecision = input.modelDecision && input.modelDecisionReadiness?.() !== false
130
181
  ? "READY"
131
182
  : "UNAVAILABLE";
132
- const modelRequirementSatisfied = input.requireModelDecision !== true || modelDecision === "READY";
183
+ const browserRequirementSatisfied = input.browserExecutor === undefined || browserExecutor === "READY";
184
+ const identityRequirementSatisfied = (input.identity === undefined && input.config.identity === undefined) ||
185
+ identity === "READY";
186
+ const modelDecisionRequirementSatisfied = input.requireModelDecision !== true || modelDecision === "READY";
133
187
  return {
134
188
  process: state,
135
189
  liveness: state === "STOPPED" ? "DOWN" : "UP",
136
190
  readiness: state === "RUNNING" &&
137
191
  accepting &&
138
192
  runtime &&
193
+ transportRequirementSatisfied &&
139
194
  browserRequirementSatisfied &&
140
195
  identityRequirementSatisfied &&
141
- transportRequirementSatisfied &&
142
- modelRequirementSatisfied
196
+ modelDecisionRequirementSatisfied
143
197
  ? "READY"
144
198
  : "NOT_READY",
145
199
  accepting,
@@ -180,6 +234,7 @@ export async function createExecutionRuntimeProcess(input) {
180
234
  ...(input.modelDecision ? { modelDecision: input.modelDecision } : {}),
181
235
  ...(input.approval ? { approval: input.approval } : {}),
182
236
  });
237
+ observerSignals = createObserverSignalPager(input.config.databasePath);
183
238
  server = createServer((request, response) => {
184
239
  const work = (async () => {
185
240
  const url = new URL(request.url ?? "/", "http://execution.local");
@@ -187,12 +242,12 @@ export async function createExecutionRuntimeProcess(input) {
187
242
  return respond(response, 200, { status: "UP" });
188
243
  if (request.method === "GET" && url.pathname === "/ready") {
189
244
  let current = status();
190
- if (current.readiness !== "READY" && input.refreshDependencies) {
245
+ if (input.refreshDependencies) {
191
246
  try {
192
247
  await input.refreshDependencies();
193
248
  }
194
249
  catch {
195
- // Keep readiness fail-closed when the bounded dependency re-probe fails.
250
+ // Owner readiness is reported separately from process readiness.
196
251
  }
197
252
  current = status();
198
253
  }
@@ -386,6 +441,8 @@ export async function createExecutionRuntimeProcess(input) {
386
441
  if (request.method === "GET" &&
387
442
  url.pathname.startsWith("/approvals/"))
388
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));
389
446
  if (request.method === "POST" && url.pathname === "/executions")
390
447
  return respond(response, 200, await runtime.executeCapability(body));
391
448
  if (request.method === "POST" &&
@@ -394,8 +451,11 @@ export async function createExecutionRuntimeProcess(input) {
394
451
  if (request.method === "POST" &&
395
452
  url.pathname === "/observer-signals/list") {
396
453
  const value = object(body ?? {}, "observer signal list input");
454
+ const limit = typeof value.limit === "number" ? value.limit : 50;
397
455
  return respond(response, 200, {
398
- signals: runtime.listExecutionObserverSignals(typeof value.limit === "number" ? value.limit : 50),
456
+ signals: value.consumer === "task-reconciliation"
457
+ ? observerSignals?.list(limit) ?? []
458
+ : runtime.listExecutionObserverSignals(limit),
399
459
  });
400
460
  }
401
461
  if (request.method === "POST" &&
@@ -449,6 +509,8 @@ export async function createExecutionRuntimeProcess(input) {
449
509
  catch (error) {
450
510
  state = "STOPPED";
451
511
  accepting = false;
512
+ observerSignals?.close();
513
+ observerSignals = undefined;
452
514
  runtime?.close();
453
515
  runtime = undefined;
454
516
  throw error;
@@ -464,6 +526,8 @@ export async function createExecutionRuntimeProcess(input) {
464
526
  if (running)
465
527
  await new Promise((resolveStop, reject) => running.close((error) => (error ? reject(error) : resolveStop())));
466
528
  await Promise.allSettled([...active]);
529
+ observerSignals?.close();
530
+ observerSignals = undefined;
467
531
  runtime?.close();
468
532
  runtime = undefined;
469
533
  state = "STOPPED";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-execution-runtime",
3
- "version": "0.1.18",
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-execution-browser-extension": "^0.1.42",
28
- "@tomflow/proflow-execution-contracts": "^0.1.10",
29
- "@tomflow/proflow-model-contracts": "^0.1.10",
27
+ "@tomflow/proflow-execution-contracts": "^0.1.12",
28
+ "@tomflow/proflow-execution-local": "^0.1.16",
30
29
  "@tomflow/proflow-module-contract": "^0.1.13",
31
- "@tomflow/proflow-execution-local": "^0.1.15"
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.18",
6
+ "moduleVersion": "0.1.20",
7
7
  "kind": "service",
8
8
  "templateVersion": "1.0.0",
9
9
  "platformCompatibility": ">=1.0.0 <2.0.0",
@@ -18,6 +18,10 @@
18
18
  }
19
19
  ],
20
20
  "requires": [
21
+ {
22
+ "contractRef": "execution-browser-executor",
23
+ "versionRange": ">=1.0.0 <2.0.0"
24
+ },
21
25
  {
22
26
  "contractRef": "execution-local",
23
27
  "versionRange": ">=1.0.0 <2.0.0"