@tomflow/proflow-execution-runtime 0.1.17 → 0.1.19

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 ADDED
@@ -0,0 +1,13 @@
1
+ # @tomflow/proflow-execution-runtime
2
+
3
+ ## 0.1.19
4
+
5
+ ### Patch Changes
6
+
7
+ - Close the Real-3 runtime reconciliation, health, browser effect-result boundary, observer pagination, and standalone execution-runtime configuration gaps for final human acceptance.
8
+
9
+ ## 0.1.18
10
+
11
+ ### Patch Changes
12
+
13
+ - Close Real-3 node generation identity fencing across Platform Host admission, durable Execution idempotency, and Observer/Approval resume provenance.
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.17";
9
+ moduleVersion: "0.1.19";
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.17";
26
+ readonly moduleVersion: "0.1.19";
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.17";
36
+ moduleVersion: "0.1.19";
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.17";
57
+ moduleVersion: "0.1.19";
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.17";
74
+ moduleVersion: "0.1.19";
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.17";
87
+ moduleVersion: "0.1.19";
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.17";
95
+ moduleVersion: "0.1.19";
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.17";
112
+ readonly moduleVersion: "0.1.19";
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.17";
121
+ readonly moduleVersion: "0.1.19";
122
122
  } | {
123
123
  contract: "deployment.result.v1";
124
124
  moduleRef: "execution-runtime";
125
- moduleVersion: "0.1.17";
125
+ moduleVersion: "0.1.19";
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.17";
6
+ readonly moduleVersion: "0.1.19";
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.17",
6
+ moduleVersion: "0.1.19",
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,
package/dist/src/index.js CHANGED
@@ -92,11 +92,42 @@ export function executionInputFingerprint(input) {
92
92
  callerRef: request.callerRef,
93
93
  taskId: request.taskId,
94
94
  nodeId: request.nodeId,
95
+ runNo: request.runNo,
95
96
  roleRef: request.roleRef,
96
97
  workerRef: request.workerRef,
97
98
  projectRoot: request.projectRoot,
98
99
  });
99
100
  }
