@tomflow/proflow-execution-runtime 0.1.19 → 0.1.21
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 +12 -0
- package/dist/deployment/adapter.d.ts +10 -10
- package/dist/deployment/descriptor.d.ts +1 -1
- package/dist/deployment/descriptor.js +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +164 -17
- package/dist/src/service.js +2 -0
- package/package.json +5 -5
- package/proflow.module.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tomflow/proflow-execution-runtime
|
|
2
2
|
|
|
3
|
+
## 0.1.21
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- fix(execution): reconcile unknown monitor effects
|
|
8
|
+
|
|
9
|
+
## 0.1.20
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- fix(execution-runtime): unify formal effect identity
|
|
14
|
+
|
|
3
15
|
## 0.1.19
|
|
4
16
|
|
|
5
17
|
### 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.
|
|
9
|
+
moduleVersion: "0.1.21";
|
|
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.
|
|
26
|
+
readonly moduleVersion: "0.1.21";
|
|
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.
|
|
36
|
+
moduleVersion: "0.1.21";
|
|
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.
|
|
57
|
+
moduleVersion: "0.1.21";
|
|
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.
|
|
74
|
+
moduleVersion: "0.1.21";
|
|
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.
|
|
87
|
+
moduleVersion: "0.1.21";
|
|
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.
|
|
95
|
+
moduleVersion: "0.1.21";
|
|
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.
|
|
112
|
+
readonly moduleVersion: "0.1.21";
|
|
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.
|
|
121
|
+
readonly moduleVersion: "0.1.21";
|
|
122
122
|
} | {
|
|
123
123
|
contract: "deployment.result.v1";
|
|
124
124
|
moduleRef: "execution-runtime";
|
|
125
|
-
moduleVersion: "0.1.
|
|
125
|
+
moduleVersion: "0.1.21";
|
|
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.
|
|
6
|
+
readonly moduleVersion: "0.1.21";
|
|
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.
|
|
6
|
+
moduleVersion: "0.1.21",
|
|
7
7
|
kind: "service",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
|
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,20 @@ 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
|
|
552
|
-
|
|
675
|
+
const monitorChatEffect = request.capability === "monitor.chat.submit" ||
|
|
676
|
+
request.capability === "monitor.chat.create";
|
|
677
|
+
if (prior.status === "UNKNOWN" &&
|
|
678
|
+
prior.sideEffectState === "UNKNOWN" &&
|
|
679
|
+
(monitorChatEffect ||
|
|
680
|
+
request.capability === "product.discussion.bootstrap.deliver" ||
|
|
681
|
+
request.capability === "product.review.deliver"))
|
|
682
|
+
return reconcileUnknownReplay(request, existing, prior);
|
|
683
|
+
// Existing PENDING and APPLIED are read-only. Only proven absence permits retry.
|
|
684
|
+
if (monitorEffect && prior.sideEffectState !== "NOT_APPLIED")
|
|
685
|
+
return prior;
|
|
686
|
+
const safelyResumable = (monitorEffect && prior.sideEffectState === "NOT_APPLIED") ||
|
|
687
|
+
(prior.status === "PENDING" &&
|
|
688
|
+
prior.sideEffectState === "NOT_STARTED") ||
|
|
553
689
|
(prior.status === "FAILED" && prior.sideEffectState === "NOT_APPLIED");
|
|
554
690
|
if (!safelyResumable)
|
|
555
691
|
return prior;
|
|
@@ -561,9 +697,11 @@ export async function createExecutionRuntime(options) {
|
|
|
561
697
|
retryable: false,
|
|
562
698
|
updatedAt: now().toISOString(),
|
|
563
699
|
});
|
|
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);
|
|
700
|
+
const claimed = database
|
|
701
|
+
.prepare("UPDATE executions SET request_json=?, record_json=?, precondition_json=NULL WHERE execution_ref=? AND record_json=?")
|
|
702
|
+
.run(JSON.stringify(protectedValue(request)), JSON.stringify(record), record.executionRef, String(existing.record_json));
|
|
703
|
+
if (claimed.changes === 0)
|
|
704
|
+
return fromRow(getRow(record.executionRef));
|
|
567
705
|
await log(record, "EXECUTION_REDECISION_REQUESTED");
|
|
568
706
|
}
|
|
569
707
|
else {
|
|
@@ -658,9 +796,6 @@ export async function createExecutionRuntime(options) {
|
|
|
658
796
|
throw new ExecutionRuntimeError(timedOut ? "TIMEOUT" : "CANCELLED", timedOut
|
|
659
797
|
? "execution runtime timeout elapsed during admission"
|
|
660
798
|
: "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
799
|
const casRow = getRow(record.executionRef);
|
|
665
800
|
if (casRow) {
|
|
666
801
|
const casRecord = fromRow(casRow);
|
|
@@ -812,7 +947,12 @@ export async function createExecutionRuntime(options) {
|
|
|
812
947
|
const row = getRow(record.executionRef);
|
|
813
948
|
if (row)
|
|
814
949
|
record = fromRow(row);
|
|
815
|
-
if (record.sideEffectState === "
|
|
950
|
+
if (monitorEffect && record.sideEffectState === "APPLIED")
|
|
951
|
+
return record;
|
|
952
|
+
if (monitorEffect && record.sideEffectState === "STARTED") {
|
|
953
|
+
record = failRecord(record, failure, "UNKNOWN");
|
|
954
|
+
}
|
|
955
|
+
else if (record.sideEffectState === "STARTED") {
|
|
816
956
|
const executor = browserIds.has(request.capability)
|
|
817
957
|
? options.browserExecutor
|
|
818
958
|
: options.localExecutor;
|
|
@@ -914,6 +1054,10 @@ export async function createExecutionRuntime(options) {
|
|
|
914
1054
|
let record = getExecutionRecord(request.executionRef);
|
|
915
1055
|
if (record.callerRef !== request.callerRef)
|
|
916
1056
|
throw new ExecutionRuntimeError("IDENTITY_INVALID", "only the bound caller may cancel execution");
|
|
1057
|
+
// Admission may still be in flight. Cancellation cannot prove it has stopped,
|
|
1058
|
+
// so it must not expose NOT_APPLIED and let another caller claim this row.
|
|
1059
|
+
if (isFormalEffectCapability(record.capability) && record.status === "PENDING")
|
|
1060
|
+
throw new ExecutionRuntimeError("INVALID_REQUEST", "Monitor PENDING must settle through effect.execute");
|
|
917
1061
|
controllers.get(request.executionRef)?.abort();
|
|
918
1062
|
if (record.status === "PENDING") {
|
|
919
1063
|
record = failRecord(record, new ExecutionRuntimeError("CANCELLED", request.reason));
|
|
@@ -926,6 +1070,8 @@ export async function createExecutionRuntime(options) {
|
|
|
926
1070
|
const rows = database.prepare("SELECT * FROM executions").all();
|
|
927
1071
|
for (const row of rows) {
|
|
928
1072
|
let record = fromRow(row);
|
|
1073
|
+
if (isFormalEffectCapability(record.capability))
|
|
1074
|
+
continue;
|
|
929
1075
|
if (record.status !== "PENDING" &&
|
|
930
1076
|
record.status !== "RUNNING" &&
|
|
931
1077
|
record.status !== "UNKNOWN")
|
|
@@ -986,6 +1132,7 @@ export async function createExecutionRuntime(options) {
|
|
|
986
1132
|
await recoverIncomplete();
|
|
987
1133
|
return Object.freeze({
|
|
988
1134
|
executeCapability,
|
|
1135
|
+
executeEffect,
|
|
989
1136
|
lookupExecutionIntent,
|
|
990
1137
|
listExecutionObserverSignals,
|
|
991
1138
|
acknowledgeExecutionObserverSignal,
|
package/dist/src/service.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.21",
|
|
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-
|
|
28
|
-
"@tomflow/proflow-execution-
|
|
29
|
-
"@tomflow/proflow-module-contract": "^0.1.13",
|
|
27
|
+
"@tomflow/proflow-execution-browser-extension": "^0.1.76",
|
|
28
|
+
"@tomflow/proflow-execution-contracts": "^0.1.12",
|
|
30
29
|
"@tomflow/proflow-execution-local": "^0.1.16",
|
|
31
|
-
"@tomflow/proflow-
|
|
30
|
+
"@tomflow/proflow-model-contracts": "^0.1.10",
|
|
31
|
+
"@tomflow/proflow-module-contract": "^0.1.13"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@tomflow/proflow-deployment-conformance": "^0.1.13"
|
package/proflow.module.json
CHANGED
|
@@ -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.
|
|
6
|
+
"moduleVersion": "0.1.21",
|
|
7
7
|
"kind": "service",
|
|
8
8
|
"templateVersion": "1.0.0",
|
|
9
9
|
"platformCompatibility": ">=1.0.0 <2.0.0",
|