@rulvar/core 1.6.0 → 1.8.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/index.d.ts CHANGED
@@ -608,6 +608,23 @@ interface UsageSlice {
608
608
  usage: Usage;
609
609
  }
610
610
  /**
611
+ * Cost-attribution facts a live run knows at settlement and a pure
612
+ * journal fold cannot re-derive: the innermost phase name at the call
613
+ * site, the agent profile, the primary invocation role, the budget
614
+ * account the call debited, and whether the dispatch spent the
615
+ * orchestrator finalize reserve. Policy, never identity, exactly like
616
+ * usageByModel: none of it enters the content key, and entries written
617
+ * before the field shipped fold under the documented fallback buckets
618
+ * (empty phase, 'unknown' agent type, role 'loop').
619
+ */
620
+ interface CostAttributionFacts {
621
+ phase?: string;
622
+ agentType?: string;
623
+ role?: InvocationRole;
624
+ budgetAccount?: string;
625
+ finalizeReserve?: boolean;
626
+ }
627
+ /**
611
628
  * The per-model slices of a terminal entry: the recorded split when the
612
629
  * call spanned several models, else the whole usage attributed to
613
630
  * `servedBy`. The fallback is what makes every journal written before the
@@ -669,6 +686,13 @@ type JournalEntry = {
669
686
  * Policy, never identity: it does not enter the content key.
670
687
  */
671
688
  usageByModel?: UsageSlice[];
689
+ /**
690
+ * Terminal usage-bearing entries: the attribution facts behind the
691
+ * CostReport breakdowns, so a pure journal fold reproduces the live
692
+ * report byte for byte on replay. Policy, never identity, exactly
693
+ * like usageByModel.
694
+ */
695
+ costAttribution?: CostAttributionFacts;
672
696
  transcriptRef?: string;
673
697
  checkpointRef?: string;
674
698
  /**
@@ -1776,7 +1800,16 @@ interface ResumeReport {
1776
1800
  misses: number;
1777
1801
  skipped: number;
1778
1802
  reruns: number;
1779
- /** Journaled operations never consumed by any live call (deleted calls). */
1803
+ /**
1804
+ * Effect roots that genuinely need recovery under the entry-type
1805
+ * pairing rules: dangling dispatches (status 'running' with no
1806
+ * terminal) and suspensions with no resolution, neither consumed by a
1807
+ * live call nor covered by abandon. Complete operations are NEVER
1808
+ * listed: settled roots, single-entry kinds (decisions, facts, plan
1809
+ * and termination entries), and resolved suspensions are whole by
1810
+ * construction. A call deleted from the code is silently skipped and
1811
+ * never re-paid; it appears here only while its effect is dangling.
1812
+ */
1780
1813
  orphaned: number[];
1781
1814
  }
1782
1815
  /**
@@ -1789,6 +1822,8 @@ declare class JournalMatcher {
1789
1822
  private readonly byScope;
1790
1823
  private readonly all;
1791
1824
  private readonly consumed;
1825
+ /** Suspension seqs holding at least one resolution ref-entry. */
1826
+ private readonly resolvedRefs;
1792
1827
  private readonly keyRing;
1793
1828
  private disposition;
1794
1829
  private aliasDisposition?;
