forge-workflow 0.1.0-beta.6 → 0.1.0-beta.7
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/docs/guides/MIGRATION.md +3 -3
- package/docs/reference/RELEASE.md +3 -3
- package/lib/commands/merge.js +2 -2
- package/lib/commands/push.js +13 -4
- package/lib/commands/shepherd.js +1 -1
- package/lib/commands/validate.js +13 -1
- package/lib/kernel/migrations.js +1 -1
- package/lib/kernel/sqlite-driver.js +1 -1
- package/lib/memory/router.js +1 -1
- package/lib/memory/usage-evidence.js +1 -1
- package/lib/pr-monitor/flow-monitor.js +2 -2
- package/lib/pr-monitor/reconcile-executor.js +1 -1
- package/lib/project-memory.js +1 -1
- package/lib/validation-receipt.js +190 -0
- package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/node_modules/@forge/contracts/index.js +32 -0
- package/node_modules/@forge/contracts/package.json +35 -0
- package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/node_modules/@forge/contracts/src/identity.js +42 -0
- package/node_modules/@forge/contracts/src/schema.js +72 -0
- package/node_modules/@forge/contracts/src/validate.js +305 -0
- package/node_modules/@forge/flow/index.js +114 -0
- package/node_modules/@forge/flow/package.json +34 -0
- package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
- package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
- package/node_modules/@forge/flow/src/executor.js +279 -0
- package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
- package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
- package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
- package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
- package/node_modules/@forge/memory/index.js +331 -0
- package/node_modules/@forge/memory/package.json +34 -0
- package/node_modules/@forge/memory/src/authority-provider.js +67 -0
- package/node_modules/@forge/memory/src/backend-registry.js +166 -0
- package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
- package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
- package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
- package/package.json +9 -1
- package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
- package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
- package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
- package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
- package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
- package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
- package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
- package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
- package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
- package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
- package/scripts/test.js +1 -1
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
canonicalize,
|
|
5
|
+
computeContentHash,
|
|
6
|
+
validateContract,
|
|
7
|
+
validateContractStructure,
|
|
8
|
+
} = require("@forge/contracts");
|
|
9
|
+
|
|
10
|
+
const LIFETIMES = new Set(["session", "run", "subject"]);
|
|
11
|
+
const TERMINAL_STATES = new Set(["PASS", "FAIL", "INCOMPLETE", "CANCELLED"]);
|
|
12
|
+
const MAX_HISTORY = 128;
|
|
13
|
+
const MAX_LIST_ITEMS = 128;
|
|
14
|
+
const MAX_PENDING = 128;
|
|
15
|
+
const MAX_RETRIES = 32;
|
|
16
|
+
const MAX_PAYLOAD_BYTES = 16_384;
|
|
17
|
+
|
|
18
|
+
class MonitorRuntimeError extends Error {
|
|
19
|
+
constructor(code, message, details = []) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "MonitorRuntimeError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
this.details = details;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nonEmptyString(value, name) {
|
|
28
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError(`${name} must be a non-empty string`);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateStringList(value, name) {
|
|
33
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_LIST_ITEMS) {
|
|
34
|
+
throw new TypeError(`${name} must contain 1 to ${MAX_LIST_ITEMS} items`);
|
|
35
|
+
}
|
|
36
|
+
for (const item of value) nonEmptyString(item, `${name} item`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function validateSpec(spec) {
|
|
40
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) throw new TypeError("MonitorSpec must be an object");
|
|
41
|
+
nonEmptyString(spec.monitorId, "monitorId");
|
|
42
|
+
nonEmptyString(spec.ownerRunId, "ownerRunId");
|
|
43
|
+
nonEmptyString(spec.packetId, "packetId");
|
|
44
|
+
nonEmptyString(spec.subject?.id, "subject.id");
|
|
45
|
+
nonEmptyString(spec.subject?.revision, "subject.revision");
|
|
46
|
+
validateStringList(spec.sourceAdapters, "sourceAdapters");
|
|
47
|
+
validateStringList(spec.deliveryTargets, "deliveryTargets");
|
|
48
|
+
if (!LIFETIMES.has(spec.lifetime)) throw new TypeError("lifetime is invalid");
|
|
49
|
+
if (typeof spec.deadline !== "string" || Number.isNaN(Date.parse(spec.deadline))) throw new TypeError("deadline is invalid");
|
|
50
|
+
if (typeof spec.reducer !== "function" || typeof spec.terminalPredicate !== "function") throw new TypeError("reducer and terminalPredicate are required");
|
|
51
|
+
if (spec.filter !== undefined && typeof spec.filter !== "function") throw new TypeError("filter must be a function");
|
|
52
|
+
if (!Number.isSafeInteger(spec.maxPending) || spec.maxPending < 1 || spec.maxPending > MAX_PENDING) {
|
|
53
|
+
throw new TypeError(`maxPending must be an integer from 1 to ${MAX_PENDING}`);
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isSafeInteger(spec.maxRetries) || spec.maxRetries < 0 || spec.maxRetries > MAX_RETRIES) {
|
|
56
|
+
throw new TypeError(`maxRetries must be an integer from 0 to ${MAX_RETRIES}`);
|
|
57
|
+
}
|
|
58
|
+
if (!Number.isSafeInteger(spec.retryBaseMs) || spec.retryBaseMs < 1) throw new TypeError("retryBaseMs must be a positive safe integer");
|
|
59
|
+
const maximumRetryDelay = spec.retryBaseMs * (2 ** Math.max(0, spec.maxRetries - 1));
|
|
60
|
+
if (!Number.isSafeInteger(maximumRetryDelay)) throw new TypeError("retry delay exceeds the safe integer ceiling");
|
|
61
|
+
if (!spec.securityPolicy || typeof spec.securityPolicy !== "object" || Array.isArray(spec.securityPolicy)
|
|
62
|
+
|| !Number.isSafeInteger(spec.securityPolicy.maxPayloadBytes)
|
|
63
|
+
|| spec.securityPolicy.maxPayloadBytes < 1
|
|
64
|
+
|| spec.securityPolicy.maxPayloadBytes > MAX_PAYLOAD_BYTES) {
|
|
65
|
+
throw new TypeError(`securityPolicy.maxPayloadBytes must be an integer from 1 to ${MAX_PAYLOAD_BYTES}`);
|
|
66
|
+
}
|
|
67
|
+
if (spec.maxHistory !== undefined
|
|
68
|
+
&& (!Number.isInteger(spec.maxHistory) || spec.maxHistory < 1 || spec.maxHistory > MAX_HISTORY)) {
|
|
69
|
+
throw new TypeError("maxHistory must be an integer from 1 to 128");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createMonitorState(spec) {
|
|
74
|
+
validateSpec(spec);
|
|
75
|
+
return {
|
|
76
|
+
monitorId: spec.monitorId,
|
|
77
|
+
ownerRunId: spec.ownerRunId,
|
|
78
|
+
lifecycle: "ACTIVE",
|
|
79
|
+
value: undefined,
|
|
80
|
+
valueDigest: undefined,
|
|
81
|
+
lastSequence: -1,
|
|
82
|
+
acknowledgementCursor: -1,
|
|
83
|
+
seenEvents: Object.create(null),
|
|
84
|
+
seenEventOrder: [],
|
|
85
|
+
evidenceHashes: [],
|
|
86
|
+
pending: [],
|
|
87
|
+
deferred: [],
|
|
88
|
+
retryCounts: {},
|
|
89
|
+
cancellationRequested: false,
|
|
90
|
+
cancellationAcknowledged: false,
|
|
91
|
+
terminalState: undefined,
|
|
92
|
+
terminalReason: undefined,
|
|
93
|
+
processCleanup: undefined,
|
|
94
|
+
leaseCleanup: undefined,
|
|
95
|
+
undeliveredCursor: undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function result(state, effects = [], modelTurns = 0) {
|
|
100
|
+
return { state, effects, modelTurns };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function terminating(state, terminalState, terminalReason, extra = {}, mutate = false) {
|
|
104
|
+
const undelivered = [...state.pending, ...state.deferred];
|
|
105
|
+
const next = mutate ? state : { ...state };
|
|
106
|
+
Object.assign(next, {
|
|
107
|
+
...extra,
|
|
108
|
+
lifecycle: "TERMINATING",
|
|
109
|
+
terminalState,
|
|
110
|
+
terminalReason,
|
|
111
|
+
undeliveredCursor: undelivered.length === 0
|
|
112
|
+
? undefined
|
|
113
|
+
: Math.min(...undelivered.map((item) => item.sequence)),
|
|
114
|
+
});
|
|
115
|
+
return result(next, [{ type: "CLEANUP_PROCESS" }, { type: "CLEANUP_LEASE" }]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateObservation(spec, event) {
|
|
119
|
+
const validation = validateContract(event, {
|
|
120
|
+
expected: { monitorId: spec.monitorId, subjectRevision: spec.subject.revision },
|
|
121
|
+
});
|
|
122
|
+
if (event?.schema_id !== "forge.memory.monitor-event.v1" || !validation.ok) {
|
|
123
|
+
throw new MonitorRuntimeError("INVALID_OBSERVATION", "MonitorEvent validation failed", validation.errors);
|
|
124
|
+
}
|
|
125
|
+
const configuredLimit = spec.securityPolicy?.maxPayloadBytes;
|
|
126
|
+
if (configuredLimit !== undefined) {
|
|
127
|
+
const bytes = Buffer.byteLength(canonicalize(event.payload.bounded_payload ?? {}), "utf8");
|
|
128
|
+
if (bytes > configuredLimit) throw new MonitorRuntimeError("INVALID_OBSERVATION", "MonitorEvent exceeds security policy");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function callbackClone(spec, value) {
|
|
133
|
+
if (value === undefined) return undefined;
|
|
134
|
+
try {
|
|
135
|
+
const clone = structuredClone(value);
|
|
136
|
+
canonicalize(clone, {
|
|
137
|
+
maxBytes: spec.securityPolicy?.maxPayloadBytes ?? 16_384,
|
|
138
|
+
maxDepth: 8,
|
|
139
|
+
maxNodes: 128,
|
|
140
|
+
});
|
|
141
|
+
return clone;
|
|
142
|
+
} catch {
|
|
143
|
+
throw new MonitorRuntimeError("INVALID_CALLBACK_VALUE", "Monitor callback value exceeds bounds");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function callbackBoolean(name, callback, ...args) {
|
|
148
|
+
let decision;
|
|
149
|
+
try {
|
|
150
|
+
decision = callback(...args);
|
|
151
|
+
} catch {
|
|
152
|
+
throw new MonitorRuntimeError("CALLBACK_FAILURE", `${name} failed`);
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
if (decision !== null
|
|
156
|
+
&& (typeof decision === "object" || typeof decision === "function")
|
|
157
|
+
&& typeof decision.then === "function") {
|
|
158
|
+
Promise.resolve(decision).catch(() => {});
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
throw new MonitorRuntimeError("CALLBACK_FAILURE", `${name} failed`);
|
|
162
|
+
}
|
|
163
|
+
if (typeof decision !== "boolean") {
|
|
164
|
+
throw new MonitorRuntimeError("INVALID_CALLBACK_DECISION", `${name} must return a synchronous boolean`);
|
|
165
|
+
}
|
|
166
|
+
return decision;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function callbackValue(spec, name, callback, ...args) {
|
|
170
|
+
let value;
|
|
171
|
+
try {
|
|
172
|
+
value = callback(...args);
|
|
173
|
+
} catch {
|
|
174
|
+
throw new MonitorRuntimeError("CALLBACK_FAILURE", `${name} failed`);
|
|
175
|
+
}
|
|
176
|
+
let thenable;
|
|
177
|
+
try {
|
|
178
|
+
thenable = value !== null
|
|
179
|
+
&& (typeof value === "object" || typeof value === "function")
|
|
180
|
+
&& typeof value.then === "function";
|
|
181
|
+
if (thenable) Promise.resolve(value).catch(() => {});
|
|
182
|
+
} catch {
|
|
183
|
+
throw new MonitorRuntimeError("CALLBACK_FAILURE", `${name} failed`);
|
|
184
|
+
}
|
|
185
|
+
if (thenable) {
|
|
186
|
+
throw new MonitorRuntimeError("INVALID_CALLBACK_VALUE", `${name} must be synchronous`);
|
|
187
|
+
}
|
|
188
|
+
return callbackClone(spec, value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function cloneIdentityMap(source) {
|
|
192
|
+
const clone = Object.create(null);
|
|
193
|
+
for (const key of Object.keys(source)) {
|
|
194
|
+
Object.defineProperty(clone, key, {
|
|
195
|
+
value: structuredClone(source[key]),
|
|
196
|
+
enumerable: true,
|
|
197
|
+
configurable: true,
|
|
198
|
+
writable: true,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return clone;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function reduceObservation(spec, state, event, mutate = false) {
|
|
205
|
+
validateObservation(spec, event);
|
|
206
|
+
const payload = event.payload;
|
|
207
|
+
const seen = Object.hasOwn(state.seenEvents, payload.event_id)
|
|
208
|
+
? state.seenEvents[payload.event_id]
|
|
209
|
+
: undefined;
|
|
210
|
+
if (seen !== undefined) {
|
|
211
|
+
if (seen.sequence !== payload.sequence || seen.contentHash !== event.content_hash) {
|
|
212
|
+
throw new MonitorRuntimeError("IDENTITY_CONFLICT", "MonitorEvent identity conflict");
|
|
213
|
+
}
|
|
214
|
+
return result(state);
|
|
215
|
+
}
|
|
216
|
+
if (payload.sequence <= state.lastSequence) {
|
|
217
|
+
throw new MonitorRuntimeError("OUT_OF_ORDER", "MonitorEvent sequence is not monotonic");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const next = mutate ? state : structuredClone(state);
|
|
221
|
+
if (!mutate) next.seenEvents = cloneIdentityMap(state.seenEvents);
|
|
222
|
+
Object.defineProperty(next.seenEvents, payload.event_id, {
|
|
223
|
+
value: { sequence: payload.sequence, contentHash: event.content_hash },
|
|
224
|
+
enumerable: true,
|
|
225
|
+
configurable: true,
|
|
226
|
+
writable: true,
|
|
227
|
+
});
|
|
228
|
+
next.seenEventOrder.push(payload.event_id);
|
|
229
|
+
next.evidenceHashes.push(event.content_hash);
|
|
230
|
+
const maxHistory = spec.maxHistory ?? MAX_HISTORY;
|
|
231
|
+
while (next.seenEventOrder.length > maxHistory) {
|
|
232
|
+
delete next.seenEvents[next.seenEventOrder.shift()];
|
|
233
|
+
}
|
|
234
|
+
if (next.evidenceHashes.length > maxHistory) {
|
|
235
|
+
next.evidenceHashes.splice(0, next.evidenceHashes.length - maxHistory);
|
|
236
|
+
}
|
|
237
|
+
next.lastSequence = payload.sequence;
|
|
238
|
+
if (spec.filter && !callbackBoolean("filter", spec.filter,
|
|
239
|
+
callbackClone(spec, payload.bounded_payload ?? {}),
|
|
240
|
+
callbackClone(spec, event),
|
|
241
|
+
)) return result(next);
|
|
242
|
+
|
|
243
|
+
const nextValue = callbackValue(spec, "reducer", spec.reducer,
|
|
244
|
+
callbackClone(spec, state.value),
|
|
245
|
+
callbackClone(spec, payload.bounded_payload ?? {}),
|
|
246
|
+
callbackClone(spec, event),
|
|
247
|
+
);
|
|
248
|
+
const nextDigest = computeContentHash({ value: nextValue });
|
|
249
|
+
const changed = nextDigest !== state.valueDigest;
|
|
250
|
+
next.value = structuredClone(nextValue);
|
|
251
|
+
next.valueDigest = nextDigest;
|
|
252
|
+
if (!changed) return result(next);
|
|
253
|
+
|
|
254
|
+
const terminal = callbackBoolean("terminalPredicate", spec.terminalPredicate,
|
|
255
|
+
callbackClone(spec, nextValue),
|
|
256
|
+
callbackClone(spec, event),
|
|
257
|
+
);
|
|
258
|
+
if (terminal) {
|
|
259
|
+
const terminalState = nextValue === "FAIL" ? "FAIL" : "PASS";
|
|
260
|
+
return terminating(next, terminalState, "terminal predicate satisfied", {}, mutate);
|
|
261
|
+
}
|
|
262
|
+
if (payload.actionability === "advisory") return result(next);
|
|
263
|
+
if (next.pending.length >= spec.maxPending) {
|
|
264
|
+
const deferred = { sequence: payload.sequence, eventId: payload.event_id };
|
|
265
|
+
if (next.deferred.length < spec.maxPending) {
|
|
266
|
+
next.deferred.push(deferred);
|
|
267
|
+
return result(next, [{ type: "BACKPRESSURE", decision: "DEFER", sequence: payload.sequence }]);
|
|
268
|
+
}
|
|
269
|
+
const dropped = next.deferred[next.deferred.length - 1];
|
|
270
|
+
next.deferred[next.deferred.length - 1] = deferred;
|
|
271
|
+
return result(next, [
|
|
272
|
+
{ type: "BACKPRESSURE", decision: "DROP", sequence: dropped.sequence, eventId: dropped.eventId },
|
|
273
|
+
{ type: "BACKPRESSURE", decision: "DEFER", sequence: payload.sequence },
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
next.pending.push({ sequence: payload.sequence, eventId: payload.event_id });
|
|
277
|
+
return result(next, [{
|
|
278
|
+
type: "DELIVER",
|
|
279
|
+
eventId: payload.event_id,
|
|
280
|
+
sequence: payload.sequence,
|
|
281
|
+
targets: [...spec.deliveryTargets],
|
|
282
|
+
}], 1);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function reduceAcknowledgement(spec, state, event, mutate = false) {
|
|
286
|
+
if (!Number.isInteger(event.sequence) || event.sequence < state.acknowledgementCursor || event.sequence > state.lastSequence) {
|
|
287
|
+
throw new MonitorRuntimeError("INVALID_ACKNOWLEDGEMENT", "Invalid acknowledgement cursor");
|
|
288
|
+
}
|
|
289
|
+
if (event.sequence === state.acknowledgementCursor) return result(state);
|
|
290
|
+
if (!state.pending.some((item) => item.sequence === event.sequence)) {
|
|
291
|
+
throw new MonitorRuntimeError("INVALID_ACKNOWLEDGEMENT", "Invalid acknowledgement cursor");
|
|
292
|
+
}
|
|
293
|
+
const next = mutate ? state : structuredClone(state);
|
|
294
|
+
next.acknowledgementCursor = event.sequence;
|
|
295
|
+
next.pending = next.pending.filter((item) => item.sequence > event.sequence);
|
|
296
|
+
for (const sequence of Object.keys(next.retryCounts)) {
|
|
297
|
+
if (Number(sequence) <= event.sequence) delete next.retryCounts[sequence];
|
|
298
|
+
}
|
|
299
|
+
const effects = [];
|
|
300
|
+
while (next.pending.length < spec.maxPending && next.deferred.length > 0) {
|
|
301
|
+
const delivery = next.deferred.shift();
|
|
302
|
+
next.pending.push(delivery);
|
|
303
|
+
effects.push({
|
|
304
|
+
type: "DELIVER",
|
|
305
|
+
eventId: delivery.eventId,
|
|
306
|
+
sequence: delivery.sequence,
|
|
307
|
+
targets: [...spec.deliveryTargets],
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return result(next, effects, effects.length);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function reduceDeliveryFailure(spec, state, event, mutate = false) {
|
|
314
|
+
if (!Number.isInteger(event.sequence) || !state.pending.some((item) => item.sequence === event.sequence)) {
|
|
315
|
+
throw new MonitorRuntimeError("INVALID_RETRY", "Retry does not reference pending delivery");
|
|
316
|
+
}
|
|
317
|
+
const next = mutate ? state : structuredClone(state);
|
|
318
|
+
const attempts = (next.retryCounts[event.sequence] ?? 0) + 1;
|
|
319
|
+
next.retryCounts[event.sequence] = attempts;
|
|
320
|
+
if (attempts > spec.maxRetries) return terminating(next, "INCOMPLETE", "delivery retries exhausted", {}, mutate);
|
|
321
|
+
const delayMs = spec.retryBaseMs * (2 ** (attempts - 1));
|
|
322
|
+
if (!Number.isSafeInteger(delayMs)) return terminating(next, "INCOMPLETE", "delivery retry delay overflow", {}, mutate);
|
|
323
|
+
return result(next, [{
|
|
324
|
+
type: "RETRY_DELIVERY",
|
|
325
|
+
sequence: event.sequence,
|
|
326
|
+
attempt: attempts,
|
|
327
|
+
delayMs,
|
|
328
|
+
observedAt: nonEmptyString(event.observedAt, "observedAt"),
|
|
329
|
+
}]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function reduceActive(spec, state, event, mutate = false) {
|
|
333
|
+
switch (event.kind) {
|
|
334
|
+
case "observation": return reduceObservation(spec, state, event.event, mutate);
|
|
335
|
+
case "acknowledge": return reduceAcknowledgement(spec, state, event, mutate);
|
|
336
|
+
case "delivery-failed": return reduceDeliveryFailure(spec, state, event, mutate);
|
|
337
|
+
case "cancel-requested": {
|
|
338
|
+
const next = mutate ? state : { ...state };
|
|
339
|
+
next.cancellationRequested = true;
|
|
340
|
+
return result(next, [{ type: "CANCEL_SOURCE" }]);
|
|
341
|
+
}
|
|
342
|
+
case "cancel-acknowledged":
|
|
343
|
+
if (!state.cancellationRequested) {
|
|
344
|
+
throw new MonitorRuntimeError("UNREQUESTED_CANCELLATION_ACK", "Invalid cancellation acknowledgement");
|
|
345
|
+
}
|
|
346
|
+
return terminating(state, "CANCELLED", "cancellation acknowledged", { cancellationAcknowledged: true }, mutate);
|
|
347
|
+
case "deadline":
|
|
348
|
+
return Date.parse(nonEmptyString(event.observedAt, "observedAt")) < Date.parse(spec.deadline)
|
|
349
|
+
? result(state)
|
|
350
|
+
: terminating(state, "INCOMPLETE", "deadline exceeded", {}, mutate);
|
|
351
|
+
case "session-ended":
|
|
352
|
+
return spec.lifetime === "session"
|
|
353
|
+
? terminating(state, "CANCELLED", "session lifetime ended", {}, mutate)
|
|
354
|
+
: result(state);
|
|
355
|
+
case "run-terminal": {
|
|
356
|
+
if (spec.lifetime !== "run") return result(state);
|
|
357
|
+
if (!TERMINAL_STATES.has(event.terminalState)) {
|
|
358
|
+
throw new MonitorRuntimeError("INVALID_TERMINAL_STATE", "Invalid terminal state");
|
|
359
|
+
}
|
|
360
|
+
return terminating(state, event.terminalState, "owner run terminal", {}, mutate);
|
|
361
|
+
}
|
|
362
|
+
case "subject-terminal": {
|
|
363
|
+
if (!TERMINAL_STATES.has(event.terminalState)) {
|
|
364
|
+
throw new MonitorRuntimeError("INVALID_TERMINAL_STATE", "Invalid terminal state");
|
|
365
|
+
}
|
|
366
|
+
return terminating(state, event.terminalState, "subject terminal", {}, mutate);
|
|
367
|
+
}
|
|
368
|
+
case "lease-lost": return terminating(state, "INCOMPLETE", "lease lost", {}, mutate);
|
|
369
|
+
default: throw new MonitorRuntimeError("UNKNOWN_EVENT", `Unsupported monitor event: ${event.kind}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function validateMonitorState(spec, state) {
|
|
374
|
+
if (!state || state.monitorId !== spec.monitorId || state.ownerRunId !== spec.ownerRunId) {
|
|
375
|
+
throw new MonitorRuntimeError("STATE_MISMATCH", "Monitor state does not match MonitorSpec");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function reduceMonitorValidated(spec, state, event, mutate = false) {
|
|
380
|
+
validateMonitorState(spec, state);
|
|
381
|
+
if (!event || typeof event !== "object" || Array.isArray(event)) throw new MonitorRuntimeError("INVALID_EVENT", "Monitor event is invalid");
|
|
382
|
+
if (state.lifecycle === "TERMINAL") throw new MonitorRuntimeError("POST_TERMINAL_EVENT", "Monitor rejects post-terminal events");
|
|
383
|
+
if (state.lifecycle === "TERMINATING") {
|
|
384
|
+
if (event.kind !== "cleanup-complete") throw new MonitorRuntimeError("TERMINATING_EVENT", "Monitor accepts only cleanup completion while terminating");
|
|
385
|
+
if (!event.processCleanup || typeof event.processCleanup !== "object" || !event.leaseCleanup || typeof event.leaseCleanup !== "object") {
|
|
386
|
+
throw new MonitorRuntimeError("INVALID_CLEANUP", "Cleanup proof is required");
|
|
387
|
+
}
|
|
388
|
+
const next = mutate ? state : { ...state };
|
|
389
|
+
next.lifecycle = "TERMINAL";
|
|
390
|
+
next.processCleanup = structuredClone(event.processCleanup);
|
|
391
|
+
next.leaseCleanup = structuredClone(event.leaseCleanup);
|
|
392
|
+
return result(next, [{ type: "ISSUE_MONITOR_RECEIPT" }]);
|
|
393
|
+
}
|
|
394
|
+
return reduceActive(spec, state, event, mutate);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function reduceMonitor(spec, state, event) {
|
|
398
|
+
validateSpec(spec);
|
|
399
|
+
return reduceMonitorValidated(spec, state, event);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function reduceMonitorBatch(spec, state, events) {
|
|
403
|
+
validateSpec(spec);
|
|
404
|
+
validateMonitorState(spec, state);
|
|
405
|
+
if (!Array.isArray(events) || events.length > MAX_HISTORY) {
|
|
406
|
+
throw new TypeError(`events must be an array with at most ${MAX_HISTORY} items`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const next = structuredClone(state);
|
|
410
|
+
next.seenEvents = cloneIdentityMap(state.seenEvents);
|
|
411
|
+
const effects = [];
|
|
412
|
+
let modelTurns = 0;
|
|
413
|
+
for (const event of events) {
|
|
414
|
+
const transition = reduceMonitorValidated(spec, next, event, true);
|
|
415
|
+
effects.push(...transition.effects);
|
|
416
|
+
modelTurns += transition.modelTurns;
|
|
417
|
+
}
|
|
418
|
+
return result(next, effects, modelTurns);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function createMonitorReceipt(spec, state, options = {}) {
|
|
422
|
+
validateSpec(spec);
|
|
423
|
+
if (state?.lifecycle !== "TERMINAL") throw new MonitorRuntimeError("NOT_TERMINAL", "Monitor is not ready for a receipt");
|
|
424
|
+
const producerInstanceId = nonEmptyString(options.producerInstanceId, "producerInstanceId");
|
|
425
|
+
const payload = {
|
|
426
|
+
monitor_id: spec.monitorId,
|
|
427
|
+
owner_run_id: spec.ownerRunId,
|
|
428
|
+
terminal_state: state.terminalState,
|
|
429
|
+
terminal_reason: state.terminalReason,
|
|
430
|
+
last_sequence: Math.max(0, state.lastSequence),
|
|
431
|
+
evidence_digest: computeContentHash({ evidence_hashes: state.evidenceHashes }),
|
|
432
|
+
cancellation_acknowledged: state.cancellationAcknowledged,
|
|
433
|
+
process_cleanup: structuredClone(state.processCleanup),
|
|
434
|
+
lease_cleanup: structuredClone(state.leaseCleanup),
|
|
435
|
+
};
|
|
436
|
+
if (state.undeliveredCursor !== undefined) payload.undelivered_cursor = state.undeliveredCursor;
|
|
437
|
+
const receipt = {
|
|
438
|
+
schema_id: "forge.memory.monitor-receipt.v1",
|
|
439
|
+
schema_version: 1,
|
|
440
|
+
object_id: nonEmptyString(options.objectId, "objectId"),
|
|
441
|
+
created_at: nonEmptyString(options.createdAt, "createdAt"),
|
|
442
|
+
producer: { product_id: "forge-flow", product_version: "0.1.0-beta.6", instance_id: producerInstanceId },
|
|
443
|
+
capabilities_used: [],
|
|
444
|
+
provenance: { source_kind: "monitor", actor_class: "system", actor_id: producerInstanceId },
|
|
445
|
+
payload,
|
|
446
|
+
extensions: {},
|
|
447
|
+
};
|
|
448
|
+
receipt.content_hash = computeContentHash(receipt);
|
|
449
|
+
const validation = validateContractStructure(receipt);
|
|
450
|
+
if (!validation.ok) throw new MonitorRuntimeError("INVALID_RECEIPT", "MonitorReceipt validation failed", validation.errors);
|
|
451
|
+
return receipt;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = {
|
|
455
|
+
MonitorRuntimeError,
|
|
456
|
+
createMonitorReceipt,
|
|
457
|
+
createMonitorState,
|
|
458
|
+
reduceMonitor,
|
|
459
|
+
reduceMonitorBatch,
|
|
460
|
+
};
|