101
+ function legacyExecutionInputFingerprint(input) {
102
+ const request = parseExecuteCapabilityRequest(input);
103
+ return sha({
104
+ capability: request.capability,
105
+ input: protectedValue(request.input),
106
+ callerRef: request.callerRef,
107
+ taskId: request.taskId,
108
+ nodeId: request.nodeId,
109
+ roleRef: request.roleRef,
110
+ workerRef: request.workerRef,
111
+ projectRoot: request.projectRoot,
112
+ });
113
+ }
114
+ function compatibleDurableInputFingerprint(row, request, currentFingerprint) {
115
+ const durableFingerprint = String(row.input_fingerprint);
116
+ if (durableFingerprint === currentFingerprint)
117
+ return durableFingerprint;
118
+ try {
119
+ const durableRequest = parseExecuteCapabilityRequest(JSON.parse(String(row.request_json)));
120
+ const durableRecord = parseExecutionRecord(JSON.parse(String(row.record_json)));
121
+ if (durableFingerprint === legacyExecutionInputFingerprint(request) &&
122
+ durableRequest.runNo === request.runNo &&
123
+ durableRecord.runNo === request.runNo)
124
+ return durableFingerprint;
125
+ }
126
+ catch {
127
+ return undefined;
128
+ }
129
+ return undefined;
130
+ }
100
131
  function materializeArtifactRefs(artifacts) {
101
132
  return artifacts.map((artifact) => ({
102
133
  ref: artifact.ref,
@@ -497,7 +528,7 @@ export async function createExecutionRuntime(options) {
497
528
  .get(request.callerRef, request.capability, request.idempotencyKey);
498
529
  if (!row)
499
530
  throw new ExecutionRuntimeError("INVALID_REQUEST", "execution intent was not found");
500
- if (String(row.input_fingerprint) !== executionInputFingerprint(request))
531
+ if (compatibleDurableInputFingerprint(row, request, executionInputFingerprint(request)) === undefined)
501
532
  throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "execution intent lookup input does not match durable input");
502
533
  return fromRow(row);
503
534
  }
@@ -506,14 +537,16 @@ export async function createExecutionRuntime(options) {
506
537
  throw new ExecutionRuntimeError("EXECUTOR_UNAVAILABLE", "execution runtime is closed");
507
538
  const request = parseExecuteCapabilityRequest(input);
508
539
  assertPatchArtifactScope(request);
509
- const inputFingerprint = executionInputFingerprint(request);
540
+ let inputFingerprint = executionInputFingerprint(request);
510
541
  const existing = database
511
542
  .prepare("SELECT * FROM executions WHERE caller_ref=? AND capability=? AND idempotency_key=?")
512
543
  .get(request.callerRef, request.capability, request.idempotencyKey);
513
544
  let record;
514
545
  if (existing) {
515
- if (String(existing.input_fingerprint) !== inputFingerprint)
546
+ const durableFingerprint = compatibleDurableInputFingerprint(existing, request, inputFingerprint);
547
+ if (durableFingerprint === undefined)
516
548
  throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "idempotency key was already used with different critical input");
549
+ inputFingerprint = durableFingerprint;
517
550
  const prior = fromRow(existing);
518
551
  const safelyResumable = (prior.status === "PENDING" &&
519
552
  prior.sideEffectState === "NOT_STARTED") ||
@@ -545,7 +578,8 @@ export async function createExecutionRuntime(options) {
545
578
  const raced = database
546
579
  .prepare("SELECT * FROM executions WHERE caller_ref=? AND capability=? AND idempotency_key=?")
547
580
  .get(request.callerRef, request.capability, request.idempotencyKey);
548
- if (!raced || String(raced.input_fingerprint) !== inputFingerprint)
581
+ if (!raced ||
582
+ compatibleDurableInputFingerprint(raced, request, inputFingerprint) === undefined)
549
583
  throw new ExecutionRuntimeError("IDEMPOTENCY_CONFLICT", "concurrent idempotency conflict");
550
584
  return fromRow(raced);
551
585
  }
@@ -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
  }
@@ -394,8 +449,11 @@ export async function createExecutionRuntimeProcess(input) {
394
449
  if (request.method === "POST" &&
395
450
  url.pathname === "/observer-signals/list") {
396
451
  const value = object(body ?? {}, "observer signal list input");
452
+ const limit = typeof value.limit === "number" ? value.limit : 50;
397
453
  return respond(response, 200, {
398
- signals: runtime.listExecutionObserverSignals(typeof value.limit === "number" ? value.limit : 50),
454
+ signals: value.consumer === "task-reconciliation"
455
+ ? observerSignals?.list(limit) ?? []
456
+ : runtime.listExecutionObserverSignals(limit),
399
457
  });
400
458
  }
401
459
  if (request.method === "POST" &&
@@ -449,6 +507,8 @@ export async function createExecutionRuntimeProcess(input) {
449
507
  catch (error) {
450
508
  state = "STOPPED";
451
509
  accepting = false;
510
+ observerSignals?.close();
511
+ observerSignals = undefined;
452
512
  runtime?.close();
453
513
  runtime = undefined;
454
514
  throw error;
@@ -464,6 +524,8 @@ export async function createExecutionRuntimeProcess(input) {
464
524
  if (running)
465
525
  await new Promise((resolveStop, reject) => running.close((error) => (error ? reject(error) : resolveStop())));
466
526
  await Promise.allSettled([...active]);
527
+ observerSignals?.close();
528
+ observerSignals = undefined;
467
529
  runtime?.close();
468
530
  runtime = undefined;
469
531
  state = "STOPPED";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/proflow-execution-runtime",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -25,10 +25,10 @@
25
25
  ],
26
26
  "dependencies": {
27
27
  "@tomflow/proflow-model-contracts": "^0.1.10",
28
- "@tomflow/proflow-execution-contracts": "^0.1.10",
29
- "@tomflow/proflow-execution-browser-extension": "^0.1.37",
30
- "@tomflow/proflow-execution-local": "^0.1.15",
31
- "@tomflow/proflow-module-contract": "^0.1.13"
28
+ "@tomflow/proflow-execution-browser-extension": "^0.1.51",
29
+ "@tomflow/proflow-module-contract": "^0.1.13",
30
+ "@tomflow/proflow-execution-local": "^0.1.16",
31
+ "@tomflow/proflow-execution-contracts": "^0.1.11"
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.17",
6
+ "moduleVersion": "0.1.19",
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"