@@ -2083,6 +2118,8 @@ interface TerminalPatch {
2083
2118
  servedBy?: ModelRef;
2084
2119
  /** Set only when the call spanned several serving models; see JournalEntry. */
2085
2120
  usageByModel?: UsageSlice[];
2121
+ /** Attribution facts behind the CostReport breakdowns; see JournalEntry. */
2122
+ costAttribution?: CostAttributionFacts;
2086
2123
  transcriptRef?: string;
2087
2124
  checkpointRef?: string;
2088
2125
  /** Terminal agent entries: Artifact list. */
@@ -3088,9 +3125,14 @@ interface TerminationLimits {
3088
3125
  kMax: number;
3089
3126
  /** B0; immutable after start, no API including HITL can top up. */
3090
3127
  runBudgetUsdCeiling: number;
3091
- /** From the orchestrator budget (DEF-7; XF-09). */
3128
+ /**
3129
+ * The resolved orchestrator cap in absolute USD (DEF-7; XF-09),
3130
+ * frozen with the counters. Journals recorded before v1.8 store 0
3131
+ * ("not yet resolved"); for them the orchestrator_budget_reserve
3132
+ * decision is the authority and is recovered on resume.
3133
+ */
3092
3134
  orchestratorCapUsd: number;
3093
- /** From the orchestrator budget (DEF-7; XF-09). */
3135
+ /** The finalize reserve carved out of the cap; 0 in pre-v1.8 journals. */
3094
3136
  finalizeReserveUsd: number;
3095
3137
  }
3096
3138
  /** Appendix A committed defaults for the countable resources. */
@@ -3348,6 +3390,26 @@ interface BudgetAccountView {
3348
3390
  parentScope?: string;
3349
3391
  }
3350
3392
  /**
3393
+ * Why a ceiling error ended the work: the first closed account walking
3394
+ * from the debited scope toward the root, plus the root state, so the
3395
+ * outward message can name WHICH ceiling actually crossed instead of
3396
+ * blaming the run ceiling for every crossing.
3397
+ */
3398
+ interface BudgetExhaustionDiagnostics {
3399
+ crossed?: {
3400
+ scope: string;
3401
+ source: "root" | "orchestrator-cap" | "child-account";
3402
+ ceilingUsd: number;
3403
+ spentUsd: number;
3404
+ committedReserveUsd: number;
3405
+ finalizeReserveUsd: number;
3406
+ };
3407
+ root: {
3408
+ ceilingUsd?: number;
3409
+ spentUsd: number;
3410
+ };
3411
+ }
3412
+ /**
3351
3413
  * The per-run budget account tree. All spend accounting is per instance;
3352
3414
  * the journal remains the durable source (the root is seeded by the
3353
3415
  * ledger fold on resume, M2; sub-account reserves are recovered from
@@ -3395,7 +3457,20 @@ declare class RunBudget {
3395
3457
  parentScope?: string;
3396
3458
  ceilingUsd?: number;
3397
3459
  finalizeReserveUsd?: number;
3460
+ kind?: "orchestrator-cap" | "child-allowance";
3398
3461
  }): void;
3462
+ /**
3463
+ * The diagnostic projection behind a ceiling error: the first CLOSED
3464
+ * account (projected commitments included, exactly the layer-1
3465
+ * closure test) walking from `scope` toward the root, plus the root
3466
+ * state. 'run budget ceiling reached' under a healthy root misled the
3467
+ * v1.6.0 follow-up review's live probe when only a 0.18 USD
3468
+ * orchestrator cap had crossed under a 0.90 USD root; the message can
3469
+ * now name the account that actually ended the work. An unknown scope
3470
+ * degrades to root-only diagnostics instead of throwing: this runs on
3471
+ * the error path.
3472
+ */
3473
+ exhaustionDiagnostics(scope: string): BudgetExhaustionDiagnostics;
3399
3474
  accountView(scope: string): BudgetAccountView | undefined;
3400
3475
  /**
3401
3476
  * The admission remainder of one account: ceiling minus spend minus
@@ -3403,6 +3478,18 @@ declare class RunBudget {
3403
3478
  * fractions never eat finalization money). Undefined when uncapped.
3404
3479
  */
3405
3480
  remainderOf(scope: string): number | undefined;
3481
+ /**
3482
+ * The tightest allowance headroom on the chain of `scope`: the minimum
3483
+ * remainder across 'child-allowance' accounts. An allowance ceiling
3484
+ * bounds the child's LIFETIME spend, so projected admission must never
3485
+ * hold more than this against the chain (the layer-2 mirror lives in
3486
+ * the orchestrator admission's childCeiling clamp): a reserve above
3487
+ * the allowance would deny work that the allowance itself already
3488
+ * bounds. Undefined when no allowance account is on the chain; the
3489
+ * clamp never applies to the run root or an orchestrator cap, whose
3490
+ * headroom is shared money that projected admission must protect.
3491
+ */
3492
+ allowanceHeadroomOf(scope: string): number | undefined;
3406
3493
  /** Layer 3 ceiling signal of the run root; live streams sever through it. */
3407
3494
  get signal(): AbortSignal;
3408
3495
  /** The layer-3 signal of one sub-account's subtree, when it exists. */
@@ -3691,6 +3778,23 @@ type AdmitRejectReason = {
3691
3778
  code: "osc_guard";
3692
3779
  spawnKey: SpawnKey;
3693
3780
  oscillationCount: number;
3781
+ } | {
3782
+ /**
3783
+ * The declared estimate cannot fit the child's own ceiling: the
3784
+ * host said the work costs more than the budget buys, so the op
3785
+ * is bounced with the actionable correction BEFORE it changes
3786
+ * plan state or consumes a spawn unit (the v1.7.0 follow-up
3787
+ * review's P1). Heuristic reserves never produce this code; they
3788
+ * clamp to the allowance instead.
3789
+ */
3790
+ code: "reserve_exceeds_budget";
3791
+ agentType: string;
3792
+ childAccount: string;
3793
+ estCostUsd: number;
3794
+ resolvedReserveUsd: number;
3795
+ childCeilingUsd: number;
3796
+ minimumBudgetUsd: number;
3797
+ message: string;
3694
3798
  };
3695
3799
  /** Every spawn origin routed through the single admission point. */
3696
3800
  type SpawnOrigin = "ctx.workflow" | "ctx.orchestrate" | "spawn_agent" | "parallel_agents" | "escalation-decomposition" | "rung-respawn" | "reuse-link";
@@ -3708,6 +3812,13 @@ interface AdmitSpec {
3708
3812
  /** Reserve hint; falls back to the flat engine default. */
3709
3813
  estCostUsd?: number;
3710
3814
  /**
3815
+ * Same-batch reserves already admitted read-only but not yet
3816
+ * committed (a multi-op plan revision): the read-only branch adds
3817
+ * them to this spawn's reserve so every embedded admit of one batch
3818
+ * is dispatchable under the same snapshot, not just the first.
3819
+ */
3820
+ pendingReserveUsd?: number;
3821
+ /**
3711
3822
  * Lineage continuation (DEF-3); absence mints a fresh lineage root. A
3712
3823
  * continuation demands a causeRef: the seq of the entry that caused the
3713
3824
  * rebirth.
@@ -3858,6 +3969,17 @@ declare class AdmissionController {
3858
3969
  * journaled by the caller so replay re-delivers it without
3859
3970
  * re-evaluation.
3860
3971
  */
3972
+ /**
3973
+ * The reserve the DISPATCH layer will actually commit for this spec:
3974
+ * the estimate (or the flat default) clamped by the explicit child
3975
+ * budget when one exists, because only an explicit budget opens a
3976
+ * child-allowance account at dispatch; the childBudgetFraction cap
3977
+ * never materializes as an account and must not shrink the
3978
+ * projection. The token-count-priced estimate of ctx.agent is
3979
+ * unreachable here (async); a divergence there lands as a journaled
3980
+ * dispatch rejection instead of a strand.
3981
+ */
3982
+ projectedDispatchReserveUsd(spec: Pick<AdmitSpec, "estCostUsd" | "budgetUsd">): number;
3861
3983
  admit(spec: AdmitSpec, options?: {
3862
3984
  commitReserve?: boolean;
3863
3985
  }): AdmissionDecision;
@@ -4146,9 +4268,14 @@ type WorkflowEvent = {
4146
4268
  /** Folds the per-run attribution buckets into the normative CostReport. */
4147
4269
  declare function buildCostReport(attribution: CostAttribution, totalUsd: number): CostReport;
4148
4270
  /**
4149
- * The pure journal fold: byModel and totals from terminal entries, the
4150
- * same summation the kernel ledger uses (terminal usage exactly once,
4151
- * priced per servedBy, abandoned subtrees contribute zero).
4271
+ * The pure journal fold: the complete CostReport from terminal entries,
4272
+ * the same summation the kernel ledger uses (terminal usage exactly
4273
+ * once, priced per servedBy slice, abandoned subtrees contribute zero).
4274
+ * The orchestrator block folds too: spend attributed to the
4275
+ * orchestrator sub-account, the reserve-funded share of it, the armed
4276
+ * wake count, and the at-cap freeze flag from the journaled cap
4277
+ * decision, so a replay-only resume reproduces the block instead of
4278
+ * reading this process's live accounts (which a replay never charges).
4152
4279
  */
4153
4280
  declare function costReportFromJournal(entries: readonly JournalEntry[], priceUsd: (servedBy: ModelRef, usage: Usage) => number | undefined): CostReport;
4154
4281
  //#endregion
@@ -4171,7 +4298,15 @@ interface CostReport {
4171
4298
  byPhase: Record<string, number>;
4172
4299
  byAgentType: Record<string, number>;
4173
4300
  byRole: Record<InvocationRole, number>;
4174
- /** All-zero with forcedFinish false in runs without a dynamic orchestrator. */
4301
+ /**
4302
+ * All-zero with forcedFinish false in runs without a dynamic
4303
+ * orchestrator (or when no cap resolved, so no sub-account opened).
4304
+ * Folded purely from the journal: spentUsd is the priced usage of
4305
+ * entries debited to the orchestrator sub-account, reserveUsedUsd its
4306
+ * reserve-funded forced-finish share, wakes the ARMED (journaled)
4307
+ * wake suspensions (a wait satisfied synchronously never suspends and
4308
+ * is not counted), and forcedFinish the journaled at-cap decision.
4309
+ */
4175
4310
  orchestrator: {
4176
4311
  spentUsd: number; /** spentUsd / max(totalUsd, 0.01): the epsilon-floored H-OrchShare input. */
4177
4312
  share: number;
@@ -4746,6 +4881,16 @@ interface OrchestratorExtensionIO {
4746
4881
  readonly gates: Record<string, unknown>;
4747
4882
  /** The run USD ceiling (B0), when one exists. */
4748
4883
  readonly runCeilingUsd?: number;
4884
+ /**
4885
+ * The resolved orchestrator cap in absolute USD (DEF-7; XF-09):
4886
+ * min(budget.capUsd, capFraction x B0) on a fresh run, the frozen
4887
+ * orchestrator_budget_reserve dollars on resume. Resolved strictly
4888
+ * before boot so an extension can freeze it into termination.init;
4889
+ * always present under PlanRunner (an unresolvable cap refuses boot).
4890
+ */
4891
+ readonly orchestratorCapUsd?: number;
4892
+ /** The finalize reserve carved out of the cap, resolved with it. */
4893
+ readonly finalizeReserveUsd?: number;
4749
4894
  /** ULID minting for engine-owned identifiers (NodeIds). */
4750
4895
  mintId(): string;
4751
4896
  /**
@@ -4852,6 +4997,13 @@ interface OrchestratorExtension {
4852
4997
  * machinery (reserves, freeze) completes in M7 (DEF-7).
4853
4998
  */
4854
4999
  interface OrchestratorBudgetSpec {
5000
+ /**
5001
+ * Absolute bound in USD. It never REPLACES the fraction bound:
5002
+ * effectiveCap = min(capUsd, (capFraction ?? 0.2) * ceiling), so an
5003
+ * explicit capUsd larger than the default fraction of the run ceiling
5004
+ * is still cut to that fraction (and a warn log says so). Pass
5005
+ * capFraction: 1.0 to make capUsd the sole bound.
5006
+ */
4855
5007
  capUsd?: number;
4856
5008
  /** default 0.2; effectiveCap = min of the given bounds */
4857
5009
  capFraction?: number;
@@ -6181,4 +6333,4 @@ interface SandboxBridge {
6181
6333
  }
6182
6334
  declare function createSandboxBridge(ctx: Ctx<never>, options: SandboxBridgeOptions): SandboxBridge;
6183
6335
  //#endregion
6184
- export { AWAIT_SCHEMA, AbandonAttempt, AbandonFold, AbandonPayload, AbandonedSpendView, AbortClass, type AdaptiveEvents, AdmissionController, AdmissionDecision, AdmissionRejectedError, AdmissionStatsBefore, AdmitLineage, AdmitRejectReason, AdmitSpec, AdmitVerdict, AgentCallError, AgentError, type AgentEvents, AgentIdentityInput, AgentOpts, AgentProfile, AgentProfilePermissions, AgentResult, AgentResultMeta, AgentStatus, ApproachSignatureInputs, ApprovalDecision, ApprovalIdentityInput, Artifact, AttemptOutcomeClass, BUDGET_ABORT_REASON, BriefOpts, BudgetAccountView, BudgetDefaults, BudgetExhaustedError, BudgetHooks, BudgetReserve, type Bytes, CANCEL_AGENT_SCHEMA, CHECKPOINT_FORMAT_V1, CLAIM_STATEMENT_MAX_CHARS, CLAIM_TTL_DAYS, COMPACTION_SUMMARY_PREFIX, CURRENT_HASH_VERSION, CacheHint, CacheTtl, CanUseTool, CanonicalId, CanonicalIdentity, CanonicalLadderSpec, CanonicalModelSpec, ChatEvent, ChatRequest, CheckpointState, ChildIdentityInput, type ClaimClass, type ClaimOp, type ClaimStatus, ClaimValidationOptions, CollectOpts, CollectedTurn, CompactionConfig, CompiledPermissionChain, CompiledWorkflow, ConfigError, type CoreEvents, CostAttribution, CostReport, CreateEngineOptions, Ctx, DEFAULT_CHILD_BUDGET_FRACTION, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_ESCALATION_LIMITS, DEFAULT_FLAT_RESERVE_USD, DEFAULT_MAX_CHILDREN_PER_NODE, DEFAULT_MAX_DEPTH, DEFAULT_MAX_OSCILLATIONS_PER_KEY, DEFAULT_MAX_PINNED_WORKTREES, DEFAULT_MAX_REVISIONS_PER_RUN, DEFAULT_MAX_TOTAL_SPAWNS, DEFAULT_MAX_TURNS, DEFAULT_MODEL_RETRY_ATTEMPTS, DEFAULT_NO_PROGRESS_TURNS, DEFAULT_PER_RUN_CONCURRENCY, DEFAULT_RETRY_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DebitResult, DeclaredLadder, DedupIndex, DedupNote, DerivedKey, DeriverRegistry, DispositionRule, DispositionTable, DonorCandidate, DonorRef, DroppedItem, EMIT_RESULT_TOOL, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, ESCALATE_TOOL_NAME, ESCALATION_REPORT_SCHEMA, ESCALATION_REQUEST_SCHEMA, EffectiveUsageLimits, Effort, Engine, EngineDefaults, EntryKind, EntryRef, EntryStatus, ErrorClass, ErrorCode, ErrorPolicy, EscalatedResult, EscalationDecision, EscalationDigest, EscalationKind, EscalationLimits, EscalationOptions, EscalationReport, EscalationRequest, EventBus, type EvidenceRef, ExtensionAppendInput, ExtensionDispatchSpec, ExternalIdentityInput, ExternalRegistry, ExtractNecessityInput, FINISH_SCHEMA, FINISH_TOOL_NAME, FailoverTarget, FailoverTrigger, FallbackField, FallbackTrigger, FileModelKnowledgeStore, FileModelKnowledgeStoreOptions, FileTranscriptStore, FinishInfo, Gate, GateAudit, type GateRecord, GitWorktreeProvider, GitWorktreeProviderOptions, GraftBoot, HashVersion, HookVerdict, INBOX_PROPOSAL_TTL_DAYS, IdentityInput, InMemoryStore, InMemoryTranscriptStore, InProcessRunner, InvalidResolutionError, InvocationRole, type IsolationProvider, type IsolationSpec, Issue$1 as Issue, JournalCompatSubCode, JournalCompatibilityError, JournalEntry, JournalMatcher, JournalMissError, JournalOperation, JournalOrderViolation, JournalSerializationHook, type JournalStore, type Json, JsonSchema, JsonlFileStore, KB_ACTIVE_CLAIMS_CAP, KB_CARD_RENDER_BUDGET_CHARS, type KbProposal, type KbProposalTrigger, KeyDeriver, KeyRing, KeyedLimiter, KnowledgeCasError, type KnowledgeSnapshot, LARGE_VALUE_WARN_BYTES, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LINEAGE_SIG_VERSION, LadderSpec, type LeasableStore, type Lease, LeaseHeldError, Ledger, LineageCounters, LineageIndex, LineageRef, LineageRelation, LineageStats, LogicalTaskId, MASKED_SECRET, MAX_DEPTH_CEILING, MatchResult, McpConfig, MechanicalGateProfile, MechanicalGateVerdict, type ModelCaps, ModelChoice, type ModelClaim, ModelEpochInputs, type ModelKnowledgeHandle, type ModelKnowledgeStore, ModelListConstraint, ModelRef, ModelRetry, ModelSpec, Msg, NoProgressDetector, NodeId, NodeLinkValue, NonSerializableValueError, ORCHESTRATE_WORKFLOW_NAME, OnEscalation, OperationDisposition, OrchestrateOptions, OrchestratorBudgetSpec, OrchestratorCapConfigError, OrchestratorExtension, OrchestratorExtensionIO, OrchestratorRuntime, Out, PARALLEL_AGENTS_SCHEMA, ParallelSiteCounter, Part, PendingExternal, PendingToolTurn, PermissionConfig, PermissionGate, PermissionHook, PermissionPreset, PermissionRule, PermissionVerdict, PhaseTarget, PipelineCollected, PipelineOpts, PlanInvariantError, PriceTable, PricedUsage, type Pricing, type PricingTier, type ProviderAdapter, QualityFloors, ROLE_EFFORT_DEFAULTS, ROOT_ACCOUNT, ROOT_SCOPE, RUN_PROFILES, RandIdentityInput, RandPayload, RefEntryAppender, RefEntryClassification, RefusalInfo, ReplayDisposition, ReplayMode, ReplayPlanHashMismatch, Replayer, ResolutionArbiter, ResolutionAttempt, ResolutionBy, ResolutionFold, ResolutionLayer, ResolutionOutcome, ResolutionPayload, ResolvedInvocation, ResolvedToolset, ResumeHandle, ResumeOptions, ResumePreview, ResumeReport, RetryClass, RetryPolicy, ReuseConfig, RiskRuleValue, Role, RulvarError, RulvarErrorCode, RunAgentOptions, RunBudget, RunEventSink, type RunFilter, RunHandle, RunInternals, type RunMeta, RunOptions, RunOutcome, RunProfile, RunStatus, RuntimeEventSink, SPAWN_AGENT_SCHEMA, SandboxBridge, SandboxBridgeOptions, SandboxError, SandboxHostToWorker, SandboxMethod, SandboxWorkerToHost, SchemaPair, SchemaSpec, SchemaValidationResult, ScopeSegment, ScriptRejected, ScriptRunner, ScrubNote, Semaphore, SerializationHook, Settled, ShellPatternRules, ShellSegment, ShellVerdict, SinglePhaseAppend, SpanMinter, SpanRegistry, SpawnAdmissionValue, SpawnAgentParams, SpawnKey, SpawnLineage, SpawnLineageOpt, SpawnOrigin, SpawnRecord, Spend, Stage, type StandardJSONSchemaV1, type StandardSchemaV1, StepIdentityInput, StructuredOutputTier, SuspendedAppend, SuspensionState, TOOL_NAME_PATTERN, type TaskClass, TaskDigest, TaskSpec, TerminalPatch, TerminationAccount, TerminationAccountSnapshot, TerminationDeniedValue, TerminationDeniedWriter, TerminationInitValue, TerminationLimits, TerminationResource, ToolCallRequest, ToolChoice, type ToolContext, ToolContextSeed, ToolContract, type ToolDef, type ToolEvents, type ToolExecutor, ToolInit, type ToolRisk, ToolRuntime, type ToolSource, type ToolSourceSession, ToolsOption, TranscriptSerializationHook, type TranscriptStore, TriggerClass, TtlState, Usage, UsageLimits, UsageSlice, VerifiedRecommendation, WAIT_FOR_EVENTS_SCHEMA, WAIT_FOR_EVENTS_TOOL_NAME, WAKE_SUMMARY_RENDER_BUDGET_CHARS, WakeBudgetBlock, WakeDigest, WakeTrigger, WireError, Workflow, WorkflowCallOpts, type WorkflowEvent, type WorkflowEventBody, WorkflowRegistry, admissionReserveUsd, affordableOutputTokens, agentErrorFromWire, agentErrorToWire, agentScope, applyClaimOps, applyStructuredOutputTier, approachSigCoarse, approachSigOf, archiveDeprecatedModelOps, atCompactionThreshold, buildAbandonFold, buildAdapterRegistry, buildCostReport, buildDeriverRegistry, buildOrchestratorTools, buildTerminationInitValue, buildToolContext, canRideLoopTurn, canonicalIsolationTag, canonicalizeLadder, canonicalizeSchema, capIssues, capsHashOf, checkFloors, checkpointRefFor, childCoveragePrefix, claimExpired, claimExpiry, claimIssues, claimOpIssues, classifyAgentError, classifyAttemptOutcome, collectDeclaredLadders, compactMessages, compilePermissionChain, compilePermissionPreset, compileVerifiedLayer, costReportFromJournal, countsAgainstLimit, createCanonicalIdMinter, createCtx, createEngine, createSandboxBridge, currentOnlyKeyRing, decodeCheckpoint, defineWorkflow, deriveContentKey, deriverV1, deriverV2, digestOf, dispositionHook, emptyDigestBlocks, emptyToolset, encodeCheckpoint, entryUsageSlices, escalateTool, evaluatePermission, evaluateReuse, executeWorkflow, exhaustionCodeOf, extractCandidate, failoverTriggerOf, fallbackTriggerOf, filterClaimsForRun, finalizeFires, foldTermination, formatRePrompt, formatScopePath, hashWorkflowBody, hashWorkflowSource, identityJcs, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lexShellCommand, liftRetainedParts, lineageWeightOf, makeOrchestratorWorkflow, maskSecrets, maskSecretsDeep, maskSecretsJson, matchArgvPattern, matchShellCommand, mcp, mergeUsageLimits, modelEpochOf, modelKnowledgeCard, modelSpecIdentity, needsSeparateExtract, nextFailover, nodeLinkKey, normalizeApproachTag, normalizeEntry, normalizeFallbacks, orchestrate, parallelScope, parseModelRef, parseScopePath, phiInitialOf, pipelineScope, planNodeScope, priceEntryUsage, priceUsdOf, profileCard, profileRegistrySnapshotHash, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, readTerminationInit, registryKeyRing, remeasureQueue, replayDisposition, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, roleConfiguredInRouting, roundOneDisposition, runAgent, runProfile, scanJournalCompatibility, schemaHash, schemaHashOfSpec, selectStructuredOutputTier, shouldCompact, spawnDepthOf, summarizeInstruction, summarizeOutput, terminationConfigDrift, tierWithinCaps, toApprovalDecision, toJournalValue, tool, toolContract, toolsetHash, ttlState, validateEditorialCommit, validateEntryShape, validateEscalationLimits, validateEscalationReport, validateSchemaSpec, validateTerminationLimits, workflowScope, workflowSourceRef, wrapJournalStore, wrapTranscriptStore };
6336
+ export { AWAIT_SCHEMA, AbandonAttempt, AbandonFold, AbandonPayload, AbandonedSpendView, AbortClass, type AdaptiveEvents, AdmissionController, AdmissionDecision, AdmissionRejectedError, AdmissionStatsBefore, AdmitLineage, AdmitRejectReason, AdmitSpec, AdmitVerdict, AgentCallError, AgentError, type AgentEvents, AgentIdentityInput, AgentOpts, AgentProfile, AgentProfilePermissions, AgentResult, AgentResultMeta, AgentStatus, ApproachSignatureInputs, ApprovalDecision, ApprovalIdentityInput, Artifact, AttemptOutcomeClass, BUDGET_ABORT_REASON, BriefOpts, BudgetAccountView, BudgetDefaults, BudgetExhaustedError, BudgetExhaustionDiagnostics, BudgetHooks, BudgetReserve, type Bytes, CANCEL_AGENT_SCHEMA, CHECKPOINT_FORMAT_V1, CLAIM_STATEMENT_MAX_CHARS, CLAIM_TTL_DAYS, COMPACTION_SUMMARY_PREFIX, CURRENT_HASH_VERSION, CacheHint, CacheTtl, CanUseTool, CanonicalId, CanonicalIdentity, CanonicalLadderSpec, CanonicalModelSpec, ChatEvent, ChatRequest, CheckpointState, ChildIdentityInput, type ClaimClass, type ClaimOp, type ClaimStatus, ClaimValidationOptions, CollectOpts, CollectedTurn, CompactionConfig, CompiledPermissionChain, CompiledWorkflow, ConfigError, type CoreEvents, CostAttribution, CostAttributionFacts, CostReport, CreateEngineOptions, Ctx, DEFAULT_CHILD_BUDGET_FRACTION, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_ESCALATION_LIMITS, DEFAULT_FLAT_RESERVE_USD, DEFAULT_MAX_CHILDREN_PER_NODE, DEFAULT_MAX_DEPTH, DEFAULT_MAX_OSCILLATIONS_PER_KEY, DEFAULT_MAX_PINNED_WORKTREES, DEFAULT_MAX_REVISIONS_PER_RUN, DEFAULT_MAX_TOTAL_SPAWNS, DEFAULT_MAX_TURNS, DEFAULT_MODEL_RETRY_ATTEMPTS, DEFAULT_NO_PROGRESS_TURNS, DEFAULT_PER_RUN_CONCURRENCY, DEFAULT_RETRY_POLICY, DEFAULT_STREAM_IDLE_TIMEOUT_MS, DebitResult, DeclaredLadder, DedupIndex, DedupNote, DerivedKey, DeriverRegistry, DispositionRule, DispositionTable, DonorCandidate, DonorRef, DroppedItem, EMIT_RESULT_TOOL, EMPTY_SCHEMA_HASH, EMPTY_TOOLSET_HASH, ESCALATE_TOOL_NAME, ESCALATION_REPORT_SCHEMA, ESCALATION_REQUEST_SCHEMA, EffectiveUsageLimits, Effort, Engine, EngineDefaults, EntryKind, EntryRef, EntryStatus, ErrorClass, ErrorCode, ErrorPolicy, EscalatedResult, EscalationDecision, EscalationDigest, EscalationKind, EscalationLimits, EscalationOptions, EscalationReport, EscalationRequest, EventBus, type EvidenceRef, ExtensionAppendInput, ExtensionDispatchSpec, ExternalIdentityInput, ExternalRegistry, ExtractNecessityInput, FINISH_SCHEMA, FINISH_TOOL_NAME, FailoverTarget, FailoverTrigger, FallbackField, FallbackTrigger, FileModelKnowledgeStore, FileModelKnowledgeStoreOptions, FileTranscriptStore, FinishInfo, Gate, GateAudit, type GateRecord, GitWorktreeProvider, GitWorktreeProviderOptions, GraftBoot, HashVersion, HookVerdict, INBOX_PROPOSAL_TTL_DAYS, IdentityInput, InMemoryStore, InMemoryTranscriptStore, InProcessRunner, InvalidResolutionError, InvocationRole, type IsolationProvider, type IsolationSpec, Issue$1 as Issue, JournalCompatSubCode, JournalCompatibilityError, JournalEntry, JournalMatcher, JournalMissError, JournalOperation, JournalOrderViolation, JournalSerializationHook, type JournalStore, type Json, JsonSchema, JsonlFileStore, KB_ACTIVE_CLAIMS_CAP, KB_CARD_RENDER_BUDGET_CHARS, type KbProposal, type KbProposalTrigger, KeyDeriver, KeyRing, KeyedLimiter, KnowledgeCasError, type KnowledgeSnapshot, LARGE_VALUE_WARN_BYTES, LEGACY_LTID_PREFIX, LEGACY_SIGNATURE_INPUTS, LINEAGE_SIG_VERSION, LadderSpec, type LeasableStore, type Lease, LeaseHeldError, Ledger, LineageCounters, LineageIndex, LineageRef, LineageRelation, LineageStats, LogicalTaskId, MASKED_SECRET, MAX_DEPTH_CEILING, MatchResult, McpConfig, MechanicalGateProfile, MechanicalGateVerdict, type ModelCaps, ModelChoice, type ModelClaim, ModelEpochInputs, type ModelKnowledgeHandle, type ModelKnowledgeStore, ModelListConstraint, ModelRef, ModelRetry, ModelSpec, Msg, NoProgressDetector, NodeId, NodeLinkValue, NonSerializableValueError, ORCHESTRATE_WORKFLOW_NAME, OnEscalation, OperationDisposition, OrchestrateOptions, OrchestratorBudgetSpec, OrchestratorCapConfigError, OrchestratorExtension, OrchestratorExtensionIO, OrchestratorRuntime, Out, PARALLEL_AGENTS_SCHEMA, ParallelSiteCounter, Part, PendingExternal, PendingToolTurn, PermissionConfig, PermissionGate, PermissionHook, PermissionPreset, PermissionRule, PermissionVerdict, PhaseTarget, PipelineCollected, PipelineOpts, PlanInvariantError, PriceTable, PricedUsage, type Pricing, type PricingTier, type ProviderAdapter, QualityFloors, ROLE_EFFORT_DEFAULTS, ROOT_ACCOUNT, ROOT_SCOPE, RUN_PROFILES, RandIdentityInput, RandPayload, RefEntryAppender, RefEntryClassification, RefusalInfo, ReplayDisposition, ReplayMode, ReplayPlanHashMismatch, Replayer, ResolutionArbiter, ResolutionAttempt, ResolutionBy, ResolutionFold, ResolutionLayer, ResolutionOutcome, ResolutionPayload, ResolvedInvocation, ResolvedToolset, ResumeHandle, ResumeOptions, ResumePreview, ResumeReport, RetryClass, RetryPolicy, ReuseConfig, RiskRuleValue, Role, RulvarError, RulvarErrorCode, RunAgentOptions, RunBudget, RunEventSink, type RunFilter, RunHandle, RunInternals, type RunMeta, RunOptions, RunOutcome, RunProfile, RunStatus, RuntimeEventSink, SPAWN_AGENT_SCHEMA, SandboxBridge, SandboxBridgeOptions, SandboxError, SandboxHostToWorker, SandboxMethod, SandboxWorkerToHost, SchemaPair, SchemaSpec, SchemaValidationResult, ScopeSegment, ScriptRejected, ScriptRunner, ScrubNote, Semaphore, SerializationHook, Settled, ShellPatternRules, ShellSegment, ShellVerdict, SinglePhaseAppend, SpanMinter, SpanRegistry, SpawnAdmissionValue, SpawnAgentParams, SpawnKey, SpawnLineage, SpawnLineageOpt, SpawnOrigin, SpawnRecord, Spend, Stage, type StandardJSONSchemaV1, type StandardSchemaV1, StepIdentityInput, StructuredOutputTier, SuspendedAppend, SuspensionState, TOOL_NAME_PATTERN, type TaskClass, TaskDigest, TaskSpec, TerminalPatch, TerminationAccount, TerminationAccountSnapshot, TerminationDeniedValue, TerminationDeniedWriter, TerminationInitValue, TerminationLimits, TerminationResource, ToolCallRequest, ToolChoice, type ToolContext, ToolContextSeed, ToolContract, type ToolDef, type ToolEvents, type ToolExecutor, ToolInit, type ToolRisk, ToolRuntime, type ToolSource, type ToolSourceSession, ToolsOption, TranscriptSerializationHook, type TranscriptStore, TriggerClass, TtlState, Usage, UsageLimits, UsageSlice, VerifiedRecommendation, WAIT_FOR_EVENTS_SCHEMA, WAIT_FOR_EVENTS_TOOL_NAME, WAKE_SUMMARY_RENDER_BUDGET_CHARS, WakeBudgetBlock, WakeDigest, WakeTrigger, WireError, Workflow, WorkflowCallOpts, type WorkflowEvent, type WorkflowEventBody, WorkflowRegistry, admissionReserveUsd, affordableOutputTokens, agentErrorFromWire, agentErrorToWire, agentScope, applyClaimOps, applyStructuredOutputTier, approachSigCoarse, approachSigOf, archiveDeprecatedModelOps, atCompactionThreshold, buildAbandonFold, buildAdapterRegistry, buildCostReport, buildDeriverRegistry, buildOrchestratorTools, buildTerminationInitValue, buildToolContext, canRideLoopTurn, canonicalIsolationTag, canonicalizeLadder, canonicalizeSchema, capIssues, capsHashOf, checkFloors, checkpointRefFor, childCoveragePrefix, claimExpired, claimExpiry, claimIssues, claimOpIssues, classifyAgentError, classifyAttemptOutcome, collectDeclaredLadders, compactMessages, compilePermissionChain, compilePermissionPreset, compileVerifiedLayer, costReportFromJournal, countsAgainstLimit, createCanonicalIdMinter, createCtx, createEngine, createSandboxBridge, currentOnlyKeyRing, decodeCheckpoint, defineWorkflow, deriveContentKey, deriverV1, deriverV2, digestOf, dispositionHook, emptyDigestBlocks, emptyToolset, encodeCheckpoint, entryUsageSlices, escalateTool, evaluatePermission, evaluateReuse, executeWorkflow, exhaustionCodeOf, extractCandidate, failoverTriggerOf, fallbackTriggerOf, filterClaimsForRun, finalizeFires, foldTermination, formatRePrompt, formatScopePath, hashWorkflowBody, hashWorkflowSource, identityJcs, isEscalated, isSchemaPairSpec, isStandardSchemaSpec, isStrictCompatibleSchema, kMaxOf, knowledgeHash, ladderLengthOf, ladderRungChoice, lexShellCommand, liftRetainedParts, lineageWeightOf, makeOrchestratorWorkflow, maskSecrets, maskSecretsDeep, maskSecretsJson, matchArgvPattern, matchShellCommand, mcp, mergeUsageLimits, modelEpochOf, modelKnowledgeCard, modelSpecIdentity, needsSeparateExtract, nextFailover, nodeLinkKey, normalizeApproachTag, normalizeEntry, normalizeFallbacks, orchestrate, parallelScope, parseModelRef, parseScopePath, phiInitialOf, pipelineScope, planNodeScope, priceEntryUsage, priceUsdOf, profileCard, profileRegistrySnapshotHash, projectHistory, projectIdentity, projectToJsonSchema, proposalStatement, providerOf, readTerminationInit, registryKeyRing, remeasureQueue, replayDisposition, resolveModelInvocation, resolvePricing, resolveToolset, retryClassOf, retryDelayMs, roleConfiguredInRouting, roundOneDisposition, runAgent, runProfile, scanJournalCompatibility, schemaHash, schemaHashOfSpec, selectStructuredOutputTier, shouldCompact, spawnDepthOf, summarizeInstruction, summarizeOutput, terminationConfigDrift, tierWithinCaps, toApprovalDecision, toJournalValue, tool, toolContract, toolsetHash, ttlState, validateEditorialCommit, validateEntryShape, validateEscalationLimits, validateEscalationReport, validateSchemaSpec, validateTerminationLimits, workflowScope, workflowSourceRef, wrapJournalStore, wrapTranscriptStore };
package/dist/index.js CHANGED
@@ -5054,6 +5054,8 @@ var JournalMatcher = class {
5054
5054
  byScope = /* @__PURE__ */ new Map();
5055
5055
  all = [];
5056
5056
  consumed = /* @__PURE__ */ new Set();
5057
+ /** Suspension seqs holding at least one resolution ref-entry. */
5058
+ resolvedRefs = /* @__PURE__ */ new Set();
5057
5059
  keyRing;
5058
5060
  disposition;
5059
5061
  aliasDisposition;
@@ -5069,7 +5071,10 @@ var JournalMatcher = class {
5069
5071
  this.disposition = options?.disposition ?? roundOneDisposition;
5070
5072
  const terminalsByRef = /* @__PURE__ */ new Map();
5071
5073
  for (const entry of entries) {
5072
- if (REF_ENTRY_KINDS.has(entry.kind)) continue;
5074
+ if (REF_ENTRY_KINDS.has(entry.kind)) {
5075
+ if (entry.kind === "resolution" && entry.ref !== void 0) this.resolvedRefs.add(entry.ref);
5076
+ continue;
5077
+ }
5073
5078
  if (entry.ref !== void 0) terminalsByRef.set(entry.ref, entry);
5074
5079
  }
5075
5080
  for (const entry of entries) {
@@ -5246,7 +5251,12 @@ var JournalMatcher = class {
5246
5251
  misses: this.missesInternal,
5247
5252
  skipped: this.skippedInternal,
5248
5253
  reruns: this.rerunsInternal,
5249
- orphaned: this.all.filter((op) => !this.consumed.has(op.running.seq)).map((op) => op.running.seq)
5254
+ orphaned: this.all.filter((op) => {
5255
+ if (this.consumed.has(op.running.seq) || op.terminal !== void 0) return false;
5256
+ if (op.running.status === "running") return true;
5257
+ if (op.running.status === "suspended") return !this.resolvedRefs.has(op.running.seq);
5258
+ return false;
5259
+ }).map((op) => op.running.seq)
5250
5260
  };
5251
5261
  }
5252
5262
  };
@@ -5484,6 +5494,7 @@ var Replayer = class {
5484
5494
  if (patch.usageApprox !== void 0) entry.usageApprox = patch.usageApprox;
5485
5495
  if (patch.servedBy !== void 0) entry.servedBy = patch.servedBy;
5486
5496
  if (patch.usageByModel !== void 0) entry.usageByModel = patch.usageByModel;
5497
+ if (patch.costAttribution !== void 0) entry.costAttribution = patch.costAttribution;
5487
5498
  if (patch.transcriptRef !== void 0) entry.transcriptRef = patch.transcriptRef;
5488
5499
  if (patch.checkpointRef !== void 0) entry.checkpointRef = patch.checkpointRef;
5489
5500
  if (patch.artifacts !== void 0) entry.artifacts = toJournalValue(patch.artifacts, "terminal artifacts");
@@ -6147,19 +6158,28 @@ var FileTranscriptStore = class {
6147
6158
  //#endregion
6148
6159
  //#region src/engine/cost-report.ts
6149
6160
  /**
6150
- * CostReport builders (M5-T03). Two
6151
- * sources, one shape:
6161
+ * CostReport builders (M5-T03; follow-up: one pure fold).
6162
+ *
6163
+ * `costReportFromJournal` is THE report: a pure fold over terminal
6164
+ * entries that both the engine's settle path and stored-run inspection
6165
+ * (shells, `rulvar inspect`) use, so a replayed run reports the same
6166
+ * numbers byte for byte. Terminal entries carry their attribution facts
6167
+ * (`costAttribution`: phase, agent type, primary role, budget account,
6168
+ * finalize-reserve flag) exactly so this fold can reproduce every
6169
+ * breakdown without live state; entries written before the facts
6170
+ * shipped fold under the documented fallbacks (empty phase, 'unknown'
6171
+ * agent type, role 'loop').
6152
6172
  *
6153
- * - `buildCostReport` folds the LIVE per-run attribution buckets (ctx
6154
- * accumulates byModel/byPhase/byAgentType/byRole per call) around the
6155
- * ledger-fold total, so report totals equal the budget ledger fold
6156
- * totals exactly at settle.
6157
- * - `costReportFromJournal` is the pure journal fold for STORED runs
6158
- * (shells, `rulvar inspect`): terminal usage priced per servedBy with
6159
- * abandoned subtrees contributing zero, exactly like the kernel's
6160
- * ledger fold. Phase, agentType, and role attribution are live-run
6161
- * facts that entries do not carry, so those buckets are empty here;
6162
- * byRole and the orchestrator block complete in M7 (DEF-7).
6173
+ * Inclusion policy, applied to the total and EVERY breakdown alike:
6174
+ * terminal usage exactly once, priced per serving slice, entries under
6175
+ * abandoned subtrees contribute zero (their spend is tracked separately
6176
+ * in the abandoned-spend ledger the orchestrator sees). Attempts that
6177
+ * were paid but never abandoned (a cancelled root attempt, a dangling
6178
+ * child) are real spend and stay included everywhere.
6179
+ *
6180
+ * `buildCostReport` folds the LIVE per-run attribution buckets around
6181
+ * the ledger total; it remains for hosts that accumulated their own
6182
+ * `CostAttribution`, but the engine no longer builds outcomes from it.
6163
6183
  *
6164
6184
  * Unpriced models surface in `unpriced`, never as a silent zero.
6165
6185
  */
@@ -6174,14 +6194,9 @@ const ROLES = [
6174
6194
  function emptyByRole() {
6175
6195
  return Object.fromEntries(ROLES.map((role) => [role, 0]));
6176
6196
  }
6177
- function zeroOrchestrator() {
6178
- return {
6179
- spentUsd: 0,
6180
- share: 0,
6181
- wakes: 0,
6182
- forcedFinish: false,
6183
- reserveUsedUsd: 0
6184
- };
6197
+ /** The orchestrator sub-account naming rule of makeOrchestratorWorkflow. */
6198
+ function isOrchestratorAccount(scope) {
6199
+ return scope === "orchestrator" || scope.endsWith("/orchestrator");
6185
6200
  }
6186
6201
  /** Folds the per-run attribution buckets into the normative CostReport. */
6187
6202
  function buildCostReport(attribution, totalUsd) {
@@ -6207,16 +6222,30 @@ function buildCostReport(attribution, totalUsd) {
6207
6222
  };
6208
6223
  }
6209
6224
  /**
6210
- * The pure journal fold: byModel and totals from terminal entries, the
6211
- * same summation the kernel ledger uses (terminal usage exactly once,
6212
- * priced per servedBy, abandoned subtrees contribute zero).
6225
+ * The pure journal fold: the complete CostReport from terminal entries,
6226
+ * the same summation the kernel ledger uses (terminal usage exactly
6227
+ * once, priced per servedBy slice, abandoned subtrees contribute zero).
6228
+ * The orchestrator block folds too: spend attributed to the
6229
+ * orchestrator sub-account, the reserve-funded share of it, the armed
6230
+ * wake count, and the at-cap freeze flag from the journaled cap
6231
+ * decision, so a replay-only resume reproduces the block instead of
6232
+ * reading this process's live accounts (which a replay never charges).
6213
6233
  */
6214
6234
  function costReportFromJournal(entries, priceUsd) {
6215
6235
  const abandonFold = buildAbandonFold(entries);
6216
6236
  const byModel = {};
6237
+ const byPhase = {};
6238
+ const byAgentType = {};
6239
+ const byRole = emptyByRole();
6217
6240
  const unpriced = [];
6218
6241
  let totalUsd = 0;
6242
+ let orchestratorSpentUsd = 0;
6243
+ let reserveUsedUsd = 0;
6244
+ let wakes = 0;
6245
+ let forcedFinish = false;
6219
6246
  for (const entry of entries) {
6247
+ if (entry.kind === "decision" && entry.value?.decisionType === "orchestrator_budget_cap") forcedFinish = true;
6248
+ if (entry.kind === "external" && entry.status === "suspended" && typeof entry.value?.key === "string" && (entry.value.key.startsWith("wake:") || entry.value.key.includes(":wake:"))) wakes += 1;
6220
6249
  if (entry.kind !== "resolution" && entry.kind !== "abandon" && abandonFold.isAbandoned(entry.ref ?? entry.seq)) continue;
6221
6250
  if (entry.status === "running" || entry.usage === void 0) continue;
6222
6251
  const priced = priceEntryUsage(entry, priceUsd);
@@ -6226,14 +6255,30 @@ function costReportFromJournal(entries, priceUsd) {
6226
6255
  });
6227
6256
  for (const slice of priced.priced) byModel[slice.servedBy] = (byModel[slice.servedBy] ?? 0) + slice.usd;
6228
6257
  totalUsd += priced.usd;
6258
+ const facts = entry.costAttribution;
6259
+ const phase = facts?.phase ?? "";
6260
+ byPhase[phase] = (byPhase[phase] ?? 0) + priced.usd;
6261
+ const agentType = facts?.agentType ?? "unknown";
6262
+ byAgentType[agentType] = (byAgentType[agentType] ?? 0) + priced.usd;
6263
+ byRole[facts?.role ?? "loop"] += priced.usd;
6264
+ if (facts?.budgetAccount !== void 0 && isOrchestratorAccount(facts.budgetAccount)) {
6265
+ orchestratorSpentUsd += priced.usd;
6266
+ if (facts.finalizeReserve === true) reserveUsedUsd += priced.usd;
6267
+ }
6229
6268
  }
6230
6269
  return {
6231
6270
  totalUsd,
6232
6271
  byModel,
6233
- byPhase: {},
6234
- byAgentType: {},
6235
- byRole: emptyByRole(),
6236
- orchestrator: zeroOrchestrator(),
6272
+ byPhase,
6273
+ byAgentType,
6274
+ byRole,
6275
+ orchestrator: {
6276
+ spentUsd: orchestratorSpentUsd,
6277
+ share: orchestratorSpentUsd / Math.max(totalUsd, .01),
6278
+ wakes,
6279
+ forcedFinish,
6280
+ reserveUsedUsd
6281
+ },
6237
6282
  unpriced
6238
6283
  };
6239
6284
  }
@@ -7250,7 +7295,7 @@ var NoProgressDetector = class {
7250
7295
  return this.streakInternal >= this.threshold;
7251
7296
  }
7252
7297
  describe() {
7253
- return `no-progress abort after ${this.streakInternal} consecutive turns without tool calls or artifact deltas (threshold ${this.threshold}; docs/06 Appendix A)`;
7298
+ return `no-progress abort after ${this.streakInternal} consecutive turns without tool calls or artifact deltas (threshold ${this.threshold}; https://docs.rulvar.com/guide/agents#the-agent-loop-and-turns)`;
7254
7299
  }
7255
7300
  };
7256
7301
  //#endregion
@@ -8510,6 +8555,28 @@ async function runAgent(options) {
8510
8555
  await saveBoundary();
8511
8556
  continue loop;
8512
8557
  }
8558
+ if (options.terminalTool !== void 0) {
8559
+ noProgress.recordTurn({ toolCalls: 0 });
8560
+ if (noProgress.tripped) {
8561
+ status = "limit";
8562
+ abortClass = "no-progress";
8563
+ agentError = {
8564
+ kind: "terminal",
8565
+ retryable: false
8566
+ };
8567
+ errorMessage = noProgress.describe();
8568
+ break;
8569
+ }
8570
+ messages.push({
8571
+ role: "user",
8572
+ parts: [{
8573
+ type: "text",
8574
+ text: outcome.finish?.reason === "max-tokens" ? `The turn was cut at the output token limit before any tool call. Be brief and call the '${options.terminalTool.name}' tool now; plain text is not a valid completion.` : `The turn ended without a tool call. Call the '${options.terminalTool.name}' tool to complete; plain text is not a valid completion.`
8575
+ }]
8576
+ });
8577
+ await saveBoundary();
8578
+ continue loop;
8579
+ }
8513
8580
  if (options.schema === void 0) {
8514
8581
  output = outcome.turn.text;
8515
8582
  break;
@@ -8942,8 +9009,43 @@ var RunBudget = class {
8942
9009
  controller: new AbortController()
8943
9010
  };
8944
9011
  if (options.ceilingUsd !== void 0) account.ceilingUsd = options.ceilingUsd;
9012
+ if (options.kind !== void 0) account.kind = options.kind;
8945
9013
  this.accounts.set(scope, account);
8946
9014
  }
9015
+ /**
9016
+ * The diagnostic projection behind a ceiling error: the first CLOSED
9017
+ * account (projected commitments included, exactly the layer-1
9018
+ * closure test) walking from `scope` toward the root, plus the root
9019
+ * state. 'run budget ceiling reached' under a healthy root misled the
9020
+ * v1.6.0 follow-up review's live probe when only a 0.18 USD
9021
+ * orchestrator cap had crossed under a 0.90 USD root; the message can
9022
+ * now name the account that actually ended the work. An unknown scope
9023
+ * degrades to root-only diagnostics instead of throwing: this runs on
9024
+ * the error path.
9025
+ */
9026
+ exhaustionDiagnostics(scope) {
9027
+ let chain;
9028
+ try {
9029
+ chain = this.chainOf(scope);
9030
+ } catch {
9031
+ chain = [this.root];
9032
+ }
9033
+ const crossed = chain.find((account) => account.ceilingUsd !== void 0 && account.spentUsd + account.committedReserveUsd + account.finalizeReserveUsd >= account.ceilingUsd);
9034
+ const root = this.root;
9035
+ const diagnostics = { root: {
9036
+ spentUsd: root.spentUsd,
9037
+ ...root.ceilingUsd === void 0 ? {} : { ceilingUsd: root.ceilingUsd }
9038
+ } };
9039
+ if (crossed?.ceilingUsd !== void 0) diagnostics.crossed = {
9040
+ scope: crossed.scope,
9041
+ source: crossed.scope === "run" ? "root" : crossed.kind === "orchestrator-cap" ? "orchestrator-cap" : "child-account",
9042
+ ceilingUsd: crossed.ceilingUsd,
9043
+ spentUsd: crossed.spentUsd,
9044
+ committedReserveUsd: crossed.committedReserveUsd,
9045
+ finalizeReserveUsd: crossed.finalizeReserveUsd
9046
+ };
9047
+ return diagnostics;
9048
+ }
8947
9049
  accountView(scope) {
8948
9050
  const account = this.accounts.get(scope);
8949
9051
  if (account === void 0) return;
@@ -8967,6 +9069,26 @@ var RunBudget = class {
8967
9069
  if (account?.ceilingUsd === void 0) return;
8968
9070
  return Math.max(0, account.ceilingUsd - account.spentUsd - account.committedReserveUsd - account.finalizeReserveUsd);
8969
9071
  }
9072
+ /**
9073
+ * The tightest allowance headroom on the chain of `scope`: the minimum
9074
+ * remainder across 'child-allowance' accounts. An allowance ceiling
9075
+ * bounds the child's LIFETIME spend, so projected admission must never
9076
+ * hold more than this against the chain (the layer-2 mirror lives in
9077
+ * the orchestrator admission's childCeiling clamp): a reserve above
9078
+ * the allowance would deny work that the allowance itself already
9079
+ * bounds. Undefined when no allowance account is on the chain; the
9080
+ * clamp never applies to the run root or an orchestrator cap, whose
9081
+ * headroom is shared money that projected admission must protect.
9082
+ */
9083
+ allowanceHeadroomOf(scope) {
9084
+ let headroom;
9085
+ for (const account of this.chainOf(scope)) {
9086
+ if (account.kind !== "child-allowance" || account.ceilingUsd === void 0) continue;
9087
+ const remainder = Math.max(0, account.ceilingUsd - account.spentUsd - account.committedReserveUsd - account.finalizeReserveUsd);
9088
+ headroom = headroom === void 0 ? remainder : Math.min(headroom, remainder);
9089
+ }
9090
+ return headroom;
9091
+ }
8970
9092
  /** Layer 3 ceiling signal of the run root; live streams sever through it. */
8971
9093
  get signal() {
8972
9094
  return this.root.controller.signal;
@@ -9324,6 +9446,20 @@ var AdmissionController = class {
9324
9446
  * journaled by the caller so replay re-delivers it without
9325
9447
  * re-evaluation.
9326
9448
  */
9449
+ /**
9450
+ * The reserve the DISPATCH layer will actually commit for this spec:
9451
+ * the estimate (or the flat default) clamped by the explicit child
9452
+ * budget when one exists, because only an explicit budget opens a
9453
+ * child-allowance account at dispatch; the childBudgetFraction cap
9454
+ * never materializes as an account and must not shrink the
9455
+ * projection. The token-count-priced estimate of ctx.agent is
9456
+ * unreachable here (async); a divergence there lands as a journaled
9457
+ * dispatch rejection instead of a strand.
9458
+ */
9459
+ projectedDispatchReserveUsd(spec) {
9460
+ const base = spec.estCostUsd ?? this.flatReserveUsd;
9461
+ return spec.budgetUsd === void 0 ? base : Math.min(base, spec.budgetUsd);
9462
+ }
9327
9463
  admit(spec, options) {
9328
9464
  const commitReserve = options?.commitReserve ?? true;
9329
9465
  const nodeKey = spec.nodeKey ?? spec.parentAccountScope;
@@ -9410,7 +9546,8 @@ var AdmissionController = class {
9410
9546
  }
9411
9547
  else {
9412
9548
  const remainder = this.budget.remainderOf(spec.parentAccountScope);
9413
- if (remainder !== void 0 && remainder <= 0) return {
9549
+ const projection = this.projectedDispatchReserveUsd(spec);
9550
+ if (remainder !== void 0 && (remainder <= 0 || remainder < projection + (spec.pendingReserveUsd ?? 0))) return {
9414
9551
  verdict: {
9415
9552
  kind: "reject",
9416
9553
  reason: { code: "budget" }
@@ -9791,6 +9928,13 @@ const kTerminalTool = Symbol("rulvar.terminalTool");
9791
9928
  * graft boot). Dangling redispatch checkpoints take precedence.
9792
9929
  */
9793
9930
  const kBootCheckpoint = Symbol("rulvar.bootCheckpoint");
9931
+ /**
9932
+ * Internal AgentOpts channel: marks the orchestrator forced-finish
9933
+ * dispatch, whose spend draws from the released finalize reserve
9934
+ * (DEF-7). Settlement stamps the flag into the terminal's cost
9935
+ * attribution so the journal fold reproduces reserveUsedUsd.
9936
+ */
9937
+ const kFinalizeReserve = Symbol("rulvar.finalizeReserve");
9794
9938
  /** Typed accessor used by the in-package consumers. */
9795
9939
  function runtimeOf(ctx) {
9796
9940
  const runtime = ctxRuntimes.get(ctx);
@@ -10386,7 +10530,8 @@ function createCtx(internals, rootWorkflow) {
10386
10530
  if (internals.flatReserveUsd !== void 0) reserveOptions.flatReserveUsd = internals.flatReserveUsd;
10387
10531
  const reserve = internals.pricingOf !== void 0 && internals.pricingOf(loopResolved.ref) === void 0 && opts.estCost === void 0 && profile?.estCost === void 0 ? 0 : admissionReserveUsd(reserveOptions);
10388
10532
  const budgetAccount = state.budgetScope ?? "run";
10389
- internals.budget.admitSpawn(reserve, budgetAccount);
10533
+ const allowanceHeadroomUsd = internals.budget.allowanceHeadroomOf(budgetAccount);
10534
+ internals.budget.admitSpawn(allowanceHeadroomUsd === void 0 ? reserve : Math.min(reserve, allowanceHeadroomUsd), budgetAccount);
10390
10535
  let acquired;
10391
10536
  if (typeof isolation === "object" && isolation.kind === "worktree") {
10392
10537
  const acquireInput = {
@@ -10653,6 +10798,13 @@ function createCtx(internals, rootWorkflow) {
10653
10798
  usage: result.usage,
10654
10799
  servedBy: result.servedBy,
10655
10800
  ...result.usageByModel === void 0 ? {} : { usageByModel: result.usageByModel },
10801
+ costAttribution: {
10802
+ ...state.phase === void 0 ? {} : { phase: state.phase },
10803
+ agentType,
10804
+ role: primaryRole,
10805
+ budgetAccount: state.budgetScope ?? "run",
10806
+ ...opts[kFinalizeReserve] === true ? { finalizeReserve: true } : {}
10807
+ },
10656
10808
  transcriptRef: result.transcriptRef
10657
10809
  };
10658
10810
  if (result.status === "escalated" && result.escalation !== void 0) terminalPatch.escalation = result.escalation;
@@ -10720,10 +10872,25 @@ function createCtx(internals, rootWorkflow) {
10720
10872
  bump(internals.cost.byPhase, state.phase ?? "", usd);
10721
10873
  bump(internals.cost.byAgentType, agentType, usd);
10722
10874
  internals.cost.byRole.set(primaryRole, (internals.cost.byRole.get(primaryRole) ?? 0) + usd);
10723
- if (result.error?.kind === "budget" || internals.budget.exhausted && result.status !== "ok") throw new BudgetExhaustedError("run budget ceiling reached during agent execution", { data: {
10724
- scope: state.scope,
10725
- entryRef: terminal.seq
10726
- } });
10875
+ if (result.error?.kind === "budget" || internals.budget.exhausted && result.status !== "ok") {
10876
+ const diagnostics = internals.budget.exhaustionDiagnostics(state.budgetScope ?? "run");
10877
+ const crossed = diagnostics.crossed;
10878
+ const rootSuffix = `run root: spent ${diagnostics.root.spentUsd.toFixed(4)}` + (diagnostics.root.ceilingUsd === void 0 ? " USD, no ceiling" : ` of ${diagnostics.root.ceilingUsd.toFixed(4)} USD`);
10879
+ throw new BudgetExhaustedError(crossed === void 0 || crossed.source === "root" ? "run budget ceiling reached during agent execution" : (crossed.source === "orchestrator-cap" ? "orchestrator budget cap reached during agent execution" : "budget sub-account ceiling reached during agent execution") + ` (account '${crossed.scope}': spent ${crossed.spentUsd.toFixed(4)}` + (crossed.committedReserveUsd + crossed.finalizeReserveUsd > 0 ? ` plus ${(crossed.committedReserveUsd + crossed.finalizeReserveUsd).toFixed(4)} reserved` : "") + ` of ${crossed.ceilingUsd.toFixed(4)} USD; ${rootSuffix})`, { data: {
10880
+ scope: state.scope,
10881
+ entryRef: terminal.seq,
10882
+ source: crossed?.source ?? "root",
10883
+ rootSpentUsd: diagnostics.root.spentUsd,
10884
+ ...diagnostics.root.ceilingUsd === void 0 ? {} : { rootCeilingUsd: diagnostics.root.ceilingUsd },
10885
+ ...crossed === void 0 ? {} : {
10886
+ crossedScope: crossed.scope,
10887
+ crossedCeilingUsd: crossed.ceilingUsd,
10888
+ crossedSpentUsd: crossed.spentUsd,
10889
+ crossedCommittedReserveUsd: crossed.committedReserveUsd,
10890
+ crossedFinalizeReserveUsd: crossed.finalizeReserveUsd
10891
+ }
10892
+ } });
10893
+ }
10727
10894
  if (opts.fallback !== void 0) {
10728
10895
  const trigger = fallbackTriggerOf(result);
10729
10896
  if (trigger !== void 0 && opts.fallback.on.includes(trigger)) return runFallbackAttempt(running.seq, trigger, spanId);
@@ -11008,7 +11175,10 @@ function createCtx(internals, rootWorkflow) {
11008
11175
  if (verdict.kind !== "admit") throw new ConfigError(`admission verdict '${verdict.kind}' has no producer before M7 (DEF-5)`);
11009
11176
  const reserve = verdict.reserve;
11010
11177
  const openOptions = { parentScope: budgetAccount };
11011
- if (reserve.childCeilingUsd !== void 0) openOptions.ceilingUsd = reserve.childCeilingUsd;
11178
+ if (reserve.childCeilingUsd !== void 0) {
11179
+ openOptions.ceilingUsd = reserve.childCeilingUsd;
11180
+ openOptions.kind = "child-allowance";
11181
+ }
11012
11182
  internals.budget.openAccount(childScope, openOptions);
11013
11183
  const running = danglingRunning ?? await internals.replayer.appendRunning({
11014
11184
  scope: state.scope,
@@ -11298,6 +11468,8 @@ function makeOrchestratorWorkflow(goal, opts) {
11298
11468
  const cardText = declaredLadderNames.length === 0 ? profileCard(advertisedProfiles) : `${profileCard(spawnableProfiles)}\nDeclared ladders (tier context for the knowledge card; NOT agentType values, never spawn them): ${declaredLadderNames.join(", ")}.`;
11299
11469
  const extension = opts?.extension;
11300
11470
  let orchestratorAccount;
11471
+ /** DEF-2 cap drift found in the sync prologue; emitted after boot. */
11472
+ const pendingCapDrifts = [];
11301
11473
  let capState;
11302
11474
  {
11303
11475
  const runCeiling = internals.budget.accountView(callingState.budgetScope ?? "run")?.ceilingUsd;
@@ -11306,17 +11478,56 @@ function makeOrchestratorWorkflow(goal, opts) {
11306
11478
  if (fraction > 1) throw new OrchestratorCapConfigError(`capFraction ${String(fraction)} exceeds 1.0 (opting out of the cap is explicit only, up to 1.0 inclusive)`);
11307
11479
  const fromFraction = runCeiling === void 0 ? void 0 : fraction * runCeiling;
11308
11480
  const bounds = [spec?.capUsd, fromFraction].filter((bound) => bound !== void 0);
11309
- if (extension !== void 0 && bounds.length === 0) throw new OrchestratorCapConfigError("the orchestrator cap is unresolvable: the run has no USD ceiling and no explicit budget.capUsd; PlanRunner requires a resolved effectiveCap");
11310
- if (bounds.length > 0) {
11481
+ const priorReserveDecision = internals.replayer.snapshot().find((entry) => {
11482
+ if (entry.kind !== "decision" || entry.scope !== callingState.scope) return false;
11483
+ return entry.value?.decisionType === "orchestrator_budget_reserve";
11484
+ });
11485
+ if (priorReserveDecision !== void 0) {
11486
+ const frozen = priorReserveDecision.value;
11487
+ const turnEstimateUsd = internals.flatReserveUsd ?? .5;
11488
+ const liveFinalizeReserveUsd = spec?.finalizeReserveUsd ?? (spec?.finalizeTurns ?? 2) * turnEstimateUsd;
11489
+ pendingCapDrifts.push(...bounds.length > 0 && Math.min(...bounds) !== frozen.capUsd ? [{
11490
+ field: "orchestratorCapUsd",
11491
+ frozenValue: frozen.capUsd,
11492
+ liveValue: Math.min(...bounds)
11493
+ }] : [], ...liveFinalizeReserveUsd !== frozen.finalizeReserveUsd ? [{
11494
+ field: "finalizeReserveUsd",
11495
+ frozenValue: frozen.finalizeReserveUsd,
11496
+ liveValue: liveFinalizeReserveUsd
11497
+ }] : []);
11498
+ orchestratorAccount = callingState.scope === "" ? "orchestrator" : `${callingState.scope}/orchestrator`;
11499
+ internals.budget.openAccount(orchestratorAccount, {
11500
+ parentScope: callingState.budgetScope ?? "run",
11501
+ ceilingUsd: frozen.capUsd,
11502
+ kind: "orchestrator-cap"
11503
+ });
11504
+ if (extension !== void 0) internals.budget.commitFinalizeReserve(orchestratorAccount, frozen.finalizeReserveUsd);
11505
+ capState = {
11506
+ effectiveCapUsd: frozen.capUsd,
11507
+ finalizeReserveUsd: frozen.finalizeReserveUsd,
11508
+ finalizeTurns: frozen.finalizeTurns,
11509
+ turnEstimateUsd,
11510
+ atCap: spec?.atCap ?? "finish-with-partial",
11511
+ source: frozen.source
11512
+ };
11513
+ }
11514
+ if (capState === void 0 && extension !== void 0 && bounds.length === 0) throw new OrchestratorCapConfigError("the orchestrator cap is unresolvable: the run has no USD ceiling and no explicit budget.capUsd; PlanRunner requires a resolved effectiveCap");
11515
+ if (capState === void 0 && bounds.length > 0) {
11311
11516
  const effectiveCapUsd = Math.min(...bounds);
11312
11517
  const turnEstimateUsd = internals.flatReserveUsd ?? .5;
11313
11518
  const finalizeTurns = spec?.finalizeTurns ?? 2;
11314
11519
  const finalizeReserveUsd = spec?.finalizeReserveUsd ?? finalizeTurns * turnEstimateUsd;
11315
11520
  if (extension !== void 0 && effectiveCapUsd < finalizeReserveUsd) throw new OrchestratorCapConfigError(`effectiveCap ${effectiveCapUsd.toFixed(4)} USD is below the finalize reserve ${finalizeReserveUsd.toFixed(4)} USD`);
11521
+ if (spec?.capUsd !== void 0 && spec.capFraction === void 0 && effectiveCapUsd < spec.capUsd) internals.events.emit({
11522
+ type: "log",
11523
+ level: "warn",
11524
+ msg: `orchestrator budget.capUsd ${spec.capUsd.toFixed(4)} USD is bounded to ${effectiveCapUsd.toFixed(4)} USD by the default capFraction 0.2 of the run ceiling (effectiveCap = min(capUsd, capFraction * ceiling)); pass capFraction: 1.0 to make capUsd the sole bound`
11525
+ }, callingState.spanId);
11316
11526
  orchestratorAccount = callingState.scope === "" ? "orchestrator" : `${callingState.scope}/orchestrator`;
11317
11527
  internals.budget.openAccount(orchestratorAccount, {
11318
11528
  parentScope: callingState.budgetScope ?? "run",
11319
- ceilingUsd: effectiveCapUsd
11529
+ ceilingUsd: effectiveCapUsd,
11530
+ kind: "orchestrator-cap"
11320
11531
  });
11321
11532
  if (extension !== void 0) internals.budget.commitFinalizeReserve(orchestratorAccount, finalizeReserveUsd);
11322
11533
  capState = {
@@ -11332,6 +11543,15 @@ function makeOrchestratorWorkflow(goal, opts) {
11332
11543
  const records = /* @__PURE__ */ new Map();
11333
11544
  const byOrdinal = /* @__PURE__ */ new Map();
11334
11545
  const rejectedByOrdinal = /* @__PURE__ */ new Map();
11546
+ /**
11547
+ * The journaled spec behind each recovered ordinal: the idempotent
11548
+ * re-execution guard compares it against the incoming call, because
11549
+ * after a cross-attempt resume a REGENERATED turn (the boundary
11550
+ * checkpoint predates the lost turn) may decide differently, and
11551
+ * handing it the prior ordinal's handle would bind the transcript
11552
+ * to a stranger's child.
11553
+ */
11554
+ const recoveredSpecByOrdinal = /* @__PURE__ */ new Map();
11335
11555
  let nextOrdinal = 0;
11336
11556
  let orchSeq;
11337
11557
  const deliveredNodeIds = /* @__PURE__ */ new Set();
@@ -11367,15 +11587,18 @@ function makeOrchestratorWorkflow(goal, opts) {
11367
11587
  const controller = new AbortController();
11368
11588
  const upstream = callingState.signal ?? internals.runSignal;
11369
11589
  const scope = placement?.childScope ?? childScopeOf();
11370
- if (placement !== void 0) internals.budget.openAccount(scope, {
11590
+ if (placement?.ownAccount === true) internals.budget.openAccount(scope, {
11371
11591
  parentScope: callingState.budgetScope ?? "run",
11372
- ...placement.childCeilingUsd === void 0 ? {} : { ceilingUsd: placement.childCeilingUsd }
11592
+ ...placement.childCeilingUsd === void 0 ? {} : {
11593
+ ceilingUsd: placement.childCeilingUsd,
11594
+ kind: "child-allowance"
11595
+ }
11373
11596
  });
11374
11597
  const childState = {
11375
11598
  scope,
11376
11599
  spanId: internals.spans.mint(callingState.spanId),
11377
11600
  signal: upstream === void 0 ? controller.signal : AbortSignal.any([upstream, controller.signal]),
11378
- budgetScope: placement !== void 0 ? scope : callingState.budgetScope ?? "run"
11601
+ budgetScope: placement?.ownAccount === true ? scope : callingState.budgetScope ?? "run"
11379
11602
  };
11380
11603
  let resolveHandle = () => void 0;
11381
11604
  const handlePromise = new Promise((resolve) => {
@@ -11438,6 +11661,10 @@ function makeOrchestratorWorkflow(goal, opts) {
11438
11661
  profiles: advertisedProfiles,
11439
11662
  gates: internals.defaults.gates ?? {},
11440
11663
  ...internals.budget.ceilingUsd === void 0 ? {} : { runCeilingUsd: internals.budget.ceilingUsd },
11664
+ ...capState === void 0 ? {} : {
11665
+ orchestratorCapUsd: capState.effectiveCapUsd,
11666
+ finalizeReserveUsd: capState.finalizeReserveUsd
11667
+ },
11441
11668
  mintId: createCanonicalIdMinter(),
11442
11669
  random: (key) => runtime.runInScope(callingState, () => Promise.resolve(ctx.random(key))),
11443
11670
  append: (input) => internals.replayer.appendSinglePhase({
@@ -11457,6 +11684,7 @@ function makeOrchestratorWorkflow(goal, opts) {
11457
11684
  nextOrdinal += 1;
11458
11685
  return { handle: (await dispatchChild(spec, spawnOrdinal, identity, {
11459
11686
  childScope,
11687
+ ownAccount: true,
11460
11688
  ...spec.budgetUsd === void 0 ? {} : { childCeilingUsd: spec.budgetUsd }
11461
11689
  })).handle };
11462
11690
  },
@@ -11487,33 +11715,61 @@ function makeOrchestratorWorkflow(goal, opts) {
11487
11715
  handle
11488
11716
  };
11489
11717
  };
11490
- /** Rebuilds spawn records from the journal (the crash-resume contract). */
11718
+ /**
11719
+ * True when `scope` is a root-attempt scope of THIS orchestration:
11720
+ * agentScope(callingState.scope, n) for some dispatch seq n. Nested
11721
+ * orchestrations live under their own wf: child scopes and never
11722
+ * match a foreign calling scope.
11723
+ */
11724
+ const scopeOfThisOrchestration = (scope) => {
11725
+ const prefix = callingState.scope === "" ? "" : `${callingState.scope}/`;
11726
+ return scope.startsWith(prefix) && /^agent:\d+$/.test(scope.slice(prefix.length));
11727
+ };
11728
+ /**
11729
+ * Rebuilds spawn records from the journal (the crash-resume
11730
+ * contract). Recovery is ORCHESTRATION-scoped, not attempt-scoped:
11731
+ * decisions journal at the orchestrate call's own scope, which is
11732
+ * stable across root attempts, so a rerun after a cancelled root
11733
+ * (the budget-abort shape the v1.6.0 follow-up review resumed) sees
11734
+ * every prior decision instead of re-deciding and re-paying.
11735
+ * Recovered children re-dispatch PINNED to their journaled child
11736
+ * scope: settled ones forward-match and replay for free, a dangling
11737
+ * one redispatches live (at-least-once), and a decision without a
11738
+ * dispatch entry rolls forward to a fresh dispatch.
11739
+ */
11491
11740
  const recover = async () => {
11492
- const scope = childScopeOf();
11741
+ const currentScope = childScopeOf();
11493
11742
  const admissions = internals.replayer.snapshot().filter((entry) => {
11494
- if (entry.kind !== "decision") return false;
11743
+ if (entry.kind !== "decision" || entry.scope !== callingState.scope) return false;
11495
11744
  const value = entry.value;
11496
- return value?.decisionType === "spawn-admission" && (value.origin === "spawn_agent" || value.origin === "parallel_agents") && value.orchestratorScope === scope;
11745
+ return value?.decisionType === "spawn-admission" && (value.origin === "spawn_agent" || value.origin === "parallel_agents");
11497
11746
  }).map((entry) => entry.value).sort((a, b) => a.spawnOrdinal - b.spawnOrdinal);
11498
11747
  for (const value of admissions) {
11499
11748
  nextOrdinal = Math.max(nextOrdinal, value.spawnOrdinal + 1);
11500
11749
  const decision = value.decision;
11750
+ recoveredSpecByOrdinal.set(value.spawnOrdinal, value.spec);
11501
11751
  if (decision.verdict.kind !== "admit") {
11502
11752
  rejectedByOrdinal.set(value.spawnOrdinal, decision);
11503
11753
  continue;
11504
11754
  }
11505
- admission.recoverChild(scope);
11506
- await dispatchChild(value.spec, value.spawnOrdinal, {
11755
+ admission.recoverChild(currentScope);
11756
+ const childScope = value.childScope ?? value.orchestratorScope;
11757
+ const record = await dispatchChild(value.spec, value.spawnOrdinal, {
11507
11758
  nodeId: decision.nodeId ?? "unknown",
11508
11759
  logicalTaskId: decision.verdict.lineage.logicalTaskId
11509
- });
11760
+ }, { childScope });
11761
+ const dispatched = internals.replayer.snapshot().find((entry) => entry.seq === record.handle);
11762
+ if (dispatched !== void 0) {
11763
+ for (const prior of internals.replayer.snapshot()) if (prior.kind === "agent" && prior.status === "running" && prior.seq !== record.handle && prior.scope === dispatched.scope && prior.key === dispatched.key && prior.ordinal === dispatched.ordinal && !records.has(prior.seq)) records.set(prior.seq, record);
11764
+ }
11510
11765
  }
11511
- const wakePrefix = `wake:${String(orchSeq ?? -1)}:`;
11512
11766
  for (const entry of internals.replayer.snapshot()) {
11513
11767
  if (entry.status !== "suspended" || entry.kind !== "external") continue;
11768
+ if (!scopeOfThisOrchestration(entry.scope)) continue;
11514
11769
  const payload = entry.value;
11515
- if (typeof payload?.key !== "string" || !payload.key.startsWith(wakePrefix)) continue;
11516
- wakeOrdinal = Math.max(wakeOrdinal, Number(payload.key.slice(wakePrefix.length)) + 1);
11770
+ const match = typeof payload?.key === "string" ? /^wake:\d+:(\d+)$/.exec(payload.key) : null;
11771
+ if (match === null) continue;
11772
+ wakeOrdinal = Math.max(wakeOrdinal, Number(match[1]) + 1);
11517
11773
  const suspension = internals.replayer.suspensionState(entry.seq);
11518
11774
  if (suspension.state === "resolved") markDelivered(suspension.value);
11519
11775
  }
@@ -11641,10 +11897,12 @@ function makeOrchestratorWorkflow(goal, opts) {
11641
11897
  await recoveryDone;
11642
11898
  const spawnOrdinal = nextOrdinal;
11643
11899
  nextOrdinal += 1;
11900
+ const priorSpec = recoveredSpecByOrdinal.get(spawnOrdinal);
11901
+ const specMatches = priorSpec === void 0 || priorSpec.agentType === params.agentType && priorSpec.prompt === params.prompt;
11644
11902
  const recovered = byOrdinal.get(spawnOrdinal);
11645
- if (recovered !== void 0) return { handle: recovered.handle };
11903
+ if (recovered !== void 0 && specMatches) return { handle: recovered.handle };
11646
11904
  const recoveredRejection = rejectedByOrdinal.get(spawnOrdinal);
11647
- if (recoveredRejection !== void 0) throw new AdmissionRejectedError(`admission rejected spawn ordinal ${String(spawnOrdinal)} (recovered verdict)`, { data: { decision: recoveredRejection } });
11905
+ if (recoveredRejection !== void 0 && specMatches) throw new AdmissionRejectedError(`admission rejected spawn ordinal ${String(spawnOrdinal)} (recovered verdict)`, { data: { decision: recoveredRejection } });
11648
11906
  if (opts?.maxSpawns !== void 0 && spawnOrdinal >= opts.maxSpawns) {
11649
11907
  internals.events.emit({
11650
11908
  type: "spawn:rejected",
@@ -11664,6 +11922,7 @@ function makeOrchestratorWorkflow(goal, opts) {
11664
11922
  parentAccountScope: callingState.budgetScope ?? "run",
11665
11923
  nodeKey: scope,
11666
11924
  ...params.budgetUsd === void 0 ? {} : { budgetUsd: params.budgetUsd },
11925
+ ...profile?.estCost === void 0 ? {} : { estCostUsd: profile.estCost },
11667
11926
  ...params.lineage === void 0 ? {} : { lineage: {
11668
11927
  continues: params.lineage.continues,
11669
11928
  causeRef: params.lineage.causeRef,
@@ -11827,6 +12086,12 @@ function makeOrchestratorWorkflow(goal, opts) {
11827
12086
  }
11828
12087
  };
11829
12088
  if (extension?.boot !== void 0) await extension.boot(io);
12089
+ for (const drift of pendingCapDrifts) internals.events.emit({
12090
+ type: "termination:config-drift",
12091
+ field: drift.field,
12092
+ frozenValue: drift.frozenValue,
12093
+ liveValue: drift.liveValue
12094
+ }, callingState.spanId);
11830
12095
  let kbCardText;
11831
12096
  const appendKbPin = async (decisionType, key) => {
11832
12097
  const handle = internals.knowledge;
@@ -11870,7 +12135,7 @@ function makeOrchestratorWorkflow(goal, opts) {
11870
12135
  }
11871
12136
  const fullCardText = kbCardText === void 0 ? cardText : `${cardText}\n${kbCardText}`;
11872
12137
  const reserveKey = deriverV2.deriveKey({ kind: "orchestrator-budget-reserve" });
11873
- if (extension !== void 0 && capState !== void 0 && !internals.replayer.snapshot().some((entry) => entry.kind === "decision" && entry.key === reserveKey)) {
12138
+ if (extension !== void 0 && capState !== void 0 && !internals.replayer.snapshot().some((entry) => entry.kind === "decision" && entry.scope === callingState.scope && entry.key === reserveKey)) {
11874
12139
  const initRef = internals.replayer.snapshot().find((entry) => entry.kind === "termination.init")?.seq;
11875
12140
  await internals.replayer.appendSinglePhase({
11876
12141
  scope: callingState.scope,
@@ -11902,7 +12167,11 @@ function makeOrchestratorWorkflow(goal, opts) {
11902
12167
  orchSeq = seq;
11903
12168
  recover().then(releaseRecovery, releaseRecovery);
11904
12169
  },
11905
- [kTerminalTool]: { name: FINISH_TOOL_NAME }
12170
+ [kTerminalTool]: { name: FINISH_TOOL_NAME },
12171
+ ...(() => {
12172
+ const priorCancelledRoot = internals.replayer.snapshot().filter((entry) => entry.kind === "agent" && entry.scope === callingState.scope && entry.status === "cancelled" && entry.checkpointRef !== void 0).at(-1);
12173
+ return priorCancelledRoot?.checkpointRef === void 0 ? {} : { [kBootCheckpoint]: priorCancelledRoot.checkpointRef };
12174
+ })()
11906
12175
  };
11907
12176
  const orchestratorState = { ...callingState };
11908
12177
  if (orchestratorAccount !== void 0) orchestratorState.budgetScope = orchestratorAccount;
@@ -11928,7 +12197,8 @@ function makeOrchestratorWorkflow(goal, opts) {
11928
12197
  limits: { maxTurns: capState?.finalizeTurns ?? 2 },
11929
12198
  ...capState === void 0 ? {} : { estCost: capState.finalizeReserveUsd },
11930
12199
  ...opts?.model === void 0 ? {} : { model: opts.model },
11931
- [kTerminalTool]: { name: FINISH_TOOL_NAME }
12200
+ [kTerminalTool]: { name: FINISH_TOOL_NAME },
12201
+ [kFinalizeReserve]: true
11932
12202
  };
11933
12203
  const finalState = { ...callingState };
11934
12204
  if (orchestratorAccount !== void 0) finalState.budgetScope = orchestratorAccount;
@@ -12109,14 +12379,20 @@ const detection = new AsyncLocalStorage();
12109
12379
  let globalsPatched = false;
12110
12380
  /**
12111
12381
  * Stack line 0 names the Error, line 1 this helper, line 2 the patched
12112
- * global, line 3 the caller whose provenance decides. Library code (a
12113
- * provider SDK, any installed dependency, rulvar's own published dist)
12114
- * lives under node_modules and is exempt: the guard exists for workflow
12115
- * code, which imports from node_modules but does not live there.
12382
+ * global, line 3 the caller whose provenance decides (the layout is
12383
+ * pinned by construction: this helper is only ever called by the two
12384
+ * patched globals). Two origins are exempt: installed dependencies (a
12385
+ * provider SDK, any transitive package, rulvar's own published dist),
12386
+ * which live under node_modules, and Node's own machinery (the undici
12387
+ * transport behind fetch, timers, stream internals), whose frames carry
12388
+ * `node:` specifiers and inherit the run's async context. The guard
12389
+ * exists for workflow code, which imports from both but lives in
12390
+ * neither.
12116
12391
  */
12117
12392
  function libraryCaller() {
12118
12393
  const caller = (/* @__PURE__ */ new Error()).stack?.split("\n")[3];
12119
- return caller !== void 0 && caller.includes("node_modules");
12394
+ if (caller === void 0) return false;
12395
+ return caller.includes("node_modules") || /[(\s]node:/.test(caller);
12120
12396
  }
12121
12397
  /**
12122
12398
  * Patches Date.now and Math.random ONCE per process and never restores:
@@ -12468,7 +12744,7 @@ function createEngine(options) {
12468
12744
  dropped: internals.dropped,
12469
12745
  pending,
12470
12746
  usage: ledger.usage,
12471
- cost: buildCostReport(internals.cost, ledger.usd)
12747
+ cost: costReportFromJournal(replayer.snapshot(), priceUsd)
12472
12748
  };
12473
12749
  if (value !== void 0 && (status === "ok" || status === "exhausted")) outcome.value = value;
12474
12750
  if (wireError !== void 0) outcome.error = wireError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/core",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Rulvar core: L0 contracts, journal kernel, ctx primitives, agent runtime, model router, tool system, dynamic orchestrator, InMemory and JSONL stores, event stream.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",