@sensigo/realm 0.33.0 → 0.34.0
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/dist/engine/execution-loop.d.ts +22 -1
- package/dist/engine/execution-loop.d.ts.map +1 -1
- package/dist/engine/execution-loop.js +345 -6
- package/dist/engine/execution-loop.js.map +1 -1
- package/dist/engine/gate-timing.d.ts +17 -0
- package/dist/engine/gate-timing.d.ts.map +1 -0
- package/dist/engine/gate-timing.js +19 -0
- package/dist/engine/gate-timing.js.map +1 -0
- package/dist/engine/reclaim-step.d.ts +8 -0
- package/dist/engine/reclaim-step.d.ts.map +1 -1
- package/dist/engine/reclaim-step.js +48 -3
- package/dist/engine/reclaim-step.js.map +1 -1
- package/dist/engine/run-health.d.ts +1 -1
- package/dist/engine/run-health.d.ts.map +1 -1
- package/dist/engine/run-health.js +39 -2
- package/dist/engine/run-health.js.map +1 -1
- package/dist/engine/settlement.d.ts +52 -14
- package/dist/engine/settlement.d.ts.map +1 -1
- package/dist/engine/settlement.js +258 -16
- package/dist/engine/settlement.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/types/response-envelope.d.ts +16 -0
- package/dist/types/response-envelope.d.ts.map +1 -1
- package/dist/types/run-record.d.ts +106 -0
- package/dist/types/run-record.d.ts.map +1 -1
- package/dist/types/settlement.d.ts +31 -2
- package/dist/types/settlement.d.ts.map +1 -1
- package/dist/types/workflow-definition.d.ts +113 -32
- package/dist/types/workflow-definition.d.ts.map +1 -1
- package/dist/types/workflow-definition.js +20 -0
- package/dist/types/workflow-definition.js.map +1 -1
- package/dist/workflow/diagnostics.d.ts +1 -1
- package/dist/workflow/diagnostics.d.ts.map +1 -1
- package/dist/workflow/diagnostics.js +3 -0
- package/dist/workflow/diagnostics.js.map +1 -1
- package/dist/workflow/structured-output-eligibility.d.ts +81 -0
- package/dist/workflow/structured-output-eligibility.d.ts.map +1 -0
- package/dist/workflow/structured-output-eligibility.js +359 -0
- package/dist/workflow/structured-output-eligibility.js.map +1 -0
- package/dist/workflow/yaml-loader.d.ts.map +1 -1
- package/dist/workflow/yaml-loader.js +145 -2
- package/dist/workflow/yaml-loader.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RunRecord, AgentTraceEntry } from '../types/run-record.js';
|
|
1
|
+
import type { RunRecord, AgentTraceEntry, StructuredOutputMeta } from '../types/run-record.js';
|
|
2
2
|
import type { ToolCallRecord } from '../types/mcp-types.js';
|
|
3
3
|
import type { ResponseEnvelope, NextAction } from '../types/response-envelope.js';
|
|
4
4
|
import { WorkflowError } from '../types/workflow-error.js';
|
|
@@ -22,9 +22,14 @@ export interface ExecuteStepOptions {
|
|
|
22
22
|
* Tool calls produced by callStepWithTools for this step.
|
|
23
23
|
* Absent on the callStep path (no tools configured).
|
|
24
24
|
* Present (possibly []) when tools were declared — threads through to captureEvidence.
|
|
25
|
+
*
|
|
26
|
+
* `structuredOutput` (issue #236): the caller's own `structured_output: 'strict'` attempt
|
|
27
|
+
* disclosure for THIS attempt — threads into the diagnostics literal at every evidence-capture
|
|
28
|
+
* site. Independent of `toolCalls` (either alone must still cause `stepMeta` to be passed).
|
|
25
29
|
*/
|
|
26
30
|
stepMeta?: {
|
|
27
31
|
toolCalls?: ToolCallRecord[];
|
|
32
|
+
structuredOutput?: StructuredOutputMeta;
|
|
28
33
|
};
|
|
29
34
|
/**
|
|
30
35
|
* Optional agent-submitted trace entries for this step.
|
|
@@ -50,6 +55,11 @@ export interface ExecuteStepOptions {
|
|
|
50
55
|
* today's byte-identical behavior.
|
|
51
56
|
*/
|
|
52
57
|
writerNonce?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Issue #291: injectable clock for deterministic expiry tests (the execute_step pre-refusal
|
|
60
|
+
* enact-then-proceed check reads this). Defaults to `new Date()` — real callers never set it.
|
|
61
|
+
*/
|
|
62
|
+
now?: Date;
|
|
53
63
|
}
|
|
54
64
|
export interface SubmitGateOptions {
|
|
55
65
|
runId: string;
|
|
@@ -70,6 +80,12 @@ export interface SubmitGateOptions {
|
|
|
70
80
|
* credential model stays authoritative); no arm ever reads it.
|
|
71
81
|
*/
|
|
72
82
|
respondedBy?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Issue #291: injectable clock for deterministic expiry tests (the F3 write-free
|
|
85
|
+
* `gate_expired_pending` refusal + the caller-issued `expire_gate` follow-up both read this).
|
|
86
|
+
* Defaults to `new Date()` — real callers never set it.
|
|
87
|
+
*/
|
|
88
|
+
now?: Date;
|
|
73
89
|
}
|
|
74
90
|
export interface ExecuteChainOptions {
|
|
75
91
|
runId: string;
|
|
@@ -82,9 +98,14 @@ export interface ExecuteChainOptions {
|
|
|
82
98
|
* Tool calls produced by callStepWithTools for this step.
|
|
83
99
|
* Absent on the callStep path (no tools configured).
|
|
84
100
|
* Present (possibly []) when tools were declared — threads through to captureEvidence.
|
|
101
|
+
*
|
|
102
|
+
* `structuredOutput` (issue #236): the caller's own `structured_output: 'strict'` attempt
|
|
103
|
+
* disclosure for THIS attempt — threads into the diagnostics literal at every evidence-capture
|
|
104
|
+
* site. Independent of `toolCalls` (either alone must still cause `stepMeta` to be passed).
|
|
85
105
|
*/
|
|
86
106
|
stepMeta?: {
|
|
87
107
|
toolCalls?: ToolCallRecord[];
|
|
108
|
+
structuredOutput?: StructuredOutputMeta;
|
|
88
109
|
};
|
|
89
110
|
/** @see ExecuteStepOptions.trace */
|
|
90
111
|
trace?: AgentTraceEntry[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution-loop.d.ts","sourceRoot":"","sources":["../../src/engine/execution-loop.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EAIT,eAAe,
|
|
1
|
+
{"version":3,"file":"execution-loop.d.ts","sourceRoot":"","sources":["../../src/engine/execution-loop.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,8 @@ import { persistsField } from '../store/store-fidelity.js';
|
|
|
4
4
|
import { storeDeclaresSeal, storeDeclaresNonceCarriage } from '../store/trace-buffer-store.js';
|
|
5
5
|
import { partitionBufferedEntries } from './trace-adoption.js';
|
|
6
6
|
import { deriveDefaultedSteps } from './defaulted-steps.js';
|
|
7
|
-
import {
|
|
7
|
+
import { computeGateDueState } from './gate-timing.js';
|
|
8
|
+
import { selectFinalizers, deriveEffectiveTriggers, applySettlement } from './settlement.js';
|
|
8
9
|
import { captureEvidence } from '../evidence/snapshot.js';
|
|
9
10
|
import { validateInputSchema, validateOutputSchema, validateTraceSchema, } from '../validation/input-schema.js';
|
|
10
11
|
import { normalizeTrace } from './trace-normalizer.js';
|
|
@@ -770,6 +771,72 @@ async function buildAlreadySettledEnvelope(store, definition, options, result, t
|
|
|
770
771
|
*/
|
|
771
772
|
const DORMANCY_ADVISORY = 'settled via the legacy compatibility path — this store does not declare atomic settlement ' +
|
|
772
773
|
'(RunStore.settleStep); upgrade the store to close the fan-out seal race (issue #279)';
|
|
774
|
+
/**
|
|
775
|
+
* issue #291 (D1 "execute_step pre-refusal" enactment point — enact-then-proceed): if `run`
|
|
776
|
+
* carries an expired, enactable gate (`expires_at` past, `on_expiry` frozen), enacts it via the
|
|
777
|
+
* SAME dormancy-discriminated pattern `submitHumanResponse` uses (settleStep when declared, else
|
|
778
|
+
* the pure `applySettlement` transform + `store.update`'s CAS write) and returns the resulting
|
|
779
|
+
* (possibly unchanged) run plus any disclosure line for the caller's `warnings` array. A
|
|
780
|
+
* finding-only gate (no `on_expiry`) is a fast no-op — nothing to enact, never touched. Any
|
|
781
|
+
* refusal from the enactment attempt (a benign race: `already_settled`/`not_expired`/
|
|
782
|
+
* `gate_mismatch`/`run_terminal`) is absorbed silently — the caller's own subsequent eligibility
|
|
783
|
+
* re-check against the returned `run` is what actually matters, and a NOOP correctly leaves `run`
|
|
784
|
+
* as the fresh state the refusal matched against. [F4] advisory-not-crash: an external store's
|
|
785
|
+
* OWN `settleStep` throwing on the `expire_gate` kind (a pre-#291 re-implementing store
|
|
786
|
+
* honoring the union-openness contract's refuse-loud mandate) degrades to a console advisory and
|
|
787
|
+
* proceeds with `run` UNCHANGED — never crashes the caller's verb.
|
|
788
|
+
*/
|
|
789
|
+
async function enactExpiredGateIfDue(store, definition, run, registry, now) {
|
|
790
|
+
const gate = run.pending_gate;
|
|
791
|
+
if (gate === undefined ||
|
|
792
|
+
gate.expires_at === undefined ||
|
|
793
|
+
gate.on_expiry === undefined ||
|
|
794
|
+
now.getTime() < new Date(gate.expires_at).getTime()) {
|
|
795
|
+
return { run };
|
|
796
|
+
}
|
|
797
|
+
const delta = { kind: 'expire_gate', gateId: gate.gate_id };
|
|
798
|
+
let expireOutcome;
|
|
799
|
+
try {
|
|
800
|
+
if (store.settleStep !== undefined) {
|
|
801
|
+
expireOutcome = await store.settleStep(run.id, delta, definition, { now });
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
const pure = applySettlement(run, delta, definition, { now });
|
|
805
|
+
if (!pure.applied) {
|
|
806
|
+
return { run: pure.run };
|
|
807
|
+
}
|
|
808
|
+
const persisted = await store.update(pure.run);
|
|
809
|
+
expireOutcome = { ...pure, run: persisted };
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
console.warn(`⚠ realm: could not enact run '${run.id}''s expired gate '${gate.gate_id}' (${err instanceof Error ? err.message : String(err)}) — proceeding with the pre-enactment state.`);
|
|
814
|
+
return { run };
|
|
815
|
+
}
|
|
816
|
+
if (!expireOutcome.applied) {
|
|
817
|
+
return { run: expireOutcome.run };
|
|
818
|
+
}
|
|
819
|
+
let finalRun = expireOutcome.run;
|
|
820
|
+
const disclosureParts = [];
|
|
821
|
+
const disposition = finalRun.settled?.[gate.step_name]?.resolved_by === 'timeout' ? 'settle_default' : 'abort';
|
|
822
|
+
disclosureParts.push(`gate '${gate.gate_id}' on '${gate.step_name}' had expired — enacted declared ${disposition} before this execute_step call (enacted_via: execute_step).`);
|
|
823
|
+
if (expireOutcome.transitioned) {
|
|
824
|
+
try {
|
|
825
|
+
const drainOutcome = await drainFinalizers(store, definition, registry, run.id);
|
|
826
|
+
finalRun = drainOutcome.run;
|
|
827
|
+
disclosureParts.push(...drainOutcome.warnings);
|
|
828
|
+
}
|
|
829
|
+
catch (err) {
|
|
830
|
+
disclosureParts.push(`post-commit finalizer drain failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const disclosure = disclosureParts.join(' ');
|
|
834
|
+
// Printed unconditionally (never silently dropped, regardless of which downstream envelope
|
|
835
|
+
// path the caller's own request takes) — the caller ALSO threads this into whichever
|
|
836
|
+
// response-envelope warnings array is in scope at its own return point.
|
|
837
|
+
console.warn(`⚠ ${disclosure}`);
|
|
838
|
+
return { run: finalRun, disclosure };
|
|
839
|
+
}
|
|
773
840
|
/**
|
|
774
841
|
* Validates eligibility, claims the step, executes it through the dispatcher with retry
|
|
775
842
|
* and timeout support, captures evidence, persists the updated run record, and returns
|
|
@@ -793,6 +860,18 @@ export async function executeStep(store, definition, options) {
|
|
|
793
860
|
});
|
|
794
861
|
return makeErrorEnvelope(options, null, internal);
|
|
795
862
|
}
|
|
863
|
+
// Step 1.5 (issue #291, D1 "execute_step pre-refusal" enactment point): if this run's gate has
|
|
864
|
+
// expired with an enactable disposition, enact it BEFORE the eligibility check below — a
|
|
865
|
+
// finding-only or non-expired gate is an immediate no-op (same `run` reference back). Level-
|
|
866
|
+
// triggering: the requested step may become newly eligible right here (settle_default/abort
|
|
867
|
+
// both clear `pending_gate`, un-blocking `findEligibleSteps`'s gate-serialization exclusion).
|
|
868
|
+
const gateExpiryCheckNow = options.now ?? new Date();
|
|
869
|
+
let gateExpiryDisclosure;
|
|
870
|
+
if (run.pending_gate !== undefined) {
|
|
871
|
+
const enacted = await enactExpiredGateIfDue(store, definition, run, options.registry, gateExpiryCheckNow);
|
|
872
|
+
run = enacted.run;
|
|
873
|
+
gateExpiryDisclosure = enacted.disclosure;
|
|
874
|
+
}
|
|
796
875
|
// Step 2: Check eligibility.
|
|
797
876
|
const eligible = findEligibleSteps(definition, run);
|
|
798
877
|
if (!eligible.includes(options.command)) {
|
|
@@ -804,7 +883,7 @@ export async function executeStep(store, definition, options) {
|
|
|
804
883
|
status: 'blocked',
|
|
805
884
|
data: {},
|
|
806
885
|
evidence: [],
|
|
807
|
-
warnings: [],
|
|
886
|
+
warnings: gateExpiryDisclosure !== undefined ? [gateExpiryDisclosure] : [],
|
|
808
887
|
errors: [],
|
|
809
888
|
agent_action: 'resolve_precondition',
|
|
810
889
|
context_hint: `Step '${options.command}' is not eligible in the current run state.`,
|
|
@@ -1022,6 +1101,10 @@ export async function executeStep(store, definition, options) {
|
|
|
1022
1101
|
// walEntries is declared at this outer scope because it is REASSIGNED to the post-claim read
|
|
1023
1102
|
// below and referenced at the captureEvidence call site further down this function.
|
|
1024
1103
|
const traceWarnings = [];
|
|
1104
|
+
// issue #291: the Step-1.5 gate-expiry disclosure (if any) now rides every downstream envelope
|
|
1105
|
+
// this function's own `traceWarnings` threading already reaches.
|
|
1106
|
+
if (gateExpiryDisclosure !== undefined)
|
|
1107
|
+
traceWarnings.push(gateExpiryDisclosure);
|
|
1025
1108
|
let preNormalizedTrace;
|
|
1026
1109
|
let walEntries = [];
|
|
1027
1110
|
let preClaimSchemaResult;
|
|
@@ -1724,6 +1807,20 @@ export async function executeStep(store, definition, options) {
|
|
|
1724
1807
|
...(attemptError === null && (run.validation_rejections?.[options.command] ?? 0) > 0
|
|
1725
1808
|
? { validation_rejections: run.validation_rejections[options.command] }
|
|
1726
1809
|
: {}),
|
|
1810
|
+
// issue #236: the attempt's structured_output disclosure. External-agent stamp [Rv6 +
|
|
1811
|
+
// R2-3]: a step that DECLARED structured_output but arrives with no
|
|
1812
|
+
// options.stepMeta.structuredOutput at all was driven by something other than
|
|
1813
|
+
// run-agent (e.g. an external agent calling execute_step over MCP directly) — realm
|
|
1814
|
+
// cannot know whether strict was honored, so it says so rather than staying silent.
|
|
1815
|
+
...(stepDef?.structured_output !== undefined
|
|
1816
|
+
? {
|
|
1817
|
+
structured_output: options.stepMeta?.structuredOutput ?? {
|
|
1818
|
+
requested: true,
|
|
1819
|
+
sent: false,
|
|
1820
|
+
downgrade_reason: 'external_agent',
|
|
1821
|
+
},
|
|
1822
|
+
}
|
|
1823
|
+
: {}),
|
|
1727
1824
|
},
|
|
1728
1825
|
...(profileData !== undefined
|
|
1729
1826
|
? { agentProfile: profile, agentProfileHash: profileData.content_hash }
|
|
@@ -1830,6 +1927,17 @@ export async function executeStep(store, definition, options) {
|
|
|
1830
1927
|
precondition_trace: preconditionTrace,
|
|
1831
1928
|
settled_by_default: true,
|
|
1832
1929
|
validation_rejections: exhaustion.details['rejections'],
|
|
1930
|
+
// issue #236: same disclosure/external-agent-stamp rule as the real dispatch-loop
|
|
1931
|
+
// capture above.
|
|
1932
|
+
...(stepDef?.structured_output !== undefined
|
|
1933
|
+
? {
|
|
1934
|
+
structured_output: options.stepMeta?.structuredOutput ?? {
|
|
1935
|
+
requested: true,
|
|
1936
|
+
sent: false,
|
|
1937
|
+
downgrade_reason: 'external_agent',
|
|
1938
|
+
},
|
|
1939
|
+
}
|
|
1940
|
+
: {}),
|
|
1833
1941
|
},
|
|
1834
1942
|
...(defaultProfileData !== undefined
|
|
1835
1943
|
? { agentProfile: defaultProfile, agentProfileHash: defaultProfileData.content_hash }
|
|
@@ -1887,6 +1995,17 @@ export async function executeStep(store, definition, options) {
|
|
|
1887
1995
|
input_token_estimate: inputTokenEstimate,
|
|
1888
1996
|
precondition_trace: preconditionTrace,
|
|
1889
1997
|
validation_rejections: exhaustion.details['rejections'],
|
|
1998
|
+
// issue #236: same disclosure/external-agent-stamp rule as the real dispatch-loop
|
|
1999
|
+
// capture above.
|
|
2000
|
+
...(stepDef?.structured_output !== undefined
|
|
2001
|
+
? {
|
|
2002
|
+
structured_output: options.stepMeta?.structuredOutput ?? {
|
|
2003
|
+
requested: true,
|
|
2004
|
+
sent: false,
|
|
2005
|
+
downgrade_reason: 'external_agent',
|
|
2006
|
+
},
|
|
2007
|
+
}
|
|
2008
|
+
: {}),
|
|
1890
2009
|
},
|
|
1891
2010
|
...(exhaustedProfileData !== undefined
|
|
1892
2011
|
? { agentProfile: exhaustedProfile, agentProfileHash: exhaustedProfileData.content_hash }
|
|
@@ -2428,6 +2547,17 @@ export async function executeStep(store, definition, options) {
|
|
|
2428
2547
|
resolvedGateMessage = raw;
|
|
2429
2548
|
}
|
|
2430
2549
|
const gateConfig = stepDef.gate;
|
|
2550
|
+
const openedAt = new Date();
|
|
2551
|
+
// issue #291 (mint-time freeze, [F2]): the gate's OWN enforce/notify clock fields, frozen
|
|
2552
|
+
// into the record HERE — never re-read from the definition by any later enactment/
|
|
2553
|
+
// notification/read-side surface (the definition-drift-livelock cure). `expires_at` derives
|
|
2554
|
+
// from THIS `openedAt` instant, never a second `new Date()` call. `reminder_max` defaults to
|
|
2555
|
+
// 3 at mint when `reminder_seconds` is declared and the author gave no explicit value — so
|
|
2556
|
+
// every later reader can treat the frozen field as authoritative without re-applying a
|
|
2557
|
+
// default itself.
|
|
2558
|
+
const expiresAt = gateConfig?.timeout_seconds !== undefined
|
|
2559
|
+
? new Date(openedAt.getTime() + gateConfig.timeout_seconds * 1000).toISOString()
|
|
2560
|
+
: undefined;
|
|
2431
2561
|
// The PendingGate object is built EXACTLY as before, regardless of which path commits it below
|
|
2432
2562
|
// (issue #279, increment 2, PR-D, Deliverable 1a — the migrated `open_gate` delta carries this
|
|
2433
2563
|
// SAME object verbatim; the legacy fallback writes it via `store.update` unchanged).
|
|
@@ -2436,13 +2566,30 @@ export async function executeStep(store, definition, options) {
|
|
|
2436
2566
|
step_name,
|
|
2437
2567
|
preview: output,
|
|
2438
2568
|
choices,
|
|
2439
|
-
opened_at:
|
|
2569
|
+
opened_at: openedAt.toISOString(),
|
|
2440
2570
|
...(gateConfig?.owner !== undefined ? { owner: gateConfig.owner } : {}),
|
|
2441
2571
|
...(resolvedGateMessage !== undefined ? { resolved_message: resolvedGateMessage } : {}),
|
|
2442
2572
|
...(gateConfig?.resolution_messages !== undefined
|
|
2443
2573
|
? { resolution_messages: gateConfig.resolution_messages }
|
|
2444
2574
|
: {}),
|
|
2575
|
+
// issue #291: the mint-frozen enforce clock.
|
|
2576
|
+
...(expiresAt !== undefined ? { expires_at: expiresAt } : {}),
|
|
2577
|
+
...(gateConfig?.on_expiry !== undefined ? { on_expiry: gateConfig.on_expiry } : {}),
|
|
2578
|
+
...(gateConfig?.default_choice !== undefined
|
|
2579
|
+
? { default_choice: gateConfig.default_choice }
|
|
2580
|
+
: {}),
|
|
2581
|
+
// issue #291: the mint-frozen notify clock (standalone-legal — independent of expiresAt).
|
|
2582
|
+
...(gateConfig?.reminder_seconds !== undefined
|
|
2583
|
+
? {
|
|
2584
|
+
reminder_seconds: gateConfig.reminder_seconds,
|
|
2585
|
+
reminder_max: gateConfig.reminder_max ?? 3,
|
|
2586
|
+
}
|
|
2587
|
+
: {}),
|
|
2445
2588
|
};
|
|
2589
|
+
// issue #291 ([F-A2-6]): computed ONCE for the whole gate-open envelope (migrated + legacy
|
|
2590
|
+
// both read it) — the absolute first-due notify-clock timestamp, when reminder_seconds was
|
|
2591
|
+
// declared.
|
|
2592
|
+
const gateOpenDueState = computeGateDueState(pendingGate, openedAt);
|
|
2446
2593
|
// gate.display fallback chain: gate.message resolved → step.prompt resolved → absent
|
|
2447
2594
|
const resolvedGateDisplay = resolvedGateMessage !== undefined
|
|
2448
2595
|
? resolvedGateMessage
|
|
@@ -2532,6 +2679,7 @@ export async function executeStep(store, definition, options) {
|
|
|
2532
2679
|
// discarded). Calm, confirm_required (the gate genuinely IS still open) — never
|
|
2533
2680
|
// report_to_user (no `agent_action` set, matching the fresh-open confirm_required shape).
|
|
2534
2681
|
const liveGate = result.gate;
|
|
2682
|
+
const liveGateDueState = computeGateDueState(liveGate, new Date());
|
|
2535
2683
|
return {
|
|
2536
2684
|
command: options.command,
|
|
2537
2685
|
run_id: options.runId,
|
|
@@ -2555,6 +2703,10 @@ export async function executeStep(store, definition, options) {
|
|
|
2555
2703
|
? { display: liveGate.resolved_message }
|
|
2556
2704
|
: {}),
|
|
2557
2705
|
response_spec: { choices: liveGate.choices },
|
|
2706
|
+
...(liveGate.expires_at !== undefined ? { expires_at: liveGate.expires_at } : {}),
|
|
2707
|
+
...(liveGateDueState.next_reminder_due_at !== undefined
|
|
2708
|
+
? { first_reminder_due_at: liveGateDueState.next_reminder_due_at }
|
|
2709
|
+
: {}),
|
|
2558
2710
|
},
|
|
2559
2711
|
};
|
|
2560
2712
|
}
|
|
@@ -2585,6 +2737,10 @@ export async function executeStep(store, definition, options) {
|
|
|
2585
2737
|
? { agent_hint: resolvedGateInstructions }
|
|
2586
2738
|
: {}),
|
|
2587
2739
|
response_spec: { choices },
|
|
2740
|
+
...(pendingGate.expires_at !== undefined ? { expires_at: pendingGate.expires_at } : {}),
|
|
2741
|
+
...(gateOpenDueState.next_reminder_due_at !== undefined
|
|
2742
|
+
? { first_reminder_due_at: gateOpenDueState.next_reminder_due_at }
|
|
2743
|
+
: {}),
|
|
2588
2744
|
},
|
|
2589
2745
|
};
|
|
2590
2746
|
}
|
|
@@ -2629,6 +2785,10 @@ export async function executeStep(store, definition, options) {
|
|
|
2629
2785
|
...(resolvedGateDisplay !== undefined ? { display: resolvedGateDisplay } : {}),
|
|
2630
2786
|
...(resolvedGateInstructions !== undefined ? { agent_hint: resolvedGateInstructions } : {}),
|
|
2631
2787
|
response_spec: { choices },
|
|
2788
|
+
...(pendingGate.expires_at !== undefined ? { expires_at: pendingGate.expires_at } : {}),
|
|
2789
|
+
...(gateOpenDueState.next_reminder_due_at !== undefined
|
|
2790
|
+
? { first_reminder_due_at: gateOpenDueState.next_reminder_due_at }
|
|
2791
|
+
: {}),
|
|
2632
2792
|
},
|
|
2633
2793
|
};
|
|
2634
2794
|
}
|
|
@@ -2946,6 +3106,112 @@ function buildGateResponseSnapshot(gate, choice, respondedAt, respondedBy) {
|
|
|
2946
3106
|
...(respondedBy !== undefined ? { responded_by: respondedBy } : {}),
|
|
2947
3107
|
};
|
|
2948
3108
|
}
|
|
3109
|
+
/** Renders a millisecond duration as a compact human-readable string ("3m", "2h 15m", "1d 4h") —
|
|
3110
|
+
* issue #291 [F8] overdue-delta disclosure. Local to this file (core has no CLI dependency);
|
|
3111
|
+
* mirrors the CLI's own `formatGateAge` shape but is independently maintained — no cross-package
|
|
3112
|
+
* import for a two-branch formatter. */
|
|
3113
|
+
function formatOverdueDuration(ms) {
|
|
3114
|
+
const totalMinutes = Math.floor(ms / 60_000);
|
|
3115
|
+
const totalHours = Math.floor(totalMinutes / 60);
|
|
3116
|
+
const totalDays = Math.floor(totalHours / 24);
|
|
3117
|
+
if (totalMinutes < 60)
|
|
3118
|
+
return `${totalMinutes}m`;
|
|
3119
|
+
if (totalHours < 24)
|
|
3120
|
+
return `${totalHours}h ${totalMinutes % 60}m`;
|
|
3121
|
+
return `${totalDays}d ${totalHours % 24}h`;
|
|
3122
|
+
}
|
|
3123
|
+
/**
|
|
3124
|
+
* issue #291 ([F3] shape c / [F8] / [F12]): composes the honest envelope for a late gate response
|
|
3125
|
+
* that lost the race to the enforce clock — called AFTER an `expire_gate` settleStep attempt,
|
|
3126
|
+
* regardless of whether THIS call enacted it (`applied: true`) or a racing enactment point already
|
|
3127
|
+
* had (`applied: false, reason: 'already_settled'` — F1's arms make both paths land on the SAME
|
|
3128
|
+
* committed disposition, read from `finalRun`). `finalRun` must be the state that actually reflects
|
|
3129
|
+
* the enactment (either `expireResult.run` directly). Drains post-commit finalizers when the
|
|
3130
|
+
* enactment itself transitioned the run (F7 — submit's existing transitioned-drain plumbing,
|
|
3131
|
+
* extended to the expire result); a store lacking `settleStep`'s companion `drainFinalizers`
|
|
3132
|
+
* capability is not a concern here — `drainFinalizers` works off any `RunStore`.
|
|
3133
|
+
*/
|
|
3134
|
+
async function composeExpiredGateEnvelope(store, definition, registry, originalGateId, originalChoice, overdueMs, expireResult) {
|
|
3135
|
+
let finalRun = expireResult.run;
|
|
3136
|
+
let drainWarnings = [];
|
|
3137
|
+
if (expireResult.applied && expireResult.transitioned) {
|
|
3138
|
+
try {
|
|
3139
|
+
const drainOutcome = await drainFinalizers(store, definition, registry, finalRun.id);
|
|
3140
|
+
finalRun = drainOutcome.run;
|
|
3141
|
+
drainWarnings = drainOutcome.warnings;
|
|
3142
|
+
}
|
|
3143
|
+
catch (err) {
|
|
3144
|
+
drainWarnings = [
|
|
3145
|
+
`post-commit finalizer drain failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
3146
|
+
];
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
const overdueLabel = formatOverdueDuration(Math.max(0, overdueMs));
|
|
3150
|
+
// settle_default disposition: a 'gate' settled entry bearing this gateId, resolved_by:'timeout'.
|
|
3151
|
+
const settledEntry = Object.entries(finalRun.settled ?? {}).find(([, e]) => e.outcome === 'gate' && e.token === originalGateId && e.resolved_by === 'timeout');
|
|
3152
|
+
if (settledEntry !== undefined) {
|
|
3153
|
+
const [stepName, entry] = settledEntry;
|
|
3154
|
+
const enactedDisclosure = `gate '${originalGateId}' expired ${overdueLabel} ago and was enacted (settle_default: '${entry.choice}') before this response arrived — enacted_via: submit.`;
|
|
3155
|
+
if (entry.choice === originalChoice) {
|
|
3156
|
+
// [F12]'s own pinned string — same choice, still honestly not "your" recorded response.
|
|
3157
|
+
return {
|
|
3158
|
+
command: stepName,
|
|
3159
|
+
run_id: finalRun.id,
|
|
3160
|
+
run_version: finalRun.version,
|
|
3161
|
+
status: 'ok',
|
|
3162
|
+
data: {},
|
|
3163
|
+
evidence: [],
|
|
3164
|
+
warnings: mergeWarnings([], enactedDisclosure, ...drainWarnings),
|
|
3165
|
+
errors: [],
|
|
3166
|
+
context_hint: 'the outcome matches your choice, but it was settled by timeout; your response was not recorded.',
|
|
3167
|
+
run_phase: finalRun.run_phase,
|
|
3168
|
+
next_actions: finalRun.terminal_state ? [] : buildNextActions(definition, finalRun),
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
const err = new WorkflowError(`Gate '${originalGateId}' was settled by timeout with choice '${entry.choice}' — your choice '${originalChoice}' was not recorded.`, {
|
|
3172
|
+
code: 'STATE_BLOCKED',
|
|
3173
|
+
category: 'STATE',
|
|
3174
|
+
agentAction: 'report_to_user',
|
|
3175
|
+
retryable: false,
|
|
3176
|
+
details: {
|
|
3177
|
+
runId: finalRun.id,
|
|
3178
|
+
gateId: originalGateId,
|
|
3179
|
+
winning_choice: entry.choice,
|
|
3180
|
+
resolved_by: 'timeout',
|
|
3181
|
+
},
|
|
3182
|
+
});
|
|
3183
|
+
const envelope = errorEnvelope(stepName, finalRun.id, finalRun.version, err, err.message, finalRun.run_phase);
|
|
3184
|
+
return { ...envelope, warnings: mergeWarnings([], enactedDisclosure, ...drainWarnings) };
|
|
3185
|
+
}
|
|
3186
|
+
// abort disposition: a skip_details entry kind 'gate_expired' bearing this gateId.
|
|
3187
|
+
const abortEntry = Object.entries(finalRun.skip_details ?? {}).find(([, d]) => d.kind === 'gate_expired' && d.gate_id === originalGateId);
|
|
3188
|
+
if (abortEntry !== undefined) {
|
|
3189
|
+
const [stepName] = abortEntry;
|
|
3190
|
+
const enactedDisclosure = `gate '${originalGateId}' expired ${overdueLabel} ago and was enacted (abort) before this response arrived — enacted_via: submit.`;
|
|
3191
|
+
const err = new WorkflowError(`Gate '${originalGateId}' on '${stepName}' expired and the run aborted per the ` +
|
|
3192
|
+
`workflow's declared on_expiry — your choice was NOT recorded.`, {
|
|
3193
|
+
code: 'STATE_RUN_TERMINAL',
|
|
3194
|
+
category: 'STATE',
|
|
3195
|
+
agentAction: 'report_to_user',
|
|
3196
|
+
retryable: false,
|
|
3197
|
+
details: { runId: finalRun.id, gateId: originalGateId, step_name: stepName },
|
|
3198
|
+
});
|
|
3199
|
+
const envelope = errorEnvelope(stepName, finalRun.id, finalRun.version, err, err.message, finalRun.run_phase);
|
|
3200
|
+
return { ...envelope, warnings: mergeWarnings([], enactedDisclosure, ...drainWarnings) };
|
|
3201
|
+
}
|
|
3202
|
+
// Unreachable in-contract (the expire delta either enacted it here or a racing enactment point
|
|
3203
|
+
// already had — one of the two branches above always matches). Defensive fallback: never throw
|
|
3204
|
+
// out of a response-envelope-returning function; report the ambiguity honestly instead.
|
|
3205
|
+
const err = new WorkflowError(`Gate '${originalGateId}' expired, but its enacted disposition could not be determined from ` +
|
|
3206
|
+
`the resulting record — this indicates a store or engine defect, not a normal refusal.`, {
|
|
3207
|
+
code: 'ENGINE_INTERNAL',
|
|
3208
|
+
category: 'ENGINE',
|
|
3209
|
+
agentAction: 'stop',
|
|
3210
|
+
retryable: false,
|
|
3211
|
+
details: { runId: finalRun.id, gateId: originalGateId },
|
|
3212
|
+
});
|
|
3213
|
+
return errorEnvelope('submit_gate', finalRun.id, finalRun.version, err, err.message, finalRun.run_phase);
|
|
3214
|
+
}
|
|
2949
3215
|
export async function submitHumanResponse(store, definition, options) {
|
|
2950
3216
|
// 1. Load run.
|
|
2951
3217
|
let run;
|
|
@@ -2963,13 +3229,16 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
2963
3229
|
});
|
|
2964
3230
|
return errorEnvelope('submit_gate', options.runId, 0, e);
|
|
2965
3231
|
}
|
|
3232
|
+
// issue #291: injectable clock, hoisted here so BOTH the migrated and legacy paths below use
|
|
3233
|
+
// the SAME instant for their expiry checks.
|
|
3234
|
+
const now = options.now ?? new Date();
|
|
2966
3235
|
// issue #279 (increment 2, PR-D, Deliverable 1b): the migrated path — the four legacy
|
|
2967
3236
|
// verify-arms below (1a-4) become settle_gate's OWN predicate arms; this branch skips them
|
|
2968
3237
|
// entirely and settles atomically against FRESH state via the store's own settleStep. Dormancy:
|
|
2969
3238
|
// an undeclaring store falls through to the byte-identical legacy path below (I16/#169
|
|
2970
3239
|
// fail-closed dormancy).
|
|
2971
3240
|
if (store.settleStep !== undefined) {
|
|
2972
|
-
const respondedAt =
|
|
3241
|
+
const respondedAt = now;
|
|
2973
3242
|
// Evidence rule (design record §6 lens-3 S2): built from the PRE-READ `pending_gate` IFF it
|
|
2974
3243
|
// matches options.gateId — else `[]` (the arm can never APPLY against a non-matching fresh
|
|
2975
3244
|
// read either, so an empty evidence array is inert there; a matching pre-read is guaranteed
|
|
@@ -2989,7 +3258,9 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
2989
3258
|
};
|
|
2990
3259
|
let result;
|
|
2991
3260
|
try {
|
|
2992
|
-
|
|
3261
|
+
// issue #291 ([F3]): the injectable `now` reaches applySettleGate's write-free
|
|
3262
|
+
// gate_expired_pending arm through this SAME options.now plumbing.
|
|
3263
|
+
result = await store.settleStep(options.runId, delta, definition, { now });
|
|
2993
3264
|
}
|
|
2994
3265
|
catch (err) {
|
|
2995
3266
|
// Thrown infra errors keep the SAME shape as the legacy path's own final-persist catch,
|
|
@@ -3006,6 +3277,34 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
3006
3277
|
}
|
|
3007
3278
|
if (!result.applied) {
|
|
3008
3279
|
switch (result.reason) {
|
|
3280
|
+
case 'gate_expired_pending': {
|
|
3281
|
+
// issue #291 ([F3] shape c): the live gate has already expired unresolved — issue the
|
|
3282
|
+
// caller-composed expire_gate settleStep ([F1]'s arms make this idempotent even under a
|
|
3283
|
+
// race with another enactment point) and compose the honest late-response envelope from
|
|
3284
|
+
// whatever the enactment result actually committed.
|
|
3285
|
+
const overdueMs = run.pending_gate?.expires_at !== undefined
|
|
3286
|
+
? now.getTime() - new Date(run.pending_gate.expires_at).getTime()
|
|
3287
|
+
: 0;
|
|
3288
|
+
const expireDelta = { kind: 'expire_gate', gateId: options.gateId };
|
|
3289
|
+
let expireResult;
|
|
3290
|
+
try {
|
|
3291
|
+
expireResult = await store.settleStep(options.runId, expireDelta, definition, {
|
|
3292
|
+
now,
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
3295
|
+
catch (err) {
|
|
3296
|
+
const e = err instanceof WorkflowError
|
|
3297
|
+
? err
|
|
3298
|
+
: new WorkflowError("Failed to enact the gate's expiry", {
|
|
3299
|
+
code: 'ENGINE_STORE_FAILED',
|
|
3300
|
+
category: 'ENGINE',
|
|
3301
|
+
agentAction: 'stop',
|
|
3302
|
+
retryable: false,
|
|
3303
|
+
});
|
|
3304
|
+
return errorEnvelope(run.pending_gate?.step_name ?? 'submit_gate', options.runId, result.run.version, e, "Failed to enact the gate's expiry.", result.run.run_phase);
|
|
3305
|
+
}
|
|
3306
|
+
return composeExpiredGateEnvelope(store, definition, options.registry, options.gateId, options.choice, overdueMs, expireResult);
|
|
3307
|
+
}
|
|
3009
3308
|
case 'already_settled': {
|
|
3010
3309
|
// Calm ok envelope stating the resolution already committed (same choice) — never
|
|
3011
3310
|
// report_to_user. Drain: on already_settled ∧ pending ledger entries non-empty — hand-
|
|
@@ -3213,6 +3512,44 @@ export async function submitHumanResponse(store, definition, options) {
|
|
|
3213
3512
|
retryable: false,
|
|
3214
3513
|
}), `Gate ID mismatch on run '${options.runId}'.`);
|
|
3215
3514
|
}
|
|
3515
|
+
// 3.5. issue #291 ([F4] legacy-store expiry — the ONE enactment point F4 explicitly gives a
|
|
3516
|
+
// legacy-CAS fallback, since it already owns one): the gate has expired AND has an enactable
|
|
3517
|
+
// disposition (on_expiry frozen — a finding-only gate, expires_at with no on_expiry, is
|
|
3518
|
+
// excluded exactly like applySettleGate's own F3 gating, so a finding-only gate's human
|
|
3519
|
+
// response resolves normally below, however overdue). Enacted via the SAME pure
|
|
3520
|
+
// `applySettlement` transform this store's declaring siblings use through `settleStep` — this
|
|
3521
|
+
// store has no `settleStep` of its own, so the result is persisted through the version-CAS
|
|
3522
|
+
// `store.update()` this legacy path already owns. A CAS-mismatch (a genuine race) surfaces as
|
|
3523
|
+
// an honest error, matching this path's existing no-retry risk profile everywhere else.
|
|
3524
|
+
if (run.pending_gate.expires_at !== undefined &&
|
|
3525
|
+
run.pending_gate.on_expiry !== undefined &&
|
|
3526
|
+
now.getTime() >= new Date(run.pending_gate.expires_at).getTime()) {
|
|
3527
|
+
const overdueMs = now.getTime() - new Date(run.pending_gate.expires_at).getTime();
|
|
3528
|
+
const expireOutcome = applySettlement(run, { kind: 'expire_gate', gateId: options.gateId }, definition, { now });
|
|
3529
|
+
if (!expireOutcome.applied) {
|
|
3530
|
+
// In-contract for this snapshot-based path: the local `run` might already reflect a prior
|
|
3531
|
+
// enactment (e.g. a same-process retry) — already_settled composes the honest envelope the
|
|
3532
|
+
// same way the migrated path's race leg does, reading disposition off `expireOutcome.run`
|
|
3533
|
+
// (the very snapshot the refusal matched against).
|
|
3534
|
+
return composeExpiredGateEnvelope(store, definition, options.registry, options.gateId, options.choice, overdueMs, expireOutcome);
|
|
3535
|
+
}
|
|
3536
|
+
let persistedExpiry;
|
|
3537
|
+
try {
|
|
3538
|
+
persistedExpiry = await store.update(expireOutcome.run);
|
|
3539
|
+
}
|
|
3540
|
+
catch (err) {
|
|
3541
|
+
const e = err instanceof WorkflowError
|
|
3542
|
+
? err
|
|
3543
|
+
: new WorkflowError("Failed to persist the gate's expiry", {
|
|
3544
|
+
code: 'ENGINE_STORE_FAILED',
|
|
3545
|
+
category: 'ENGINE',
|
|
3546
|
+
agentAction: 'stop',
|
|
3547
|
+
retryable: false,
|
|
3548
|
+
});
|
|
3549
|
+
return errorEnvelope(run.pending_gate.step_name, options.runId, run.version, e, "Failed to enact the gate's expiry.", run.run_phase);
|
|
3550
|
+
}
|
|
3551
|
+
return composeExpiredGateEnvelope(store, definition, options.registry, options.gateId, options.choice, overdueMs, { ...expireOutcome, run: persistedExpiry });
|
|
3552
|
+
}
|
|
3216
3553
|
// 4. Validate choice.
|
|
3217
3554
|
if (!run.pending_gate.choices.includes(options.choice)) {
|
|
3218
3555
|
const expected = run.pending_gate.choices.join(', ');
|
|
@@ -3475,8 +3812,10 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
3475
3812
|
// settlement.ts's selectFinalizers, shared with `mintFresh` — this call passes the SAME inputs
|
|
3476
3813
|
// the inline loop used to compute over, so the returned name list is byte-identical to what
|
|
3477
3814
|
// `[...groupA, ...groupB]` produced before the extraction.
|
|
3815
|
+
// issue #302 (chokepoint 2 of 2): derive the full effective trigger set from sealDraft — the
|
|
3816
|
+
// legacy seal path for a non-declaring external store (the dormancy fallback).
|
|
3478
3817
|
const settled = new Set([...sealDraft.completed_steps, ...sealDraft.failed_steps]);
|
|
3479
|
-
const selected = selectFinalizers(definition, settled, outcome);
|
|
3818
|
+
const selected = selectFinalizers(definition, settled, deriveEffectiveTriggers(outcome, sealDraft));
|
|
3480
3819
|
let record = sealDraft;
|
|
3481
3820
|
for (const name of selected) {
|
|
3482
3821
|
const step = definition.steps[name];
|