@sensigo/realm 0.25.0 → 0.27.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/eligibility.d.ts +13 -0
- package/dist/engine/eligibility.d.ts.map +1 -1
- package/dist/engine/eligibility.js +20 -8
- package/dist/engine/eligibility.js.map +1 -1
- package/dist/engine/execution-loop.d.ts +13 -0
- package/dist/engine/execution-loop.d.ts.map +1 -1
- package/dist/engine/execution-loop.js +376 -45
- package/dist/engine/execution-loop.js.map +1 -1
- package/dist/engine/reclaim-step.d.ts.map +1 -1
- package/dist/engine/reclaim-step.js +198 -4
- package/dist/engine/reclaim-step.js.map +1 -1
- package/dist/engine/trace-adoption.d.ts +53 -0
- package/dist/engine/trace-adoption.d.ts.map +1 -0
- package/dist/engine/trace-adoption.js +49 -0
- package/dist/engine/trace-adoption.js.map +1 -0
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -4
- package/dist/index.js.map +1 -1
- package/dist/store/fs-io.d.ts +26 -0
- package/dist/store/fs-io.d.ts.map +1 -1
- package/dist/store/fs-io.js +37 -2
- package/dist/store/fs-io.js.map +1 -1
- package/dist/store/json-file-store.d.ts +7 -1
- package/dist/store/json-file-store.d.ts.map +1 -1
- package/dist/store/json-file-store.js +12 -5
- package/dist/store/json-file-store.js.map +1 -1
- package/dist/store/store-fidelity.d.ts +9 -0
- package/dist/store/store-fidelity.d.ts.map +1 -0
- package/dist/store/store-fidelity.js +10 -0
- package/dist/store/store-fidelity.js.map +1 -0
- package/dist/store/store-interface.d.ts +43 -0
- package/dist/store/store-interface.d.ts.map +1 -1
- package/dist/store/trace-buffer-store.d.ts +419 -7
- package/dist/store/trace-buffer-store.d.ts.map +1 -1
- package/dist/store/trace-buffer-store.js +401 -30
- package/dist/store/trace-buffer-store.js.map +1 -1
- package/dist/types/response-envelope.d.ts +20 -0
- package/dist/types/response-envelope.d.ts.map +1 -1
- package/dist/types/run-record.d.ts +33 -10
- package/dist/types/run-record.d.ts.map +1 -1
- package/dist/types/workflow-error.d.ts +1 -1
- package/dist/types/workflow-error.d.ts.map +1 -1
- package/dist/types/workflow-error.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { extensionIdentityDiffers } from '../types/extension-identity.js';
|
|
2
2
|
import { WorkflowError } from '../types/workflow-error.js';
|
|
3
|
+
import { persistsField } from '../store/store-fidelity.js';
|
|
4
|
+
import { storeDeclaresSeal, storeDeclaresNonceCarriage } from '../store/trace-buffer-store.js';
|
|
5
|
+
import { partitionBufferedEntries } from './trace-adoption.js';
|
|
3
6
|
import { captureEvidence } from '../evidence/snapshot.js';
|
|
4
7
|
import { validateInputSchema, validateOutputSchema, validateTraceSchema, } from '../validation/input-schema.js';
|
|
5
8
|
import { normalizeTrace } from './trace-normalizer.js';
|
|
@@ -347,13 +350,75 @@ export function buildNextActions(definition, run) {
|
|
|
347
350
|
.map((name) => stepToNextAction(name, definition.steps[name], context));
|
|
348
351
|
}
|
|
349
352
|
/**
|
|
350
|
-
* Merges call-scoped trace-schema warnings with
|
|
351
|
-
*
|
|
353
|
+
* Merges call-scoped trace-schema warnings with any number of optional extra warnings into a
|
|
354
|
+
* single warnings array. Trace warnings are listed first (deterministic order); `extraWarnings`
|
|
355
|
+
* entries are appended in call order, `undefined` entries skipped. Variadic since issue #207
|
|
356
|
+
* PR-2 (the success-settle path now has TWO independent optional warnings to merge — a
|
|
357
|
+
* handler-level warning and a WAL-cleanup warning — where every earlier call site had at most
|
|
358
|
+
* one).
|
|
352
359
|
*/
|
|
353
|
-
function mergeWarnings(traceWarnings,
|
|
354
|
-
|
|
360
|
+
function mergeWarnings(traceWarnings, ...extraWarnings) {
|
|
361
|
+
const defined = extraWarnings.filter((w) => w !== undefined);
|
|
362
|
+
if (traceWarnings.length === 0 && defined.length === 0)
|
|
355
363
|
return [];
|
|
356
|
-
return
|
|
364
|
+
return [...traceWarnings, ...defined];
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Compensating un-claim (issue #207 PR-2, D3 §5): built from `pendingRun` — the record OUR OWN
|
|
368
|
+
* `claimStep` call returned, never a fresh get — removing the step from `in_progress_steps` AND
|
|
369
|
+
* `claims[step]` in the SAME mutation (the settle-site invariant every other settle path in this
|
|
370
|
+
* file upholds), plus an audit-evidence entry. The caller CAS's this against `pendingRun.version`
|
|
371
|
+
* (by passing the returned record straight to `store.update`): any intervening write (a
|
|
372
|
+
* concurrent settle, reclaim, or second claim) bumps version, so this compensating un-claim can
|
|
373
|
+
* never stomp it — a CAS mismatch means some other actor already resolved the claim, and the
|
|
374
|
+
* caller must stop immediately rather than retry, leaving the claim exactly as that actor left
|
|
375
|
+
* it. A step already absent from `in_progress_steps` (should not happen here, but the filter is
|
|
376
|
+
* naturally idempotent) is simply a no-op mutation, not a special case.
|
|
377
|
+
*/
|
|
378
|
+
function buildCompensatingUnclaim(pendingRun, stepName, now) {
|
|
379
|
+
const auditEvidence = captureEvidence({
|
|
380
|
+
stepId: stepName,
|
|
381
|
+
startedAt: now,
|
|
382
|
+
completedAt: now,
|
|
383
|
+
input: {},
|
|
384
|
+
output: {
|
|
385
|
+
compensating_unclaim: true,
|
|
386
|
+
reason: 'adoption-read failure after claim',
|
|
387
|
+
unclaimed_at: now.toISOString(),
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
return {
|
|
391
|
+
...pendingRun,
|
|
392
|
+
in_progress_steps: pendingRun.in_progress_steps.filter((s) => s !== stepName),
|
|
393
|
+
claims: omitClaim(pendingRun.claims, stepName),
|
|
394
|
+
evidence: [...pendingRun.evidence, auditEvidence],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Guard for the settle-time seal attempt (issue #197 PR-2, deliverable 1f) — ONE lock-free
|
|
399
|
+
* `store.get` re-verifying the run exists and this step has actually LEFT `in_progress_steps`
|
|
400
|
+
* (i.e. our own settling `store.update` already landed) — the "purge-guard shape" (mirrors
|
|
401
|
+
* #184's terminal-re-verify-under-lock precedent). In the normal case this always passes: by the
|
|
402
|
+
* time either settle site calls `sealFenced`, the settling update has already committed
|
|
403
|
+
* synchronously just above it. A run genuinely gone (e.g. concurrently purged) surfaces as
|
|
404
|
+
* `store.get`'s own typed `STATE_RUN_NOT_FOUND` throw — deliberately NOT special-cased here; it
|
|
405
|
+
* propagates as an ordinary guard THROW, which the caller's uniform "a throw ⇒ warn + skip the
|
|
406
|
+
* delete" handling already covers correctly (residue-not-loss either way).
|
|
407
|
+
*/
|
|
408
|
+
function buildSettleSealGuard(store, runId, stepName) {
|
|
409
|
+
return async () => {
|
|
410
|
+
const fresh = await store.get(runId);
|
|
411
|
+
if (fresh.in_progress_steps.includes(stepName)) {
|
|
412
|
+
throw new WorkflowError(`Refusing to seal trace buffer for run '${runId}' step '${stepName}': the step is still ` +
|
|
413
|
+
'in_progress (the settling update has not yet landed) — residue-not-loss, the live WAL ' +
|
|
414
|
+
'is left intact.', {
|
|
415
|
+
code: 'STATE_STEP_PENDING',
|
|
416
|
+
category: 'STATE',
|
|
417
|
+
agentAction: 'report_to_user',
|
|
418
|
+
retryable: true,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
};
|
|
357
422
|
}
|
|
358
423
|
/**
|
|
359
424
|
* Issue #185 Fix 1 (budget-priority): builds the merged, canonicalized trace for an agent step,
|
|
@@ -616,17 +681,57 @@ export async function executeStep(store, definition, options) {
|
|
|
616
681
|
let preNormalizedTrace;
|
|
617
682
|
let walEntries = [];
|
|
618
683
|
let preClaimSchemaResult;
|
|
684
|
+
// issue #197 PR-2 (design §3, the activation gate): computed once — options.traceBufferStore
|
|
685
|
+
// is invariant for this whole call. `carriageActive` also gates whether the NEW
|
|
686
|
+
// adopted_own/adopted_anonymous/preserved_foreign fields are surfaced on the 'ok' envelope
|
|
687
|
+
// (absent entirely on a bare-floor store, never just zeroed) — see the settle-site comment.
|
|
688
|
+
const carriageActive = options.traceBufferStore !== undefined && storeDeclaresNonceCarriage(options.traceBufferStore);
|
|
689
|
+
const effectiveClaimantNonce = carriageActive ? options.writerNonce : undefined;
|
|
690
|
+
// Post-claim adoption partition (issue #197 PR-2) — lifted to this outer scope because it is
|
|
691
|
+
// read again at the settle sites (seal-vs-delete decision) and at 'ok' envelope build, both far
|
|
692
|
+
// below the post-claim block that computes it. `undefined` for a non-agent step (never computed
|
|
693
|
+
// there) — the settle sites and envelope build both treat that as "nothing to preserve/report".
|
|
694
|
+
let adoptionPartition;
|
|
619
695
|
if (stepDef?.execution === 'agent') {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
696
|
+
// issue #207 PR-2 (D3 §5): wrapped — a rejection here (e.g. lock contention under a fenced
|
|
697
|
+
// trio's serialized reads) happens BEFORE claimStep, so no claim exists to compensate for;
|
|
698
|
+
// return a typed, retryable envelope instead of letting the read throw uncaught.
|
|
699
|
+
try {
|
|
700
|
+
walEntries =
|
|
701
|
+
options.traceBufferStore !== undefined
|
|
702
|
+
? await options.traceBufferStore.read(options.runId, options.command)
|
|
703
|
+
: [];
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
return makeErrorEnvelope(options, run, new WorkflowError('Failed to read trace buffer before claiming step', {
|
|
707
|
+
code: 'ENGINE_STORE_FAILED',
|
|
708
|
+
category: 'ENGINE',
|
|
709
|
+
agentAction: 'stop',
|
|
710
|
+
retryable: true,
|
|
711
|
+
details: {
|
|
712
|
+
step_id: options.command,
|
|
713
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
714
|
+
},
|
|
715
|
+
}), definition);
|
|
716
|
+
}
|
|
717
|
+
// issue #197 PR-2 (design §3, missing-leg advisory): a caller-supplied nonce this store
|
|
718
|
+
// cannot carry is IGNORED for adoption purposes (carriageActive is false) — loudly, once per
|
|
719
|
+
// call, so a minting client discovers the silent floor rather than assuming attribution.
|
|
720
|
+
if (options.writerNonce !== undefined && !carriageActive) {
|
|
721
|
+
traceWarnings.push('writer_nonce ignored: the trace-buffer store does not declare writer_nonce_carriage — ' +
|
|
722
|
+
'adoption falls back to the honest floor');
|
|
723
|
+
}
|
|
624
724
|
const hasAnyTrace = walEntries.length > 0 || (options.trace !== undefined && options.trace.length > 0);
|
|
625
725
|
if (hasAnyTrace) {
|
|
726
|
+
// issue #197 PR-2 (design §2, ADOPTION_CONGRUENCE): the pre-claim enforce-gate validates
|
|
727
|
+
// ONLY the adopted subset — a foreign-nonce-only WAL must never gate a (nonced) claimant.
|
|
728
|
+
// Congruence with the post-claim partition below is mandatory: both call the SAME
|
|
729
|
+
// `partitionBufferedEntries` helper against the SAME `effectiveClaimantNonce`.
|
|
730
|
+
const prClaimPartition = partitionBufferedEntries(walEntries, effectiveClaimantNonce);
|
|
626
731
|
// issue #185 Fix 1: budget-priority merge (see buildPriorityMergedTrace's own doc) — this
|
|
627
732
|
// pre-claim pass exists only to feed validateTraceSchema below; its RESULT is discarded
|
|
628
733
|
// (not stored into the outer preNormalizedTrace) once the enforce-gate decision is made.
|
|
629
|
-
const preClaimNormalized = buildPriorityMergedTrace(
|
|
734
|
+
const preClaimNormalized = buildPriorityMergedTrace(prClaimPartition.adopted, options.trace);
|
|
630
735
|
// Validate trace schema if configured (unchanged call site).
|
|
631
736
|
if (stepDef.trace_schema !== undefined) {
|
|
632
737
|
const mode = stepDef.trace_validation_mode ?? 'warn';
|
|
@@ -708,14 +813,51 @@ export async function executeStep(store, definition, options) {
|
|
|
708
813
|
// complete, post-claim set, which is also what the captureEvidence call site further below
|
|
709
814
|
// keys its `stepDef?.execution === 'agent' && walEntries.length > 0` check on.
|
|
710
815
|
if (stepDef?.execution === 'agent') {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
816
|
+
// issue #207 PR-2 (D3 §5): wrapped — a rejection here happens AFTER claimStep, so a claim IS
|
|
817
|
+
// outstanding. COMPENSATING UN-CLAIM: built from `pendingRun` (our own claimStep result,
|
|
818
|
+
// never a fresh get) and CAS'd against `pendingRun.version` — an intervening write (someone
|
|
819
|
+
// else already resolved this claim) makes the CAS fail with STATE_SNAPSHOT_MISMATCH, in
|
|
820
|
+
// which case we stop immediately and leave the claim exactly as it is. Either way (compensated
|
|
821
|
+
// or left in place), the caller always gets the same typed retryable envelope — see
|
|
822
|
+
// buildCompensatingUnclaim's own doc for the full contract.
|
|
823
|
+
try {
|
|
824
|
+
walEntries =
|
|
825
|
+
options.traceBufferStore !== undefined
|
|
826
|
+
? await options.traceBufferStore.read(options.runId, options.command)
|
|
827
|
+
: [];
|
|
828
|
+
}
|
|
829
|
+
catch (err) {
|
|
830
|
+
try {
|
|
831
|
+
await store.update(buildCompensatingUnclaim(pendingRun, options.command, new Date()));
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
// CAS mismatch (someone else already resolved the claim) or any other failure to even
|
|
835
|
+
// un-claim: stop immediately, leave the claim exactly as it is — never retry here.
|
|
836
|
+
}
|
|
837
|
+
return makeErrorEnvelope(options, pendingRun, new WorkflowError('Failed to read trace buffer after claiming step', {
|
|
838
|
+
code: 'ENGINE_STORE_FAILED',
|
|
839
|
+
category: 'ENGINE',
|
|
840
|
+
agentAction: 'stop',
|
|
841
|
+
retryable: true,
|
|
842
|
+
details: {
|
|
843
|
+
step_id: options.command,
|
|
844
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
845
|
+
},
|
|
846
|
+
}), definition, traceWarnings.length > 0 ? traceWarnings : undefined);
|
|
847
|
+
}
|
|
848
|
+
// issue #197 PR-2 (design §2): the SAME predicate as the pre-claim pass, now over the
|
|
849
|
+
// complete post-claim set. Lifted to `adoptionPartition` (outer scope) — read again at the
|
|
850
|
+
// settle sites (seal-vs-delete) and at 'ok' envelope build, both below this block.
|
|
851
|
+
adoptionPartition = partitionBufferedEntries(walEntries, effectiveClaimantNonce);
|
|
852
|
+
// issue #197 PR-2 (design §2, the "keying pin"): this condition stays keyed on the FULL
|
|
853
|
+
// post-claim WAL set, NOT the adopted subset — a foreign-only WAL (adoptionPartition.adopted
|
|
854
|
+
// empty) with no options.trace must still enter this block so foreign_lines_preserved below
|
|
855
|
+
// is captured into trace_summary; otherwise that count is silently lost.
|
|
715
856
|
if (walEntries.length > 0 || (options.trace !== undefined && options.trace.length > 0)) {
|
|
716
857
|
// issue #185 Fix 1: same budget-priority merge as the pre-claim pass, now over the
|
|
717
|
-
// complete post-claim
|
|
718
|
-
|
|
858
|
+
// complete post-claim ADOPTED subset only — a foreign line never reaches canonical
|
|
859
|
+
// evidence (issue #197 PR-2, design §2).
|
|
860
|
+
preNormalizedTrace = buildPriorityMergedTrace(adoptionPartition.adopted, options.trace);
|
|
719
861
|
// Carry over the enforce-gate's schema-validation result (computed pre-claim against a
|
|
720
862
|
// possibly-incomplete set) rather than re-validating here — Fix 2 deliberately keeps
|
|
721
863
|
// validation pre-claim (see that block's comment); this just republishes its verdict onto
|
|
@@ -725,13 +867,48 @@ export async function executeStep(store, definition, options) {
|
|
|
725
867
|
preNormalizedTrace.summary.validation_mode = preClaimSchemaResult.validation_mode;
|
|
726
868
|
preNormalizedTrace.summary.validation_errors = preClaimSchemaResult.validation_errors;
|
|
727
869
|
}
|
|
728
|
-
// issue #185 Fix 3
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
870
|
+
// issue #185 Fix 3 / issue #197 PR-2 (design §2/§6): the three-way honest split.
|
|
871
|
+
// buffered_lines_adopted now counts ONLY the adopted-ANONYMOUS entries (bare-adopted by a
|
|
872
|
+
// ⊥ claimant) — for all-bare traffic this is numerically IDENTICAL to before #197 (every
|
|
873
|
+
// adopted line was, and still is, bare). attributed_lines_adopted counts own-nonce
|
|
874
|
+
// adoptions — NO caveat (design §6 wording, verbatim below). foreign_lines_preserved counts
|
|
875
|
+
// lines from a different writer, preserved (sealed where supported) but never adopted —
|
|
876
|
+
// its accompanying pointer warning is the only way an agent learns to retrieve them.
|
|
877
|
+
if (adoptionPartition.adopted_anonymous > 0) {
|
|
878
|
+
preNormalizedTrace.summary.buffered_lines_adopted = adoptionPartition.adopted_anonymous;
|
|
879
|
+
}
|
|
880
|
+
if (adoptionPartition.adopted_own > 0) {
|
|
881
|
+
preNormalizedTrace.summary.attributed_lines_adopted = adoptionPartition.adopted_own;
|
|
882
|
+
}
|
|
883
|
+
if (adoptionPartition.preserved_foreign > 0) {
|
|
884
|
+
preNormalizedTrace.summary.foreign_lines_preserved = adoptionPartition.preserved_foreign;
|
|
885
|
+
traceWarnings.push(`${adoptionPartition.preserved_foreign} buffered line(s) from a different writer were ` +
|
|
886
|
+
'preserved, not adopted — retrieve via `realm run export`');
|
|
887
|
+
}
|
|
888
|
+
// issue #197 PR-2 (design §6, the half-minted advisory): signature heuristics over the RAW
|
|
889
|
+
// walEntries/foreign set (never the adopted subset — these two cases are both about
|
|
890
|
+
// content this claimant did NOT adopt), neutral phrasing, values never echoed. Mutually
|
|
891
|
+
// exclusive by claimant type (nonced vs ⊥), so an if/else-if is exact, not a simplification.
|
|
892
|
+
if (effectiveClaimantNonce !== undefined &&
|
|
893
|
+
walEntries.length > 0 &&
|
|
894
|
+
walEntries.every((e) => e._nonce === undefined)) {
|
|
895
|
+
// A nonced claimant found nothing but bare lines — if those are this SAME attempt's own
|
|
896
|
+
// earlier append() calls, the client minted inconsistently (nonce on execute_step but not
|
|
897
|
+
// on the preceding append_trace calls, or vice versa).
|
|
898
|
+
traceWarnings.push(`the ${walEntries.length} buffered line(s) were bare and were preserved, not adopted — ` +
|
|
899
|
+
'if they are yours from this same attempt, you minted inconsistently (mint on both ' +
|
|
900
|
+
'calls, or neither)');
|
|
901
|
+
}
|
|
902
|
+
else if (effectiveClaimantNonce === undefined && adoptionPartition.foreign.length > 0) {
|
|
903
|
+
const distinctForeignNonces = new Set(adoptionPartition.foreign.map((e) => e._nonce));
|
|
904
|
+
if (distinctForeignNonces.size === 1) {
|
|
905
|
+
// A bare claimant found every foreign line under exactly ONE other nonce — if that
|
|
906
|
+
// nonce is this SAME attempt's own (minted on append_trace but the execute_step call
|
|
907
|
+
// stayed bare), the client minted inconsistently.
|
|
908
|
+
traceWarnings.push(`${adoptionPartition.foreign.length} line(s) under a different writer_nonce were ` +
|
|
909
|
+
'preserved, not adopted — if these are yours from this same attempt, you minted ' +
|
|
910
|
+
'inconsistently');
|
|
911
|
+
}
|
|
735
912
|
}
|
|
736
913
|
}
|
|
737
914
|
}
|
|
@@ -744,6 +921,15 @@ export async function executeStep(store, definition, options) {
|
|
|
744
921
|
...pendingRun,
|
|
745
922
|
workflow_context_snapshots: contextSnapshots,
|
|
746
923
|
});
|
|
924
|
+
// issue #188 field-fidelity gate: advisory only, never blocks — the re-snapshot above
|
|
925
|
+
// already self-healed CURRENT context regardless of what the store persists. This warns
|
|
926
|
+
// that the HISTORY of snapshots won't survive on a store that doesn't declare the field
|
|
927
|
+
// (every future execution will re-enter this branch and re-snapshot from scratch).
|
|
928
|
+
if (!persistsField(store, 'workflow_context_snapshots')) {
|
|
929
|
+
traceWarnings.push("this run store does not persist 'workflow_context_snapshots' — snapshot history is " +
|
|
930
|
+
'not durable on this store (re-snapshotting recovers current context each time, but ' +
|
|
931
|
+
'prior snapshots are lost)');
|
|
932
|
+
}
|
|
747
933
|
}
|
|
748
934
|
// Extension-code drift evidence (issue #119): lazy append-on-change, mirroring the
|
|
749
935
|
// workflow-context lazy write above. When the caller's registry carries a CLI-computed
|
|
@@ -754,6 +940,17 @@ export async function executeStep(store, definition, options) {
|
|
|
754
940
|
// identity (extension-free runs) → byte-identical behavior, field never written.
|
|
755
941
|
const registryIdentity = options.registry?.identity;
|
|
756
942
|
if (registryIdentity !== undefined) {
|
|
943
|
+
// issue #188 field-fidelity gate: a SEPARATE, unconditional advisory (independent of
|
|
944
|
+
// whether THIS call's append-on-change detects an actual diff below) — if the store can't
|
|
945
|
+
// persist extension_identity at all, the baseline resets every execution, so drift can
|
|
946
|
+
// NEVER accumulate or be detected on this store, not just "not detected this time". Preserves
|
|
947
|
+
// the #119 WARN-never-gate flow exactly: this only ever pushes a warning, never blocks or
|
|
948
|
+
// alters the append-on-change logic that follows.
|
|
949
|
+
if (!persistsField(store, 'extension_identity')) {
|
|
950
|
+
traceWarnings.push("this run store does not persist 'extension_identity' — drift detection is unavailable " +
|
|
951
|
+
'(the baseline resets every execution, so drift can never accumulate or be detected ' +
|
|
952
|
+
'on this store)');
|
|
953
|
+
}
|
|
757
954
|
const identityHistory = pendingRun.extension_identity ?? [];
|
|
758
955
|
const lastIdentity = identityHistory[identityHistory.length - 1];
|
|
759
956
|
if (lastIdentity === undefined || extensionIdentityDiffers(lastIdentity, registryIdentity)) {
|
|
@@ -1071,14 +1268,15 @@ export async function executeStep(store, definition, options) {
|
|
|
1071
1268
|
catch (storeErr) {
|
|
1072
1269
|
blockStoreWarning = `Failed to persist capability block: ${storeErr instanceof Error ? storeErr.message : String(storeErr)}`;
|
|
1073
1270
|
}
|
|
1074
|
-
|
|
1075
|
-
try
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1271
|
+
// issue #207 PR-2 (D3 §5): NO WAL delete belongs on this capability-block settle path — the
|
|
1272
|
+
// prior try/catch here was removed, not just gated. Contract-consistency hygiene, not a
|
|
1273
|
+
// functional fix: capability-block is reachable ONLY for `execution: 'auto'` steps
|
|
1274
|
+
// (ENGINE_HANDLER_NOT_REGISTERED/ENGINE_ADAPTER_NOT_REGISTERED mint only in the
|
|
1275
|
+
// auto-dispatch branches), and a WAL only ever exists for `execution: 'agent'` steps
|
|
1276
|
+
// (`append_trace` refuses non-agent steps) — DISJOINT populations; there is no in-repo
|
|
1277
|
+
// blocked-path WAL to clean up. A custom embedder whose dispatcher throws NOT_REGISTERED
|
|
1278
|
+
// for an agent step would leave WAL residue reaped at purge (an enumerated, accepted
|
|
1279
|
+
// residue class — D3 §8 residual 6), never silently destroyed here.
|
|
1082
1280
|
// Non-terminal 'stop' → 'report_to_user' via the existing mapping: a human must provision the
|
|
1083
1281
|
// runner (or re-run on a capable one); no further progress is possible on THIS runner.
|
|
1084
1282
|
const blockedAction = resolvePostDispatchAgentAction(dispatchError, false);
|
|
@@ -1103,7 +1301,7 @@ export async function executeStep(store, definition, options) {
|
|
|
1103
1301
|
status: 'error',
|
|
1104
1302
|
data: {},
|
|
1105
1303
|
evidence: allEvidence,
|
|
1106
|
-
warnings: mergeWarnings(traceWarnings, blockStoreWarning
|
|
1304
|
+
warnings: mergeWarnings(traceWarnings, blockStoreWarning),
|
|
1107
1305
|
errors: [dispatchError.message],
|
|
1108
1306
|
agent_action: blockedAction,
|
|
1109
1307
|
error_code: recoverableCode,
|
|
@@ -1158,12 +1356,60 @@ export async function executeStep(store, definition, options) {
|
|
|
1158
1356
|
storeCleanupWarning = `Failed to persist step failure: ${storeErr instanceof Error ? storeErr.message : String(storeErr)}`;
|
|
1159
1357
|
}
|
|
1160
1358
|
let walCleanupWarning;
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1359
|
+
// issue #207 PR-2 (D3 §5): gate the WAL cleanup on the preceding store.update having actually
|
|
1360
|
+
// SUCCEEDED (persistedRun defined) — a failed persist leaves the step in_progress with its
|
|
1361
|
+
// trace already read into an evidence write that never landed anywhere durable; the WAL is
|
|
1362
|
+
// the SOLE remaining evidence copy until reclaim recovers the wedge (#186 posture). Reclaim's
|
|
1363
|
+
// own drain (reclaim-step.ts) warns with the destroyed entry count when it eventually clears
|
|
1364
|
+
// this same buffer.
|
|
1365
|
+
//
|
|
1366
|
+
// issue #197 PR-2 (deliverable 1f): when this step's post-claim read found foreign (preserved,
|
|
1367
|
+
// not adopted) lines AND the store declares `seal`, retire the WAL to a sealed artifact
|
|
1368
|
+
// instead of destroying it — `preserved_foreign === 0` (the overwhelming common case, and the
|
|
1369
|
+
// ONLY case on a non-seal-declaring store, since carriage requires seal by the ladder) takes
|
|
1370
|
+
// the exact same plain `delete()` this always has, below, byte-identical.
|
|
1371
|
+
if (persistedRun !== undefined) {
|
|
1372
|
+
let performPlainDelete = true;
|
|
1373
|
+
if (adoptionPartition !== undefined &&
|
|
1374
|
+
adoptionPartition.preserved_foreign > 0 &&
|
|
1375
|
+
options.traceBufferStore !== undefined &&
|
|
1376
|
+
storeDeclaresSeal(options.traceBufferStore)) {
|
|
1377
|
+
try {
|
|
1378
|
+
const sealResult = await options.traceBufferStore.sealFenced(options.runId, options.command, buildSettleSealGuard(store, options.runId, options.command));
|
|
1379
|
+
if (sealResult.sealed) {
|
|
1380
|
+
performPlainDelete = false;
|
|
1381
|
+
walCleanupWarning =
|
|
1382
|
+
`${adoptionPartition.preserved_foreign} foreign line(s) preserved (sealed) — ` +
|
|
1383
|
+
'retrieve via `realm run export`';
|
|
1384
|
+
}
|
|
1385
|
+
else if (sealResult.reason === 'capped') {
|
|
1386
|
+
// Fall back to the SAME plain delete below (loud + bounded, never a silent eviction
|
|
1387
|
+
// of an already-sealed artifact to make room) — the destroyed-count warning names
|
|
1388
|
+
// exactly what happened; performPlainDelete stays true.
|
|
1389
|
+
walCleanupWarning = 'preservation cap reached — foreign lines destroyed, not preserved';
|
|
1390
|
+
}
|
|
1391
|
+
else {
|
|
1392
|
+
// 'absent' — the live WAL vanished between the post-claim read and this seal attempt
|
|
1393
|
+
// (e.g. a concurrent purge/reclaim) — nothing left to delete either; residue-not-loss.
|
|
1394
|
+
performPlainDelete = false;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
catch (err) {
|
|
1398
|
+
// A THROW (lock contention, genuine I/O failure) ⇒ warn + SKIP the delete
|
|
1399
|
+
// (residue-not-loss; the fence refuses further appends; purge reaps).
|
|
1400
|
+
performPlainDelete = false;
|
|
1401
|
+
walCleanupWarning = `Failed to seal trace buffer after step failure: ${err instanceof Error ? err.message : String(err)}`;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
if (performPlainDelete) {
|
|
1405
|
+
try {
|
|
1406
|
+
// Delete WAL after run state is written for failure — entries are now in evidence.
|
|
1407
|
+
await options.traceBufferStore?.delete(options.runId, options.command);
|
|
1408
|
+
}
|
|
1409
|
+
catch (walErr) {
|
|
1410
|
+
walCleanupWarning = `Failed to clean up trace buffer after step failure: ${walErr instanceof Error ? walErr.message : String(walErr)}`;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1167
1413
|
}
|
|
1168
1414
|
// Derive the agent_action from the error semantics and run termination state.
|
|
1169
1415
|
//
|
|
@@ -1392,8 +1638,54 @@ export async function executeStep(store, definition, options) {
|
|
|
1392
1638
|
});
|
|
1393
1639
|
return makeErrorEnvelope(options, pendingRun, internal, definition, traceWarnings.length > 0 ? traceWarnings : undefined);
|
|
1394
1640
|
}
|
|
1395
|
-
// Delete WAL after successful run update — entries are now in evidence.
|
|
1396
|
-
|
|
1641
|
+
// Delete WAL after successful run update — entries are now in evidence. issue #207 PR-2 (D3
|
|
1642
|
+
// §5, clause (a) of the unified deletion contract): wrapped in try/catch-to-warning — the
|
|
1643
|
+
// settlement already committed and is durably visible, so a cleanup failure here (e.g. lock
|
|
1644
|
+
// contention) must degrade to a warning, never surface as though the STEP itself failed (this
|
|
1645
|
+
// was previously unwrapped and would have thrown straight out of executeStep).
|
|
1646
|
+
//
|
|
1647
|
+
// issue #197 PR-2 (deliverable 1f): same seal-vs-delete decision as the failure-settle site
|
|
1648
|
+
// above — see its comment for the full outcome table. preserved_foreign === 0 (the common case,
|
|
1649
|
+
// and the ONLY case on a non-seal-declaring store) takes the exact same plain delete() this
|
|
1650
|
+
// always has, byte-identical.
|
|
1651
|
+
let successWalCleanupWarning;
|
|
1652
|
+
{
|
|
1653
|
+
let performPlainDelete = true;
|
|
1654
|
+
if (adoptionPartition !== undefined &&
|
|
1655
|
+
adoptionPartition.preserved_foreign > 0 &&
|
|
1656
|
+
options.traceBufferStore !== undefined &&
|
|
1657
|
+
storeDeclaresSeal(options.traceBufferStore)) {
|
|
1658
|
+
try {
|
|
1659
|
+
const sealResult = await options.traceBufferStore.sealFenced(options.runId, options.command, buildSettleSealGuard(store, options.runId, options.command));
|
|
1660
|
+
if (sealResult.sealed) {
|
|
1661
|
+
performPlainDelete = false;
|
|
1662
|
+
successWalCleanupWarning =
|
|
1663
|
+
`${adoptionPartition.preserved_foreign} foreign line(s) preserved (sealed) — ` +
|
|
1664
|
+
'retrieve via `realm run export`';
|
|
1665
|
+
}
|
|
1666
|
+
else if (sealResult.reason === 'capped') {
|
|
1667
|
+
successWalCleanupWarning =
|
|
1668
|
+
'preservation cap reached — foreign lines destroyed, not preserved';
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
// 'absent' — residue-not-loss; nothing left to delete either.
|
|
1672
|
+
performPlainDelete = false;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
catch (err) {
|
|
1676
|
+
performPlainDelete = false;
|
|
1677
|
+
successWalCleanupWarning = `Failed to seal trace buffer after step completion: ${err instanceof Error ? err.message : String(err)}`;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
if (performPlainDelete) {
|
|
1681
|
+
try {
|
|
1682
|
+
await options.traceBufferStore?.delete(options.runId, options.command);
|
|
1683
|
+
}
|
|
1684
|
+
catch (err) {
|
|
1685
|
+
successWalCleanupWarning = `Failed to clean up trace buffer after step completion: ${err instanceof Error ? err.message : String(err)}`;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1397
1689
|
// Step 7: Build and return ResponseEnvelope.
|
|
1398
1690
|
const nextActions = savedRun.terminal_state ? [] : buildNextActions(definition, savedRun);
|
|
1399
1691
|
const orientation = savedRun.terminal_state
|
|
@@ -1408,11 +1700,22 @@ export async function executeStep(store, definition, options) {
|
|
|
1408
1700
|
status: 'ok',
|
|
1409
1701
|
data: output,
|
|
1410
1702
|
evidence: allEvidence,
|
|
1411
|
-
warnings: mergeWarnings(traceWarnings, currentWarn),
|
|
1703
|
+
warnings: mergeWarnings(traceWarnings, currentWarn, successWalCleanupWarning),
|
|
1412
1704
|
errors: [],
|
|
1413
1705
|
context_hint: orientation,
|
|
1414
1706
|
run_phase: savedRun.run_phase,
|
|
1415
1707
|
next_actions: nextActions,
|
|
1708
|
+
// issue #197 PR-2 (deliverable 2e): additive per-partition counts, SET BY THE ENGINE (never
|
|
1709
|
+
// the MCP tool layer, which strips data/evidence and never sees per-line nonces). Gated on
|
|
1710
|
+
// `carriageActive` (store CAPABILITY), not on whether a nonce happened to be provided this
|
|
1711
|
+
// call — "absent for bare-floor stores" means incapable, not merely unused-this-time.
|
|
1712
|
+
...(carriageActive && adoptionPartition !== undefined
|
|
1713
|
+
? {
|
|
1714
|
+
adopted_own: adoptionPartition.adopted_own,
|
|
1715
|
+
adopted_anonymous: adoptionPartition.adopted_anonymous,
|
|
1716
|
+
preserved_foreign: adoptionPartition.preserved_foreign,
|
|
1717
|
+
}
|
|
1718
|
+
: {}),
|
|
1416
1719
|
};
|
|
1417
1720
|
}
|
|
1418
1721
|
/**
|
|
@@ -1803,7 +2106,19 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
1803
2106
|
}
|
|
1804
2107
|
return { ...record, terminal_state: true };
|
|
1805
2108
|
}
|
|
1806
|
-
async function executeChainInternal(store, definition, options, depth, chainedSteps
|
|
2109
|
+
async function executeChainInternal(store, definition, options, depth, chainedSteps,
|
|
2110
|
+
/**
|
|
2111
|
+
* issue #197 PR-2 (chain-replacement disposition, accepted): a settled DEPTH-0 step (typically
|
|
2112
|
+
* an agent step — never itself recorded in `chainedSteps`, which is auto-steps-only, feeding
|
|
2113
|
+
* the VISIBLE `chained_auto_steps` list) whose own envelope is about to be discarded in favor
|
|
2114
|
+
* of a deeper auto step's envelope would otherwise silently lose its OWN warnings (seal
|
|
2115
|
+
* outcome / half-minted / missing-carriage-leg advisories) — this separate accumulator exists
|
|
2116
|
+
* ONLY to carry those forward; it never touches `chainedSteps`'/`chained_auto_steps`'s shape.
|
|
2117
|
+
* The three NEW adoption counts on that depth-0 envelope are NOT similarly rescued — they
|
|
2118
|
+
* persist authoritatively in the settled step's own `trace_summary` regardless (see
|
|
2119
|
+
* `ResponseEnvelope.adopted_own`'s own doc) — only the warnings are a load-bearing rescue.
|
|
2120
|
+
*/
|
|
2121
|
+
depth0Warnings) {
|
|
1807
2122
|
if (depth > MAX_CHAIN_DEPTH) {
|
|
1808
2123
|
return {
|
|
1809
2124
|
command: options.command,
|
|
@@ -1945,7 +2260,20 @@ async function executeChainInternal(store, definition, options, depth, chainedSt
|
|
|
1945
2260
|
// Only agent steps or nothing — stop chain, return with latest next_actions.
|
|
1946
2261
|
return result;
|
|
1947
2262
|
}
|
|
1948
|
-
|
|
2263
|
+
// issue #197 PR-2 (chain-replacement disposition): THIS step's own result is about to be
|
|
2264
|
+
// discarded in favor of the recursive call's — capture its warnings now, before that happens.
|
|
2265
|
+
// Every step past depth 0 in this recursion is guaranteed 'auto' (nextAutoStep's own filter),
|
|
2266
|
+
// so depth === 0 is the ONLY case where a non-auto (agent) step's warnings would otherwise be
|
|
2267
|
+
// lost here (an 'auto' depth-0 step's warnings are already captured above via `chainedSteps`,
|
|
2268
|
+
// so this would double them — the `depth === 0` guard is exact, not merely conservative:
|
|
2269
|
+
// `chainedSteps` only records 'auto' steps, so a depth-0 'auto' step's warnings are recorded
|
|
2270
|
+
// there, never here, avoiding any double-count).
|
|
2271
|
+
if (depth === 0 &&
|
|
2272
|
+
definition.steps[options.command]?.execution !== 'auto' &&
|
|
2273
|
+
result.warnings.length > 0) {
|
|
2274
|
+
depth0Warnings.push(...result.warnings);
|
|
2275
|
+
}
|
|
2276
|
+
return executeChainInternal(store, definition, { ...options, command: nextAutoStep, input: {} }, depth + 1, chainedSteps, depth0Warnings);
|
|
1949
2277
|
}
|
|
1950
2278
|
/**
|
|
1951
2279
|
* Executes a step and automatically chains into subsequent `execution: auto` steps.
|
|
@@ -1997,8 +2325,11 @@ export async function executeChain(store, definition, options) {
|
|
|
1997
2325
|
registry: options.registry ?? createDefaultRegistry(),
|
|
1998
2326
|
};
|
|
1999
2327
|
const chained = [];
|
|
2000
|
-
|
|
2001
|
-
|
|
2328
|
+
// issue #197 PR-2 (chain-replacement disposition) — see executeChainInternal's own doc on this
|
|
2329
|
+
// parameter for the full contract.
|
|
2330
|
+
const depth0Warnings = [];
|
|
2331
|
+
const result = await executeChainInternal(store, definition, effectiveOptions, 0, chained, depth0Warnings);
|
|
2332
|
+
const chainWarnings = [...depth0Warnings, ...chained.flatMap((s) => s.warnings ?? [])];
|
|
2002
2333
|
const envelope = {
|
|
2003
2334
|
...result,
|
|
2004
2335
|
command: options.command,
|