@sensigo/realm 0.26.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 +355 -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 +187 -12
- 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 +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -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.map +1 -1
- package/dist/store/json-file-store.js +2 -5
- package/dist/store/json-file-store.js.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,6 +1,8 @@
|
|
|
1
1
|
import { extensionIdentityDiffers } from '../types/extension-identity.js';
|
|
2
2
|
import { WorkflowError } from '../types/workflow-error.js';
|
|
3
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';
|
|
4
6
|
import { captureEvidence } from '../evidence/snapshot.js';
|
|
5
7
|
import { validateInputSchema, validateOutputSchema, validateTraceSchema, } from '../validation/input-schema.js';
|
|
6
8
|
import { normalizeTrace } from './trace-normalizer.js';
|
|
@@ -348,13 +350,75 @@ export function buildNextActions(definition, run) {
|
|
|
348
350
|
.map((name) => stepToNextAction(name, definition.steps[name], context));
|
|
349
351
|
}
|
|
350
352
|
/**
|
|
351
|
-
* Merges call-scoped trace-schema warnings with
|
|
352
|
-
*
|
|
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).
|
|
353
359
|
*/
|
|
354
|
-
function mergeWarnings(traceWarnings,
|
|
355
|
-
|
|
360
|
+
function mergeWarnings(traceWarnings, ...extraWarnings) {
|
|
361
|
+
const defined = extraWarnings.filter((w) => w !== undefined);
|
|
362
|
+
if (traceWarnings.length === 0 && defined.length === 0)
|
|
356
363
|
return [];
|
|
357
|
-
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
|
+
};
|
|
358
422
|
}
|
|
359
423
|
/**
|
|
360
424
|
* Issue #185 Fix 1 (budget-priority): builds the merged, canonicalized trace for an agent step,
|
|
@@ -617,17 +681,57 @@ export async function executeStep(store, definition, options) {
|
|
|
617
681
|
let preNormalizedTrace;
|
|
618
682
|
let walEntries = [];
|
|
619
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;
|
|
620
695
|
if (stepDef?.execution === 'agent') {
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
+
}
|
|
625
724
|
const hasAnyTrace = walEntries.length > 0 || (options.trace !== undefined && options.trace.length > 0);
|
|
626
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);
|
|
627
731
|
// issue #185 Fix 1: budget-priority merge (see buildPriorityMergedTrace's own doc) — this
|
|
628
732
|
// pre-claim pass exists only to feed validateTraceSchema below; its RESULT is discarded
|
|
629
733
|
// (not stored into the outer preNormalizedTrace) once the enforce-gate decision is made.
|
|
630
|
-
const preClaimNormalized = buildPriorityMergedTrace(
|
|
734
|
+
const preClaimNormalized = buildPriorityMergedTrace(prClaimPartition.adopted, options.trace);
|
|
631
735
|
// Validate trace schema if configured (unchanged call site).
|
|
632
736
|
if (stepDef.trace_schema !== undefined) {
|
|
633
737
|
const mode = stepDef.trace_validation_mode ?? 'warn';
|
|
@@ -709,14 +813,51 @@ export async function executeStep(store, definition, options) {
|
|
|
709
813
|
// complete, post-claim set, which is also what the captureEvidence call site further below
|
|
710
814
|
// keys its `stepDef?.execution === 'agent' && walEntries.length > 0` check on.
|
|
711
815
|
if (stepDef?.execution === 'agent') {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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.
|
|
716
856
|
if (walEntries.length > 0 || (options.trace !== undefined && options.trace.length > 0)) {
|
|
717
857
|
// issue #185 Fix 1: same budget-priority merge as the pre-claim pass, now over the
|
|
718
|
-
// complete post-claim
|
|
719
|
-
|
|
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);
|
|
720
861
|
// Carry over the enforce-gate's schema-validation result (computed pre-claim against a
|
|
721
862
|
// possibly-incomplete set) rather than re-validating here — Fix 2 deliberately keeps
|
|
722
863
|
// validation pre-claim (see that block's comment); this just republishes its verdict onto
|
|
@@ -726,13 +867,48 @@ export async function executeStep(store, definition, options) {
|
|
|
726
867
|
preNormalizedTrace.summary.validation_mode = preClaimSchemaResult.validation_mode;
|
|
727
868
|
preNormalizedTrace.summary.validation_errors = preClaimSchemaResult.validation_errors;
|
|
728
869
|
}
|
|
729
|
-
// issue #185 Fix 3
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
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
|
+
}
|
|
736
912
|
}
|
|
737
913
|
}
|
|
738
914
|
}
|
|
@@ -1092,14 +1268,15 @@ export async function executeStep(store, definition, options) {
|
|
|
1092
1268
|
catch (storeErr) {
|
|
1093
1269
|
blockStoreWarning = `Failed to persist capability block: ${storeErr instanceof Error ? storeErr.message : String(storeErr)}`;
|
|
1094
1270
|
}
|
|
1095
|
-
|
|
1096
|
-
try
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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.
|
|
1103
1280
|
// Non-terminal 'stop' → 'report_to_user' via the existing mapping: a human must provision the
|
|
1104
1281
|
// runner (or re-run on a capable one); no further progress is possible on THIS runner.
|
|
1105
1282
|
const blockedAction = resolvePostDispatchAgentAction(dispatchError, false);
|
|
@@ -1124,7 +1301,7 @@ export async function executeStep(store, definition, options) {
|
|
|
1124
1301
|
status: 'error',
|
|
1125
1302
|
data: {},
|
|
1126
1303
|
evidence: allEvidence,
|
|
1127
|
-
warnings: mergeWarnings(traceWarnings, blockStoreWarning
|
|
1304
|
+
warnings: mergeWarnings(traceWarnings, blockStoreWarning),
|
|
1128
1305
|
errors: [dispatchError.message],
|
|
1129
1306
|
agent_action: blockedAction,
|
|
1130
1307
|
error_code: recoverableCode,
|
|
@@ -1179,12 +1356,60 @@ export async function executeStep(store, definition, options) {
|
|
|
1179
1356
|
storeCleanupWarning = `Failed to persist step failure: ${storeErr instanceof Error ? storeErr.message : String(storeErr)}`;
|
|
1180
1357
|
}
|
|
1181
1358
|
let walCleanupWarning;
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
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
|
+
}
|
|
1188
1413
|
}
|
|
1189
1414
|
// Derive the agent_action from the error semantics and run termination state.
|
|
1190
1415
|
//
|
|
@@ -1413,8 +1638,54 @@ export async function executeStep(store, definition, options) {
|
|
|
1413
1638
|
});
|
|
1414
1639
|
return makeErrorEnvelope(options, pendingRun, internal, definition, traceWarnings.length > 0 ? traceWarnings : undefined);
|
|
1415
1640
|
}
|
|
1416
|
-
// Delete WAL after successful run update — entries are now in evidence.
|
|
1417
|
-
|
|
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
|
+
}
|
|
1418
1689
|
// Step 7: Build and return ResponseEnvelope.
|
|
1419
1690
|
const nextActions = savedRun.terminal_state ? [] : buildNextActions(definition, savedRun);
|
|
1420
1691
|
const orientation = savedRun.terminal_state
|
|
@@ -1429,11 +1700,22 @@ export async function executeStep(store, definition, options) {
|
|
|
1429
1700
|
status: 'ok',
|
|
1430
1701
|
data: output,
|
|
1431
1702
|
evidence: allEvidence,
|
|
1432
|
-
warnings: mergeWarnings(traceWarnings, currentWarn),
|
|
1703
|
+
warnings: mergeWarnings(traceWarnings, currentWarn, successWalCleanupWarning),
|
|
1433
1704
|
errors: [],
|
|
1434
1705
|
context_hint: orientation,
|
|
1435
1706
|
run_phase: savedRun.run_phase,
|
|
1436
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
|
+
: {}),
|
|
1437
1719
|
};
|
|
1438
1720
|
}
|
|
1439
1721
|
/**
|
|
@@ -1824,7 +2106,19 @@ async function buildFinalizedSeal(definition, sealDraft, outcome, registry) {
|
|
|
1824
2106
|
}
|
|
1825
2107
|
return { ...record, terminal_state: true };
|
|
1826
2108
|
}
|
|
1827
|
-
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) {
|
|
1828
2122
|
if (depth > MAX_CHAIN_DEPTH) {
|
|
1829
2123
|
return {
|
|
1830
2124
|
command: options.command,
|
|
@@ -1966,7 +2260,20 @@ async function executeChainInternal(store, definition, options, depth, chainedSt
|
|
|
1966
2260
|
// Only agent steps or nothing — stop chain, return with latest next_actions.
|
|
1967
2261
|
return result;
|
|
1968
2262
|
}
|
|
1969
|
-
|
|
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);
|
|
1970
2277
|
}
|
|
1971
2278
|
/**
|
|
1972
2279
|
* Executes a step and automatically chains into subsequent `execution: auto` steps.
|
|
@@ -2018,8 +2325,11 @@ export async function executeChain(store, definition, options) {
|
|
|
2018
2325
|
registry: options.registry ?? createDefaultRegistry(),
|
|
2019
2326
|
};
|
|
2020
2327
|
const chained = [];
|
|
2021
|
-
|
|
2022
|
-
|
|
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 ?? [])];
|
|
2023
2333
|
const envelope = {
|
|
2024
2334
|
...result,
|
|
2025
2335
|
command: options.command,
|