@smthrs/engine-store 1.0.0-rc.3 → 1.0.0-rc.4
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/cjs/EngineStore.d.ts +1 -1
- package/dist/cjs/EngineStore.js.map +1 -1
- package/dist/cjs/Inconsistency.d.ts +1 -1
- package/dist/cjs/Inconsistency.js.map +1 -1
- package/dist/cjs/PlanScheduler.js.map +1 -1
- package/dist/cjs/internal/ExecutionSnapshotRead.d.ts.map +1 -1
- package/dist/cjs/internal/ExecutionSnapshotRead.js +8 -6
- package/dist/cjs/internal/ExecutionSnapshotRead.js.map +2 -2
- package/dist/cjs/internal/RunDriver.d.ts.map +1 -1
- package/dist/cjs/internal/RunDriver.js +6 -4
- package/dist/cjs/internal/RunDriver.js.map +2 -2
- package/dist/esm/EngineStore.d.ts +1 -1
- package/dist/esm/EngineStore.js +1 -1
- package/dist/esm/EngineStore.js.map +1 -1
- package/dist/esm/Inconsistency.d.ts +1 -1
- package/dist/esm/Inconsistency.js +1 -1
- package/dist/esm/Inconsistency.js.map +1 -1
- package/dist/esm/PlanScheduler.js +1 -1
- package/dist/esm/PlanScheduler.js.map +1 -1
- package/dist/esm/internal/ExecutionSnapshotRead.d.ts.map +1 -1
- package/dist/esm/internal/ExecutionSnapshotRead.js.map +1 -1
- package/dist/esm/internal/RunDriver.d.ts.map +1 -1
- package/dist/esm/internal/RunDriver.js.map +1 -1
- package/package.json +18 -18
- package/src/EngineStore.ts +1 -1
- package/src/Inconsistency.ts +1 -1
- package/src/PlanScheduler.ts +1 -1
- package/src/internal/ExecutionSnapshotRead.ts +8 -6
- package/src/internal/RunDriver.ts +6 -4
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Governing designs: `docs/concepts/ownership-and-fencing.md`,
|
|
5
5
|
* `docs/concepts/attempts-and-replay.md`, and
|
|
6
|
-
* `
|
|
6
|
+
* `packages/smithers/flows/flow/docs/concepts/flows-and-actions.md`.
|
|
7
7
|
*
|
|
8
8
|
* @since 0.1.0
|
|
9
9
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/EngineStore.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Durable `FlowEngine.Encoded` composition over `@smthrs/journal`.\n *\n * Governing designs: `docs/concepts/ownership-and-fencing.md`,\n * `docs/concepts/attempts-and-replay.md`, and\n * `apps/site/src/content/docs/docs/concepts/flows-actions-plans.mdx`.\n *\n * @since 0.1.0\n */\n\nimport * as CapabilitySet from \"@smthrs/capability/CapabilitySet\"\nimport { Sha256 } from \"@smthrs/crypto\"\nimport type { DurableWriter } from \"@smthrs/database/DurableWriter\"\nimport { FlowEngine } from \"@smthrs/engine\"\nimport { Action, Flow, FlowRuntime } from \"@smthrs/flow\"\nimport { FileBoundary } from \"@smthrs/flow/FileBoundary\"\nimport { Journal } from \"@smthrs/journal\"\nimport { Jj } from \"@smthrs/kernel\"\nimport { AttemptStore, RunStore } from \"@smthrs/run-store\"\nimport type { Ownership } from \"@smthrs/run-store\"\nimport { CacheStore } from \"@smthrs/step-cache\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport type * as Schedule from \"effect/Schedule\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as DurableEngineState from \"./DurableEngineState.ts\"\nimport * as ActionPersistence from \"./internal/ActionPersistence.ts\"\nimport * as AttemptAdmission from \"./internal/AttemptAdmission.ts\"\nimport * as AttemptProbe from \"./internal/AttemptProbe.ts\"\nimport * as CacheAgeVerdicts from \"./internal/CacheAgeVerdicts.ts\"\nimport * as DeferredPersistence from \"./internal/DeferredPersistence.ts\"\nimport * as EngineJj from \"./internal/EngineJj.ts\"\nimport * as NodeJournal from \"./internal/NodeJournal.ts\"\nimport * as RunDriver from \"./internal/RunDriver.ts\"\nimport * as OwnerIdentity from \"./OwnerIdentity.ts\"\nimport * as ReplayOnly from \"./ReplayOnly.ts\"\nimport { RunState } from \"./RunState.ts\"\nimport * as StepBoundary from \"./StepBoundary.ts\"\nimport * as StepSandbox from \"./StepSandbox.ts\"\nimport * as WakeBus from \"./WakeBus.ts\"\nimport * as WorkspaceSandbox from \"./WorkspaceSandbox.ts\"\n\n/**\n * Why the engine itself asked a parked execution to resume, as reported to\n * {@link Options.requestResume}.\n *\n * @since 1.0.0-rc.0\n * @category models\n */\nexport type RequestedResumeReason = RunDriver.RequestedResumeReason\n\n/**\n * Engine-store construction options.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface Options {\n readonly owner: {\n readonly hostId: string\n }\n readonly journalSource: string\n /**\n * The directory a declaration path is recorded relative to.\n *\n * A node record can carry where its action was declared, and the path the\n * runtime reports is absolute. A journal is read on machines that did not\n * write it, so the absolute form is at best noise and at worst an\n * operator's home directory published into a run's history: the writer\n * strips this prefix, and omits the path entirely when it cannot.\n *\n * Defaults to the process working directory where there is one. A host\n * without a filesystem identity \u2014 a worker, a browser \u2014 passes nothing and\n * records no declaration paths at all, which is the honest answer.\n */\n readonly declarationRoot?: string | undefined\n /**\n * The revision of the tree this host read its flows out of.\n *\n * A node record can carry where its action was declared, and a path with a\n * line does not say which bytes were at that line: the tree moves, and the\n * same path after an edit is a different file. A host that can name the\n * revision it loaded from declares it here, and every recorded graph page\n * carries it, so a reader can open that file AT that revision.\n *\n * Absent by default, which is the honest answer for a host served out of\n * no version control or one that cannot name a revision holding what it\n * loaded. A reader with nothing shows no code rather than code it cannot\n * bind (D-068).\n *\n * A host that only learns the answer AFTER this store is built declares a\n * reader instead of a string, and it is asked once per recorded page. The\n * native host is one: the modules whose sites these records carry are read\n * during registration, which runs after this layer, so the revision that\n * describes them is not known when the store is composed. An answer that\n * is not a non-empty string records nothing, exactly as `undefined` does.\n */\n readonly sourceRevision?: string | (() => string | undefined) | undefined\n /**\n * Liveness arbitration consulted before this store steals a run whose lease\n * has expired. Answering `true` refuses the takeover.\n *\n * Optional. The default is `Ownership.leaseLiveness(heartbeatStaleAfter)`:\n * the owner is alive while its persisted heartbeat is younger than the\n * staleness cutoff, and gone once it is not. That is the weakest honest\n * answer and the one every host can give, so a composition that supplies\n * nothing still reclaims the runs of an owner killed with SIGKILL \u2014 the\n * steal it admits carries `lease-expired` evidence, which the run store\n * verifies against the row in the same write.\n *\n * A deployment that can say more supplies its own check and refuses the\n * takeover for longer: `Ownership.sameHostIncarnation` is the guard a local\n * `process.kill(pid, 0)` probe applies before reading `owner.pid`.\n */\n readonly isAlive?: Ownership.LivenessCheck | undefined\n /** Host routing, checked before claiming a run (including automatic wakes). */\n readonly canExecute?: ((row: RunStore.RunRow) => Effect.Effect<boolean>) | undefined\n /**\n * Read-only host admission fence, rechecked in the native activation transaction.\n * Use captured native reads and synchronous local probes only; external I/O\n * and module loading belong in `canExecute`.\n */\n readonly canActivate?: ((row: RunStore.RunRow) => Effect.Effect<boolean>) | undefined\n /**\n * Records, for a host that keeps one, that this engine has asked a parked\n * execution to resume: a durable clock fired, a durable deferred completed,\n * or a child settled under a parent that parked on it.\n *\n * A host that refuses to re-enter a parked run nobody asked for \u2014 the\n * control plane's executor does \u2014 reads the record to tell those wakes from\n * its own heartbeat sweep. Without it a run that slept never settled.\n *\n * Optional; a composition with no such host leaves it undefined.\n *\n * @since 1.0.0-rc.0\n */\n readonly requestResume?:\n | ((executionId: string, reason: RequestedResumeReason) => Effect.Effect<void>)\n | undefined\n /**\n * Redispatch policy for a durable clock whose fire failed. Defaults to\n * {@link DeferredPersistence.defaultFireRetryPolicy} \u2014 exponential from\n * 100ms, capped at 30s, forever. Same shape as the engine's\n * `suspendedRetryPolicy` option: the built-in behavior is the default, and\n * a deployment that wants a different backoff supplies one here rather than\n * patching the store.\n */\n readonly clockFireRetryPolicy?: Schedule.Schedule<unknown, unknown> | undefined\n}\n\ntype PublicRequirements =\n | AttemptStore.AttemptStore\n | CacheStore.CacheStore\n // DECIDED (2026-08-11, pending review): hashing is a construction-time\n // requirement of this composition rather than a per-call one.\n // The run driver derives each trampoline round's execution id from\n // (lineage, ordinal) with the injected SHA-256, on the coordinator's own\n // fiber rather than a caller's, so hashing is a construction-time\n // requirement of the composition (`packages/smithers/flows/engine/docs/api.md`).\n | Crypto.Crypto\n | DurableEngineState.DurableEngineState\n | DurableWriter\n | Journal.Journal\n | Jj.Jj\n | OwnerIdentity.OwnerIdentity\n | RunStore.RunStore\n | Scope.Scope\n | StepBoundary.Service\n\ntype Requirements = PublicRequirements | EngineJj.EngineJj\n\nconst isBoundaryMetadata = Schema.is(FileBoundary)\n\n/**\n * Constructs the production encoded composition.\n *\n * Registrations and active fibers are scoped in memory. Run identity, encoded\n * payload, results, deferred completions, clocks, actions, and ownership\n * are persisted by the supplied layers. Supply the same DurableWriter used\n * by the journal and run store so cancellation can retain complete failures.\n * The capability ceiling current at construction is the engine's host\n * ceiling: every run it admits records it, and every join, poll, and resume\n * is compared under it, whichever context later calls the service.\n *\n * @since 0.1.0\n * @category constructors\n */\nconst makeWithEngineJj = (\n options: Options\n): Effect.Effect<FlowRuntime.FlowRuntime[\"Service\"], never, Requirements> =>\n Effect.gen(function*() {\n const host = yield* CapabilitySet.current\n const ownerIdentity = yield* OwnerIdentity.OwnerIdentity\n const owner = yield* ownerIdentity.ownerId(options.owner.hostId)\n // One admission mutex per incarnation, shared by every dispatch this\n // store drives: `ActionPersistence.make` runs per dispatch below, so a\n // per-make default would never contend and the same-key exclusion the\n // adoption evidence rests on (issues #102, #103) would silently vanish.\n const admission = AttemptAdmission.makeUnsafe()\n // Run instances own the projection's lifetime, including nested actions.\n // A resumed instance rebuilds it without retaining completed runs here.\n const ageVerdicts = new WeakMap<FlowRuntime.FlowInstance[\"Service\"], ReturnType<typeof CacheAgeVerdicts.make>>()\n const attemptStore = yield* AttemptStore.AttemptStore\n const cacheStore = yield* CacheStore.CacheStore\n const journal = yield* Journal.Journal\n const declarationRoot = NodeJournal.hostRoot(options.declarationRoot)\n /*\n * Stated by the host or not at all; nothing here derives a revision. A\n * reader is asked where the record is written rather than here, because a\n * host that learns its revision after this layer builds would otherwise\n * be captured before it knows the answer.\n */\n const declaredRevision = options.sourceRevision\n const sourceRevision = (): string | undefined => {\n const answer = typeof declaredRevision === \"function\" ? declaredRevision() : declaredRevision\n return typeof answer === \"string\" && answer.length > 0 ? answer : undefined\n }\n const actionJj = yield* Jj.Jj\n const engineJj = yield* EngineJj.EngineJj\n const runStore = yield* RunStore.RunStore\n const forkOrigins = new WeakMap<FlowRuntime.FlowInstance[\"Service\"], ReadonlyArray<string>>()\n const keyOrigins = Effect.fnUntraced(function*(instance: FlowRuntime.FlowInstance[\"Service\"]) {\n const cached = forkOrigins.get(instance)\n if (cached !== undefined) return cached\n const row = yield* runStore.get(instance.executionId).pipe(Effect.orDie)\n const state = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(RunState))(row.stateJson).pipe(Effect.orDie)\n const origins = state.forkKeyRunIds ?? []\n forkOrigins.set(instance, origins)\n return origins\n })\n const stepBoundary = yield* StepBoundary.StepBoundary\n /**\n * Both halves of the isolated-execution lane are OPTIONAL and resolved\n * here, at composition time, for the same reason every service above is:\n * `actionExecute` runs on the engine's fiber, which does not carry the\n * store's layer context, so anything the dispatch needs has to be captured\n * now and re-provided below. A composition without a sandbox keeps the\n * pre-existing behaviour \u2014 the body runs against the host directly \u2014 and\n * one without a dispatcher journals what a transaction queued without\n * sending it.\n */\n const workspaceSandbox = yield* Effect.serviceOption(WorkspaceSandbox.WorkspaceSandbox)\n const stepSandbox = yield* Effect.serviceOption(StepSandbox.StepSandbox)\n const effectDispatcher = yield* Effect.serviceOption(WorkspaceSandbox.EffectDispatcher)\n // Resolved at composition like the sandboxes: a verifier composes an\n // engine that replays and executes nothing (`ReplayOnly`).\n const replayOnly = Option.getOrUndefined(yield* Effect.serviceOption(ReplayOnly.ReplayOnly))\n const engineState = yield* DurableEngineState.DurableEngineState\n const attemptSurvivors = engineState.attemptSurvivors\n /**\n * Resolved like the sandbox lane above: OPTIONAL, at composition time. A\n * host that provides `WakeBus.layer` shares one bus between this engine\n * and its own wake sources; a composition given none builds a private\n * bus, which is complete for the in-process seam because every wake\n * source below (driver, deferred persistence) publishes through it.\n */\n const providedWakeBus = yield* Effect.serviceOption(WakeBus.WakeBus)\n const wakeBus = Option.getOrElse(providedWakeBus, WakeBus.makeUnsafe)\n\n const engine = yield* Deferred.make<FlowRuntime.FlowRuntime[\"Service\"]>()\n const driver = yield* RunDriver.make({\n owner,\n journalSource: options.journalSource,\n isAlive: options.isAlive,\n canExecute: options.canExecute,\n canActivate: options.canActivate,\n requestResume: options.requestResume,\n engine: Deferred.await(engine),\n wakeBus\n })\n const deferred = yield* DeferredPersistence.make({\n owner,\n journalSource: options.journalSource,\n scheduleResume: (flowName, executionId, reason, sourceId) =>\n driver.scheduleResume(flowName, executionId, reason, sourceId),\n fireRetryPolicy: options.clockFireRetryPolicy\n })\n\n const actionExecute = Effect.fn(\"FlowEngine.actionExecute\")(function*(input: FlowEngine.ActionExecuteOptions) {\n const parent = yield* FlowRuntime.FlowInstance\n let cacheAgeVerdict = ageVerdicts.get(parent)\n if (cacheAgeVerdict === undefined) {\n cacheAgeVerdict = CacheAgeVerdicts.make(parent.executionId)\n ageVerdicts.set(parent, cacheAgeVerdict)\n }\n yield* Effect.annotateCurrentSpan({\n runId: parent.executionId,\n action: input.action.name,\n // `stepKey`, never `key`. `@smthrs/journal`'s redaction replaces the\n // value of any field whose name is a standalone trailing `key` word,\n // and `RedactedLogger` applies that rule to every log line this\n // package emits under a real host. A field named `key` reached the\n // operator as `[REDACTED]`, destroying the one field that says which\n // dispatch the line describes. The span carries the same name so the\n // trace and the log agree.\n stepKey: input.key,\n attempt: input.attempt,\n tier: input.tier\n })\n const instance = FlowEngine.makeInstance(\n parent.flow,\n parent.executionId\n )\n ageVerdicts.set(instance, cacheAgeVerdict)\n const flowEngine = yield* Deferred.await(engine)\n instance.interrupted = parent.interrupted\n // DECIDED (2026-08-11, pending review): the waiting classification is\n // threaded through the dispatch's instance and back, because the dispatch\n // runs under an instance of its own while `annotateWaiting` is documented\n // to reach the parked row. An implementation that declares one \u2014 `Sleep`\n // under `timer`, `WaitFor` under `event` with its wake token \u2014 writes it\n // here, so without the thread-back `RunDriver` would park on the derived\n // default and an action's declaration would be inert. It is seeded as\n // well as copied back so a body that annotated before dispatching keeps\n // its own declaration, and so the consumption `deferredResult` performs\n // on a settled wait travels out the same way (issue #42).\n const waitingBefore = parent.waiting\n instance.waiting = waitingBefore\n const logContext = {\n runId: parent.executionId,\n action: input.action.name,\n stepKey: input.key,\n attempt: input.attempt,\n tier: input.tier\n }\n yield* Effect.logTrace(\"action dispatch started\", logContext)\n // The boundary preparation runs under the dispatching parent, not the\n // action's own instance, exactly as the engine would run it itself.\n const snapshot = input.snapshot === undefined\n ? undefined\n : Effect.provideContext(input.snapshot, yield* Effect.context<FlowRuntime.FlowInstance | Crypto.Crypto>())\n const result = yield* ActionPersistence.make({\n runId: parent.executionId,\n owner,\n sourceId: options.journalSource,\n engineJj,\n execute: (actionInput) => (actionInput.action as Action.Any).executeEncoded as Effect.Effect<unknown, unknown>,\n idempotencyKey: input.action.idempotencyKey === undefined\n ? undefined\n : input.key,\n admission,\n cacheAgeVerdict,\n replayOnly\n })({\n action: input.action,\n attempt: input.attempt,\n key: input.key,\n tier: input.tier,\n ...(input.nondeterministic === undefined ? {} : { nondeterministic: input.nondeterministic }),\n ...(isBoundaryMetadata(input.metadata)\n ? { metadata: input.metadata }\n : {}),\n ...(snapshot === undefined ? {} : { snapshot })\n }).pipe(\n // This is an engine safety refusal, not a failure declared by the\n // action. Keep it on the defect channel like the numbered-retry\n // guard in FlowEngine, before the action's error codec can mask it.\n Effect.catchIf(\n (cause): cause is Action.IrreversibleRetryRequiresIdempotencyKey =>\n cause instanceof Action.IrreversibleRetryRequiresIdempotencyKey,\n // Encode the tagged fields before the generic defect codec reduces\n // an Error instance to just its name and message.\n (cause) => Effect.die(Schema.encodeSync(Action.IrreversibleRetryRequiresIdempotencyKey)(cause))\n ),\n Flow.intoResult,\n Effect.provideService(FlowRuntime.FlowInstance, instance),\n Effect.provideService(\n FlowRuntime.FlowRuntime,\n flowEngine\n ),\n Effect.provideService(AttemptStore.AttemptStore, attemptStore),\n Effect.provideService(CacheStore.CacheStore, cacheStore),\n Effect.provideService(Journal.Journal, journal),\n Effect.provideService(Jj.Jj, actionJj),\n Effect.provideService(RunStore.RunStore, runStore),\n Effect.provideService(StepBoundary.StepBoundary, stepBoundary),\n (dispatch) =>\n Option.isNone(stepSandbox)\n ? dispatch\n : Effect.provideService(dispatch, StepSandbox.StepSandbox, stepSandbox.value),\n (dispatch) =>\n Option.isNone(workspaceSandbox)\n ? dispatch\n : Effect.provideService(dispatch, WorkspaceSandbox.WorkspaceSandbox, workspaceSandbox.value),\n (dispatch) =>\n Option.isNone(effectDispatcher)\n ? dispatch\n : Effect.provideService(dispatch, WorkspaceSandbox.EffectDispatcher, effectDispatcher.value),\n Effect.ensuring(Effect.sync(() => {\n if (parent.waiting === waitingBefore) parent.waiting = instance.waiting\n }))\n )\n const outcome = result._tag === \"Complete\"\n ? result.exit._tag === \"Success\" ? \"success\" : \"failure\"\n : result._tag.toLowerCase()\n yield* Effect.annotateCurrentSpan({ outcome })\n yield* Effect.logTrace(\"action dispatch settled\", { ...logContext, outcome })\n return result\n })\n\n const encoded: FlowEngine.Encoded = {\n // Unspanned here: `driver.register` already opens the\n // `FlowEngine.register` span, and a second identical wrapper would nest\n // two same-name spans around one operation.\n register: (flow, execute) =>\n driver.register(flow, execute).pipe(\n Effect.tap(() => deferred.sweepDue(flow._tag))\n ),\n execute: driver.execute,\n poll: driver.poll,\n interrupt: driver.interrupt,\n // The driver's refusal, forwarded rather than re-implemented: the\n // durable engine has one cancellation path, so `interruptUnsafe` fails\n // with `unsafe_interrupt_unsupported` here too (the release policy).\n interruptUnsafe: driver.interruptUnsafe,\n resume: driver.resume,\n actionExecute,\n /**\n * The graph a walk drove, and how each of its nodes settled.\n *\n * Addressed by the record's own `sourceId` rather than a minted one, so\n * a resumed walk writes the identities the first walk used and the\n * journal's `(run, source, sequence)` uniqueness holds one row per node\n * instead of one per observation. A `Duplicate` receipt is that\n * collapse happening, and it is the expected answer on a resume.\n *\n * On the durable, owner-fenced channel, like every other engine record:\n * a writer that lost the run self-interrupts rather than narrating a\n * walk it no longer owns.\n */\n nodeRecordBytes: (record) =>\n Effect.map(FlowRuntime.FlowInstance, (parent) =>\n NodeJournal.encodedBytes({\n runId: parent.executionId,\n sourceId: options.journalSource,\n lineageId: FlowEngine.Lineage.root(parent.executionId),\n root: declarationRoot,\n sourceRevision: sourceRevision()\n }, record)),\n recordNode: (record) =>\n Effect.flatMap(FlowRuntime.FlowInstance, (parent) =>\n journal.emitDurable(\n NodeJournal.entry({\n runId: parent.executionId,\n sourceId: options.journalSource,\n lineageId: FlowEngine.Lineage.root(parent.executionId),\n root: declarationRoot,\n sourceRevision: sourceRevision()\n }, record),\n owner\n ).pipe(\n Effect.catch((error) => error.code === \"fence_lost\" ? Effect.interrupt : Effect.die(error)),\n Effect.asVoid\n )),\n actionReplayKey: Effect.fnUntraced(function*(derive) {\n const instance = yield* FlowRuntime.FlowInstance\n const origins = yield* keyOrigins(instance)\n if (origins.length === 0) return yield* derive(instance.executionId, false)\n // Prefer this child's own work on resume, then only ancestor keys\n // backed by attempts copied into this child. Never read parent rows.\n const probed = new Set<string>()\n for (const runId of [instance.executionId, ...origins]) {\n for (const runScoped of [true, false]) {\n const key = yield* derive(runId, runScoped)\n if (probed.has(key)) continue\n probed.add(key)\n const digest = yield* Schema.decodeUnknownEffect(Sha256)(key).pipe(Effect.orDie)\n const found = yield* AttemptProbe.probeAttempts(\n attemptStore,\n attemptSurvivors,\n instance.executionId,\n digest\n )\n if (Option.isSome(found)) return key\n }\n }\n return yield* derive(instance.executionId, true)\n }),\n // The durable schedule-to-close origin (issue #45): the first\n // attempt's persisted `startedAtMs` for the action key. It lives in\n // the same `flows_attempts` rows that already restore the attempt\n // sequence across park/resume and process death, so the engine's\n // `expirationMs` budget is measured from the true first attempt\n // instead of restarting on every process.\n actionRetryOrigin: Effect.fnUntraced(function*(input: {\n readonly key: string\n }) {\n const parent = yield* FlowRuntime.FlowInstance\n const survivors = yield* AttemptProbe.probeAttempts(\n attemptStore,\n attemptSurvivors,\n parent.executionId,\n yield* Schema.decodeUnknownEffect(Sha256)(input.key).pipe(Effect.orDie)\n )\n return Option.map(survivors, ({ earliestStartedAtMs }) => earliestStartedAtMs)\n }),\n // The durable attempt counter (issue #59): the highest contiguous\n // persisted attempt for the key, so a resumed run replays failed\n // attempts under their original numbers instead of restarting at 1 \u2014\n // the persisted failure is rethrown by `ActionPersistence`, the\n // retry decision sees the true attempt count, and the backoff ladder\n // is not re-slept.\n actionLatestAttempt: Effect.fnUntraced(function*(input: {\n readonly key: string\n }) {\n const parent = yield* FlowRuntime.FlowInstance\n const survivors = yield* AttemptProbe.probeAttempts(\n attemptStore,\n attemptSurvivors,\n parent.executionId,\n yield* Schema.decodeUnknownEffect(Sha256)(input.key).pipe(Effect.orDie)\n )\n return Option.map(survivors, ({ latest }) => latest)\n }),\n // The durable restore-before-retry handle (issue #1805): the earliest\n // boundary snapshot persisted in the key's surviving attempt rows, so a\n // restarted process restores the original pre-image before a retry.\n actionSnapshot: Effect.fnUntraced(function*(input: {\n readonly key: string\n }) {\n const parent = yield* FlowRuntime.FlowInstance\n const stepKeyDigest = yield* Schema.decodeUnknownEffect(Sha256)(input.key).pipe(Effect.orDie)\n const survivors = yield* AttemptProbe.probeAttempts(\n attemptStore,\n attemptSurvivors,\n parent.executionId,\n stepKeyDigest\n )\n if (Option.isNone(survivors)) return Option.none()\n return yield* ActionPersistence.earliestBoundarySnapshot(attemptStore, parent.executionId, stepKeyDigest, {\n earliest: survivors.value.earliestAttempt,\n latest: survivors.value.latest\n })\n }),\n deferredResult: deferred.deferredResult,\n deferredDone: deferred.deferredDone,\n deferredDoneIfWaiting: deferred.deferredDoneIfWaiting,\n scheduleClock: deferred.scheduleClock,\n // The engine races this against its suspension backoff sleep, so an\n // IN-PROCESS wake \u2014 deferred completed, clock fired, operator resume,\n // run settled \u2014 resumes the waiting caller immediately, with the\n // polling schedule as the bounded fallback. The bus is edge-triggered\n // and miss-tolerant (`WakeBus.ts`); a wake published before the caller\n // re-subscribes only costs one poll interval. Cross-process wakes ride\n // the polling schedule and the heartbeat sweeps: the bus is in-process,\n // and the journal offers no committed cross-process subscription.\n resumeSignal: (_flow, executionId) => wakeBus.awaitWake(executionId)\n }\n const service = {\n ...FlowEngine.bindHostCeiling(host, FlowEngine.makeUnsafe(encoded)),\n durability: \"durable\" as const\n }\n yield* Deferred.succeed(engine, service)\n return service\n })\n\n/**\n * Constructs the production encoded composition using the ambient `Jj` for\n * both public action access and private engine bookkeeping.\n *\n * @since 0.1.0\n * @category constructors\n */\nexport const make = (\n options: Options\n): Effect.Effect<FlowRuntime.FlowRuntime[\"Service\"], never, PublicRequirements> =>\n Effect.gen(function*() {\n const jj = yield* Jj.Jj\n return yield* makeWithEngineJj(options).pipe(\n Effect.provideService(EngineJj.EngineJj, jj)\n )\n })\n\n/**\n * Provides the durable flow engine.\n *\n * @since 0.1.0\n * @category layers\n */\nconst layerBase = (\n options: Options\n): Layer.Layer<\n FlowEngine.SnapshotBoundary | FlowRuntime.FlowRuntime,\n never,\n Requirements\n> => {\n const snapshotBoundary = Layer.effect(\n FlowEngine.SnapshotBoundary,\n Effect.map(EngineJj.EngineJj, (jj) =>\n FlowEngine.SnapshotBoundary.of({\n snapshot: Effect.fn(\"SnapshotBoundary.snapshot\")(({ key, attempt }) =>\n Effect.annotateCurrentSpan({ stepKey: key, attempt }).pipe(\n Effect.andThen(jj.snapshot(`smithers action ${key} attempt ${attempt}`)),\n Effect.orDie,\n // The commit id, never the change id: an agent's `jj squash` in the\n // step rewrites the change, and a change id would then name the\n // step's edits instead of the pre-image.\n Effect.map((snapshot) => snapshot.commitId)\n )\n ),\n restore: Effect.fn(\"SnapshotBoundary.restore\")((snapshot) =>\n Effect.annotateCurrentSpan({ snapshot: String(snapshot) }).pipe(\n Effect.andThen(jj.restore(snapshot as never)),\n Effect.orDie\n )\n ),\n diff: Effect.fn(\"SnapshotBoundary.diff\")((snapshot, { key, attempt }) =>\n Effect.annotateCurrentSpan({ stepKey: key, attempt }).pipe(\n Effect.andThen(jj.snapshot(`smithers action ${key} attempt ${attempt} settled`)),\n Effect.orDie,\n Effect.flatMap((current) => jj.diff(snapshot as never, current.commitId).pipe(Effect.orDie))\n )\n )\n }))\n )\n return Layer.merge(\n Layer.effect(FlowRuntime.FlowRuntime, makeWithEngineJj(options)),\n snapshotBoundary\n )\n}\n\n/**\n * Provides the durable engine using the ambient `Jj` for both action and\n * engine contexts. Host compositions that guard action access should use\n * {@link layerWithPrivilegedJj} instead.\n *\n * @since 0.1.0\n * @category layers\n */\nexport const layer = (\n options: Options\n): Layer.Layer<FlowEngine.SnapshotBoundary | FlowRuntime.FlowRuntime, never, PublicRequirements> =>\n layerBase(options).pipe(Layer.provide(EngineJj.layerFromJj))\n\n/**\n * Provides the durable engine with a private repository implementation for\n * engine bookkeeping while retaining ambient `Jj` for action bodies.\n *\n * @since 1.0.0\n * @category layers\n */\nexport const layerWithPrivilegedJj = <E, R>(\n options: Options,\n privilegedJj: Layer.Layer<Jj.Jj, E, R>\n): Layer.Layer<\n FlowEngine.SnapshotBoundary | FlowRuntime.FlowRuntime,\n E,\n PublicRequirements | R\n> =>\n layerBase(options).pipe(\n Layer.provide(EngineJj.layerFromJj.pipe(Layer.provide(privilegedJj)))\n )\n"],
|
|
4
|
+
"sourcesContent": ["/**\n * Durable `FlowEngine.Encoded` composition over `@smthrs/journal`.\n *\n * Governing designs: `docs/concepts/ownership-and-fencing.md`,\n * `docs/concepts/attempts-and-replay.md`, and\n * `packages/smithers/flows/flow/docs/concepts/flows-and-actions.md`.\n *\n * @since 0.1.0\n */\n\nimport * as CapabilitySet from \"@smthrs/capability/CapabilitySet\"\nimport { Sha256 } from \"@smthrs/crypto\"\nimport type { DurableWriter } from \"@smthrs/database/DurableWriter\"\nimport { FlowEngine } from \"@smthrs/engine\"\nimport { Action, Flow, FlowRuntime } from \"@smthrs/flow\"\nimport { FileBoundary } from \"@smthrs/flow/FileBoundary\"\nimport { Journal } from \"@smthrs/journal\"\nimport { Jj } from \"@smthrs/kernel\"\nimport { AttemptStore, RunStore } from \"@smthrs/run-store\"\nimport type { Ownership } from \"@smthrs/run-store\"\nimport { CacheStore } from \"@smthrs/step-cache\"\nimport type * as Crypto from \"effect/Crypto\"\nimport * as Deferred from \"effect/Deferred\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as Option from \"effect/Option\"\nimport type * as Schedule from \"effect/Schedule\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Scope from \"effect/Scope\"\nimport * as DurableEngineState from \"./DurableEngineState.ts\"\nimport * as ActionPersistence from \"./internal/ActionPersistence.ts\"\nimport * as AttemptAdmission from \"./internal/AttemptAdmission.ts\"\nimport * as AttemptProbe from \"./internal/AttemptProbe.ts\"\nimport * as CacheAgeVerdicts from \"./internal/CacheAgeVerdicts.ts\"\nimport * as DeferredPersistence from \"./internal/DeferredPersistence.ts\"\nimport * as EngineJj from \"./internal/EngineJj.ts\"\nimport * as NodeJournal from \"./internal/NodeJournal.ts\"\nimport * as RunDriver from \"./internal/RunDriver.ts\"\nimport * as OwnerIdentity from \"./OwnerIdentity.ts\"\nimport * as ReplayOnly from \"./ReplayOnly.ts\"\nimport { RunState } from \"./RunState.ts\"\nimport * as StepBoundary from \"./StepBoundary.ts\"\nimport * as StepSandbox from \"./StepSandbox.ts\"\nimport * as WakeBus from \"./WakeBus.ts\"\nimport * as WorkspaceSandbox from \"./WorkspaceSandbox.ts\"\n\n/**\n * Why the engine itself asked a parked execution to resume, as reported to\n * {@link Options.requestResume}.\n *\n * @since 1.0.0-rc.0\n * @category models\n */\nexport type RequestedResumeReason = RunDriver.RequestedResumeReason\n\n/**\n * Engine-store construction options.\n *\n * @since 0.1.0\n * @category models\n */\nexport interface Options {\n readonly owner: {\n readonly hostId: string\n }\n readonly journalSource: string\n /**\n * The directory a declaration path is recorded relative to.\n *\n * A node record can carry where its action was declared, and the path the\n * runtime reports is absolute. A journal is read on machines that did not\n * write it, so the absolute form is at best noise and at worst an\n * operator's home directory published into a run's history: the writer\n * strips this prefix, and omits the path entirely when it cannot.\n *\n * Defaults to the process working directory where there is one. A host\n * without a filesystem identity \u2014 a worker, a browser \u2014 passes nothing and\n * records no declaration paths at all, which is the honest answer.\n */\n readonly declarationRoot?: string | undefined\n /**\n * The revision of the tree this host read its flows out of.\n *\n * A node record can carry where its action was declared, and a path with a\n * line does not say which bytes were at that line: the tree moves, and the\n * same path after an edit is a different file. A host that can name the\n * revision it loaded from declares it here, and every recorded graph page\n * carries it, so a reader can open that file AT that revision.\n *\n * Absent by default, which is the honest answer for a host served out of\n * no version control or one that cannot name a revision holding what it\n * loaded. A reader with nothing shows no code rather than code it cannot\n * bind (D-068).\n *\n * A host that only learns the answer AFTER this store is built declares a\n * reader instead of a string, and it is asked once per recorded page. The\n * native host is one: the modules whose sites these records carry are read\n * during registration, which runs after this layer, so the revision that\n * describes them is not known when the store is composed. An answer that\n * is not a non-empty string records nothing, exactly as `undefined` does.\n */\n readonly sourceRevision?: string | (() => string | undefined) | undefined\n /**\n * Liveness arbitration consulted before this store steals a run whose lease\n * has expired. Answering `true` refuses the takeover.\n *\n * Optional. The default is `Ownership.leaseLiveness(heartbeatStaleAfter)`:\n * the owner is alive while its persisted heartbeat is younger than the\n * staleness cutoff, and gone once it is not. That is the weakest honest\n * answer and the one every host can give, so a composition that supplies\n * nothing still reclaims the runs of an owner killed with SIGKILL \u2014 the\n * steal it admits carries `lease-expired` evidence, which the run store\n * verifies against the row in the same write.\n *\n * A deployment that can say more supplies its own check and refuses the\n * takeover for longer: `Ownership.sameHostIncarnation` is the guard a local\n * `process.kill(pid, 0)` probe applies before reading `owner.pid`.\n */\n readonly isAlive?: Ownership.LivenessCheck | undefined\n /** Host routing, checked before claiming a run (including automatic wakes). */\n readonly canExecute?: ((row: RunStore.RunRow) => Effect.Effect<boolean>) | undefined\n /**\n * Read-only host admission fence, rechecked in the native activation transaction.\n * Use captured native reads and synchronous local probes only; external I/O\n * and module loading belong in `canExecute`.\n */\n readonly canActivate?: ((row: RunStore.RunRow) => Effect.Effect<boolean>) | undefined\n /**\n * Records, for a host that keeps one, that this engine has asked a parked\n * execution to resume: a durable clock fired, a durable deferred completed,\n * or a child settled under a parent that parked on it.\n *\n * A host that refuses to re-enter a parked run nobody asked for \u2014 the\n * control plane's executor does \u2014 reads the record to tell those wakes from\n * its own heartbeat sweep. Without it a run that slept never settled.\n *\n * Optional; a composition with no such host leaves it undefined.\n *\n * @since 1.0.0-rc.0\n */\n readonly requestResume?:\n | ((executionId: string, reason: RequestedResumeReason) => Effect.Effect<void>)\n | undefined\n /**\n * Redispatch policy for a durable clock whose fire failed. Defaults to\n * {@link DeferredPersistence.defaultFireRetryPolicy} \u2014 exponential from\n * 100ms, capped at 30s, forever. Same shape as the engine's\n * `suspendedRetryPolicy` option: the built-in behavior is the default, and\n * a deployment that wants a different backoff supplies one here rather than\n * patching the store.\n */\n readonly clockFireRetryPolicy?: Schedule.Schedule<unknown, unknown> | undefined\n}\n\ntype PublicRequirements =\n | AttemptStore.AttemptStore\n | CacheStore.CacheStore\n // DECIDED (2026-08-11, pending review): hashing is a construction-time\n // requirement of this composition rather than a per-call one.\n // The run driver derives each trampoline round's execution id from\n // (lineage, ordinal) with the injected SHA-256, on the coordinator's own\n // fiber rather than a caller's, so hashing is a construction-time\n // requirement of the composition (`packages/smithers/flows/engine/docs/api.md`).\n | Crypto.Crypto\n | DurableEngineState.DurableEngineState\n | DurableWriter\n | Journal.Journal\n | Jj.Jj\n | OwnerIdentity.OwnerIdentity\n | RunStore.RunStore\n | Scope.Scope\n | StepBoundary.Service\n\ntype Requirements = PublicRequirements | EngineJj.EngineJj\n\nconst isBoundaryMetadata = Schema.is(FileBoundary)\n\n/**\n * Constructs the production encoded composition.\n *\n * Registrations and active fibers are scoped in memory. Run identity, encoded\n * payload, results, deferred completions, clocks, actions, and ownership\n * are persisted by the supplied layers. Supply the same DurableWriter used\n * by the journal and run store so cancellation can retain complete failures.\n * The capability ceiling current at construction is the engine's host\n * ceiling: every run it admits records it, and every join, poll, and resume\n * is compared under it, whichever context later calls the service.\n *\n * @since 0.1.0\n * @category constructors\n */\nconst makeWithEngineJj = (\n options: Options\n): Effect.Effect<FlowRuntime.FlowRuntime[\"Service\"], never, Requirements> =>\n Effect.gen(function*() {\n const host = yield* CapabilitySet.current\n const ownerIdentity = yield* OwnerIdentity.OwnerIdentity\n const owner = yield* ownerIdentity.ownerId(options.owner.hostId)\n // One admission mutex per incarnation, shared by every dispatch this\n // store drives: `ActionPersistence.make` runs per dispatch below, so a\n // per-make default would never contend and the same-key exclusion the\n // adoption evidence rests on (issues #102, #103) would silently vanish.\n const admission = AttemptAdmission.makeUnsafe()\n // Run instances own the projection's lifetime, including nested actions.\n // A resumed instance rebuilds it without retaining completed runs here.\n const ageVerdicts = new WeakMap<FlowRuntime.FlowInstance[\"Service\"], ReturnType<typeof CacheAgeVerdicts.make>>()\n const attemptStore = yield* AttemptStore.AttemptStore\n const cacheStore = yield* CacheStore.CacheStore\n const journal = yield* Journal.Journal\n const declarationRoot = NodeJournal.hostRoot(options.declarationRoot)\n /*\n * Stated by the host or not at all; nothing here derives a revision. A\n * reader is asked where the record is written rather than here, because a\n * host that learns its revision after this layer builds would otherwise\n * be captured before it knows the answer.\n */\n const declaredRevision = options.sourceRevision\n const sourceRevision = (): string | undefined => {\n const answer = typeof declaredRevision === \"function\" ? declaredRevision() : declaredRevision\n return typeof answer === \"string\" && answer.length > 0 ? answer : undefined\n }\n const actionJj = yield* Jj.Jj\n const engineJj = yield* EngineJj.EngineJj\n const runStore = yield* RunStore.RunStore\n const forkOrigins = new WeakMap<FlowRuntime.FlowInstance[\"Service\"], ReadonlyArray<string>>()\n const keyOrigins = Effect.fnUntraced(function*(instance: FlowRuntime.FlowInstance[\"Service\"]) {\n const cached = forkOrigins.get(instance)\n if (cached !== undefined) return cached\n const row = yield* runStore.get(instance.executionId).pipe(Effect.orDie)\n const state = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(RunState))(row.stateJson).pipe(Effect.orDie)\n const origins = state.forkKeyRunIds ?? []\n forkOrigins.set(instance, origins)\n return origins\n })\n const stepBoundary = yield* StepBoundary.StepBoundary\n /**\n * Both halves of the isolated-execution lane are OPTIONAL and resolved\n * here, at composition time, for the same reason every service above is:\n * `actionExecute` runs on the engine's fiber, which does not carry the\n * store's layer context, so anything the dispatch needs has to be captured\n * now and re-provided below. A composition without a sandbox keeps the\n * pre-existing behaviour \u2014 the body runs against the host directly \u2014 and\n * one without a dispatcher journals what a transaction queued without\n * sending it.\n */\n const workspaceSandbox = yield* Effect.serviceOption(WorkspaceSandbox.WorkspaceSandbox)\n const stepSandbox = yield* Effect.serviceOption(StepSandbox.StepSandbox)\n const effectDispatcher = yield* Effect.serviceOption(WorkspaceSandbox.EffectDispatcher)\n // Resolved at composition like the sandboxes: a verifier composes an\n // engine that replays and executes nothing (`ReplayOnly`).\n const replayOnly = Option.getOrUndefined(yield* Effect.serviceOption(ReplayOnly.ReplayOnly))\n const engineState = yield* DurableEngineState.DurableEngineState\n const attemptSurvivors = engineState.attemptSurvivors\n /**\n * Resolved like the sandbox lane above: OPTIONAL, at composition time. A\n * host that provides `WakeBus.layer` shares one bus between this engine\n * and its own wake sources; a composition given none builds a private\n * bus, which is complete for the in-process seam because every wake\n * source below (driver, deferred persistence) publishes through it.\n */\n const providedWakeBus = yield* Effect.serviceOption(WakeBus.WakeBus)\n const wakeBus = Option.getOrElse(providedWakeBus, WakeBus.makeUnsafe)\n\n const engine = yield* Deferred.make<FlowRuntime.FlowRuntime[\"Service\"]>()\n const driver = yield* RunDriver.make({\n owner,\n journalSource: options.journalSource,\n isAlive: options.isAlive,\n canExecute: options.canExecute,\n canActivate: options.canActivate,\n requestResume: options.requestResume,\n engine: Deferred.await(engine),\n wakeBus\n })\n const deferred = yield* DeferredPersistence.make({\n owner,\n journalSource: options.journalSource,\n scheduleResume: (flowName, executionId, reason, sourceId) =>\n driver.scheduleResume(flowName, executionId, reason, sourceId),\n fireRetryPolicy: options.clockFireRetryPolicy\n })\n\n const actionExecute = Effect.fn(\"FlowEngine.actionExecute\")(function*(input: FlowEngine.ActionExecuteOptions) {\n const parent = yield* FlowRuntime.FlowInstance\n let cacheAgeVerdict = ageVerdicts.get(parent)\n if (cacheAgeVerdict === undefined) {\n cacheAgeVerdict = CacheAgeVerdicts.make(parent.executionId)\n ageVerdicts.set(parent, cacheAgeVerdict)\n }\n yield* Effect.annotateCurrentSpan({\n runId: parent.executionId,\n action: input.action.name,\n // `stepKey`, never `key`. `@smthrs/journal`'s redaction replaces the\n // value of any field whose name is a standalone trailing `key` word,\n // and `RedactedLogger` applies that rule to every log line this\n // package emits under a real host. A field named `key` reached the\n // operator as `[REDACTED]`, destroying the one field that says which\n // dispatch the line describes. The span carries the same name so the\n // trace and the log agree.\n stepKey: input.key,\n attempt: input.attempt,\n tier: input.tier\n })\n const instance = FlowEngine.makeInstance(\n parent.flow,\n parent.executionId\n )\n ageVerdicts.set(instance, cacheAgeVerdict)\n const flowEngine = yield* Deferred.await(engine)\n instance.interrupted = parent.interrupted\n // DECIDED (2026-08-11, pending review): the waiting classification is\n // threaded through the dispatch's instance and back, because the dispatch\n // runs under an instance of its own while `annotateWaiting` is documented\n // to reach the parked row. An implementation that declares one \u2014 `Sleep`\n // under `timer`, `WaitFor` under `event` with its wake token \u2014 writes it\n // here, so without the thread-back `RunDriver` would park on the derived\n // default and an action's declaration would be inert. It is seeded as\n // well as copied back so a body that annotated before dispatching keeps\n // its own declaration, and so the consumption `deferredResult` performs\n // on a settled wait travels out the same way (issue #42).\n const waitingBefore = parent.waiting\n instance.waiting = waitingBefore\n const logContext = {\n runId: parent.executionId,\n action: input.action.name,\n stepKey: input.key,\n attempt: input.attempt,\n tier: input.tier\n }\n yield* Effect.logTrace(\"action dispatch started\", logContext)\n // The boundary preparation runs under the dispatching parent, not the\n // action's own instance, exactly as the engine would run it itself.\n const snapshot = input.snapshot === undefined\n ? undefined\n : Effect.provideContext(input.snapshot, yield* Effect.context<FlowRuntime.FlowInstance | Crypto.Crypto>())\n const result = yield* ActionPersistence.make({\n runId: parent.executionId,\n owner,\n sourceId: options.journalSource,\n engineJj,\n execute: (actionInput) => (actionInput.action as Action.Any).executeEncoded as Effect.Effect<unknown, unknown>,\n idempotencyKey: input.action.idempotencyKey === undefined\n ? undefined\n : input.key,\n admission,\n cacheAgeVerdict,\n replayOnly\n })({\n action: input.action,\n attempt: input.attempt,\n key: input.key,\n tier: input.tier,\n ...(input.nondeterministic === undefined ? {} : { nondeterministic: input.nondeterministic }),\n ...(isBoundaryMetadata(input.metadata)\n ? { metadata: input.metadata }\n : {}),\n ...(snapshot === undefined ? {} : { snapshot })\n }).pipe(\n // This is an engine safety refusal, not a failure declared by the\n // action. Keep it on the defect channel like the numbered-retry\n // guard in FlowEngine, before the action's error codec can mask it.\n Effect.catchIf(\n (cause): cause is Action.IrreversibleRetryRequiresIdempotencyKey =>\n cause instanceof Action.IrreversibleRetryRequiresIdempotencyKey,\n // Encode the tagged fields before the generic defect codec reduces\n // an Error instance to just its name and message.\n (cause) => Effect.die(Schema.encodeSync(Action.IrreversibleRetryRequiresIdempotencyKey)(cause))\n ),\n Flow.intoResult,\n Effect.provideService(FlowRuntime.FlowInstance, instance),\n Effect.provideService(\n FlowRuntime.FlowRuntime,\n flowEngine\n ),\n Effect.provideService(AttemptStore.AttemptStore, attemptStore),\n Effect.provideService(CacheStore.CacheStore, cacheStore),\n Effect.provideService(Journal.Journal, journal),\n Effect.provideService(Jj.Jj, actionJj),\n Effect.provideService(RunStore.RunStore, runStore),\n Effect.provideService(StepBoundary.StepBoundary, stepBoundary),\n (dispatch) =>\n Option.isNone(stepSandbox)\n ? dispatch\n : Effect.provideService(dispatch, StepSandbox.StepSandbox, stepSandbox.value),\n (dispatch) =>\n Option.isNone(workspaceSandbox)\n ? dispatch\n : Effect.provideService(dispatch, WorkspaceSandbox.WorkspaceSandbox, workspaceSandbox.value),\n (dispatch) =>\n Option.isNone(effectDispatcher)\n ? dispatch\n : Effect.provideService(dispatch, WorkspaceSandbox.EffectDispatcher, effectDispatcher.value),\n Effect.ensuring(Effect.sync(() => {\n if (parent.waiting === waitingBefore) parent.waiting = instance.waiting\n }))\n )\n const outcome = result._tag === \"Complete\"\n ? result.exit._tag === \"Success\" ? \"success\" : \"failure\"\n : result._tag.toLowerCase()\n yield* Effect.annotateCurrentSpan({ outcome })\n yield* Effect.logTrace(\"action dispatch settled\", { ...logContext, outcome })\n return result\n })\n\n const encoded: FlowEngine.Encoded = {\n // Unspanned here: `driver.register` already opens the\n // `FlowEngine.register` span, and a second identical wrapper would nest\n // two same-name spans around one operation.\n register: (flow, execute) =>\n driver.register(flow, execute).pipe(\n Effect.tap(() => deferred.sweepDue(flow._tag))\n ),\n execute: driver.execute,\n poll: driver.poll,\n interrupt: driver.interrupt,\n // The driver's refusal, forwarded rather than re-implemented: the\n // durable engine has one cancellation path, so `interruptUnsafe` fails\n // with `unsafe_interrupt_unsupported` here too (the release policy).\n interruptUnsafe: driver.interruptUnsafe,\n resume: driver.resume,\n actionExecute,\n /**\n * The graph a walk drove, and how each of its nodes settled.\n *\n * Addressed by the record's own `sourceId` rather than a minted one, so\n * a resumed walk writes the identities the first walk used and the\n * journal's `(run, source, sequence)` uniqueness holds one row per node\n * instead of one per observation. A `Duplicate` receipt is that\n * collapse happening, and it is the expected answer on a resume.\n *\n * On the durable, owner-fenced channel, like every other engine record:\n * a writer that lost the run self-interrupts rather than narrating a\n * walk it no longer owns.\n */\n nodeRecordBytes: (record) =>\n Effect.map(FlowRuntime.FlowInstance, (parent) =>\n NodeJournal.encodedBytes({\n runId: parent.executionId,\n sourceId: options.journalSource,\n lineageId: FlowEngine.Lineage.root(parent.executionId),\n root: declarationRoot,\n sourceRevision: sourceRevision()\n }, record)),\n recordNode: (record) =>\n Effect.flatMap(FlowRuntime.FlowInstance, (parent) =>\n journal.emitDurable(\n NodeJournal.entry({\n runId: parent.executionId,\n sourceId: options.journalSource,\n lineageId: FlowEngine.Lineage.root(parent.executionId),\n root: declarationRoot,\n sourceRevision: sourceRevision()\n }, record),\n owner\n ).pipe(\n Effect.catch((error) => error.code === \"fence_lost\" ? Effect.interrupt : Effect.die(error)),\n Effect.asVoid\n )),\n actionReplayKey: Effect.fnUntraced(function*(derive) {\n const instance = yield* FlowRuntime.FlowInstance\n const origins = yield* keyOrigins(instance)\n if (origins.length === 0) return yield* derive(instance.executionId, false)\n // Prefer this child's own work on resume, then only ancestor keys\n // backed by attempts copied into this child. Never read parent rows.\n const probed = new Set<string>()\n for (const runId of [instance.executionId, ...origins]) {\n for (const runScoped of [true, false]) {\n const key = yield* derive(runId, runScoped)\n if (probed.has(key)) continue\n probed.add(key)\n const digest = yield* Schema.decodeUnknownEffect(Sha256)(key).pipe(Effect.orDie)\n const found = yield* AttemptProbe.probeAttempts(\n attemptStore,\n attemptSurvivors,\n instance.executionId,\n digest\n )\n if (Option.isSome(found)) return key\n }\n }\n return yield* derive(instance.executionId, true)\n }),\n // The durable schedule-to-close origin (issue #45): the first\n // attempt's persisted `startedAtMs` for the action key. It lives in\n // the same `flows_attempts` rows that already restore the attempt\n // sequence across park/resume and process death, so the engine's\n // `expirationMs` budget is measured from the true first attempt\n // instead of restarting on every process.\n actionRetryOrigin: Effect.fnUntraced(function*(input: {\n readonly key: string\n }) {\n const parent = yield* FlowRuntime.FlowInstance\n const survivors = yield* AttemptProbe.probeAttempts(\n attemptStore,\n attemptSurvivors,\n parent.executionId,\n yield* Schema.decodeUnknownEffect(Sha256)(input.key).pipe(Effect.orDie)\n )\n return Option.map(survivors, ({ earliestStartedAtMs }) => earliestStartedAtMs)\n }),\n // The durable attempt counter (issue #59): the highest contiguous\n // persisted attempt for the key, so a resumed run replays failed\n // attempts under their original numbers instead of restarting at 1 \u2014\n // the persisted failure is rethrown by `ActionPersistence`, the\n // retry decision sees the true attempt count, and the backoff ladder\n // is not re-slept.\n actionLatestAttempt: Effect.fnUntraced(function*(input: {\n readonly key: string\n }) {\n const parent = yield* FlowRuntime.FlowInstance\n const survivors = yield* AttemptProbe.probeAttempts(\n attemptStore,\n attemptSurvivors,\n parent.executionId,\n yield* Schema.decodeUnknownEffect(Sha256)(input.key).pipe(Effect.orDie)\n )\n return Option.map(survivors, ({ latest }) => latest)\n }),\n // The durable restore-before-retry handle (issue #1805): the earliest\n // boundary snapshot persisted in the key's surviving attempt rows, so a\n // restarted process restores the original pre-image before a retry.\n actionSnapshot: Effect.fnUntraced(function*(input: {\n readonly key: string\n }) {\n const parent = yield* FlowRuntime.FlowInstance\n const stepKeyDigest = yield* Schema.decodeUnknownEffect(Sha256)(input.key).pipe(Effect.orDie)\n const survivors = yield* AttemptProbe.probeAttempts(\n attemptStore,\n attemptSurvivors,\n parent.executionId,\n stepKeyDigest\n )\n if (Option.isNone(survivors)) return Option.none()\n return yield* ActionPersistence.earliestBoundarySnapshot(attemptStore, parent.executionId, stepKeyDigest, {\n earliest: survivors.value.earliestAttempt,\n latest: survivors.value.latest\n })\n }),\n deferredResult: deferred.deferredResult,\n deferredDone: deferred.deferredDone,\n deferredDoneIfWaiting: deferred.deferredDoneIfWaiting,\n scheduleClock: deferred.scheduleClock,\n // The engine races this against its suspension backoff sleep, so an\n // IN-PROCESS wake \u2014 deferred completed, clock fired, operator resume,\n // run settled \u2014 resumes the waiting caller immediately, with the\n // polling schedule as the bounded fallback. The bus is edge-triggered\n // and miss-tolerant (`WakeBus.ts`); a wake published before the caller\n // re-subscribes only costs one poll interval. Cross-process wakes ride\n // the polling schedule and the heartbeat sweeps: the bus is in-process,\n // and the journal offers no committed cross-process subscription.\n resumeSignal: (_flow, executionId) => wakeBus.awaitWake(executionId)\n }\n const service = {\n ...FlowEngine.bindHostCeiling(host, FlowEngine.makeUnsafe(encoded)),\n durability: \"durable\" as const\n }\n yield* Deferred.succeed(engine, service)\n return service\n })\n\n/**\n * Constructs the production encoded composition using the ambient `Jj` for\n * both public action access and private engine bookkeeping.\n *\n * @since 0.1.0\n * @category constructors\n */\nexport const make = (\n options: Options\n): Effect.Effect<FlowRuntime.FlowRuntime[\"Service\"], never, PublicRequirements> =>\n Effect.gen(function*() {\n const jj = yield* Jj.Jj\n return yield* makeWithEngineJj(options).pipe(\n Effect.provideService(EngineJj.EngineJj, jj)\n )\n })\n\n/**\n * Provides the durable flow engine.\n *\n * @since 0.1.0\n * @category layers\n */\nconst layerBase = (\n options: Options\n): Layer.Layer<\n FlowEngine.SnapshotBoundary | FlowRuntime.FlowRuntime,\n never,\n Requirements\n> => {\n const snapshotBoundary = Layer.effect(\n FlowEngine.SnapshotBoundary,\n Effect.map(EngineJj.EngineJj, (jj) =>\n FlowEngine.SnapshotBoundary.of({\n snapshot: Effect.fn(\"SnapshotBoundary.snapshot\")(({ key, attempt }) =>\n Effect.annotateCurrentSpan({ stepKey: key, attempt }).pipe(\n Effect.andThen(jj.snapshot(`smithers action ${key} attempt ${attempt}`)),\n Effect.orDie,\n // The commit id, never the change id: an agent's `jj squash` in the\n // step rewrites the change, and a change id would then name the\n // step's edits instead of the pre-image.\n Effect.map((snapshot) => snapshot.commitId)\n )\n ),\n restore: Effect.fn(\"SnapshotBoundary.restore\")((snapshot) =>\n Effect.annotateCurrentSpan({ snapshot: String(snapshot) }).pipe(\n Effect.andThen(jj.restore(snapshot as never)),\n Effect.orDie\n )\n ),\n diff: Effect.fn(\"SnapshotBoundary.diff\")((snapshot, { key, attempt }) =>\n Effect.annotateCurrentSpan({ stepKey: key, attempt }).pipe(\n Effect.andThen(jj.snapshot(`smithers action ${key} attempt ${attempt} settled`)),\n Effect.orDie,\n Effect.flatMap((current) => jj.diff(snapshot as never, current.commitId).pipe(Effect.orDie))\n )\n )\n }))\n )\n return Layer.merge(\n Layer.effect(FlowRuntime.FlowRuntime, makeWithEngineJj(options)),\n snapshotBoundary\n )\n}\n\n/**\n * Provides the durable engine using the ambient `Jj` for both action and\n * engine contexts. Host compositions that guard action access should use\n * {@link layerWithPrivilegedJj} instead.\n *\n * @since 0.1.0\n * @category layers\n */\nexport const layer = (\n options: Options\n): Layer.Layer<FlowEngine.SnapshotBoundary | FlowRuntime.FlowRuntime, never, PublicRequirements> =>\n layerBase(options).pipe(Layer.provide(EngineJj.layerFromJj))\n\n/**\n * Provides the durable engine with a private repository implementation for\n * engine bookkeeping while retaining ambient `Jj` for action bodies.\n *\n * @since 1.0.0\n * @category layers\n */\nexport const layerWithPrivilegedJj = <E, R>(\n options: Options,\n privilegedJj: Layer.Layer<Jj.Jj, E, R>\n): Layer.Layer<\n FlowEngine.SnapshotBoundary | FlowRuntime.FlowRuntime,\n E,\n PublicRequirements | R\n> =>\n layerBase(options).pipe(\n Layer.provide(EngineJj.layerFromJj.pipe(Layer.provide(privilegedJj)))\n )\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* The core default is strict so an invariant violation cannot pass silently.
|
|
15
15
|
*
|
|
16
16
|
* Governing designs: `docs/concepts/attempts-and-replay.md` and
|
|
17
|
-
* `
|
|
17
|
+
* `docs/concepts/cache-admission.md`.
|
|
18
18
|
*
|
|
19
19
|
* @since 0.1.0
|
|
20
20
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/Inconsistency.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Receiver for detected engine inconsistencies.\n *\n * Modeled on Skyframe's `GraphInconsistencyReceiver`: the store layer detects\n * an invariant violation \u2014 today, a content-addressed cache key that produced\n * a different result than the recorded row \u2014 and this service decides whether\n * the run fails or tolerates it. Either way the conflict is journaled, so the\n * detector is never wired to /dev/null.\n *\n * The `note` signature deliberately matches the plugin spec's\n * `cacheInconsistency` hook (`(event) => Effect<InconsistencyVerdict>`); the\n * strict layer is the future default plugin.\n *\n * The core default is strict so an invariant violation cannot pass silently.\n *\n * Governing designs: `docs/concepts/attempts-and-replay.md` and\n * `
|
|
4
|
+
"sourcesContent": ["/**\n * Receiver for detected engine inconsistencies.\n *\n * Modeled on Skyframe's `GraphInconsistencyReceiver`: the store layer detects\n * an invariant violation \u2014 today, a content-addressed cache key that produced\n * a different result than the recorded row \u2014 and this service decides whether\n * the run fails or tolerates it. Either way the conflict is journaled, so the\n * detector is never wired to /dev/null.\n *\n * The `note` signature deliberately matches the plugin spec's\n * `cacheInconsistency` hook (`(event) => Effect<InconsistencyVerdict>`); the\n * strict layer is the future default plugin.\n *\n * The core default is strict so an invariant violation cannot pass silently.\n *\n * Governing designs: `docs/concepts/attempts-and-replay.md` and\n * `docs/concepts/cache-admission.md`.\n *\n * @since 0.1.0\n */\n\nimport { FlowEngine } from \"@smthrs/engine\"\nimport { Journal } from \"@smthrs/journal\"\nimport type { Ownership } from \"@smthrs/run-store\"\nimport type { CacheStore } from \"@smthrs/step-cache\"\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport * as JournalRecords from \"./internal/JournalRecords.ts\"\n\n/**\n * Decision returned by an inconsistency receiver.\n *\n * @category models\n * @since 0.1.0\n */\nexport type InconsistencyVerdict = \"fail\" | \"tolerate\"\n\n/**\n * A content-addressed cache key that produced a result different from the\n * recorded row \u2014 a hermeticity/determinism violation.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface CacheConflict {\n readonly key: string\n readonly existing: CacheStore.CacheEntry | undefined\n readonly attempted: CacheStore.CacheEntry\n}\n\n/**\n * Recorded boundary evidence whose blob no longer hashes to its recorded\n * digest \u2014 on-disk corruption of the content-addressed store, detected while\n * replaying a verified cache hit or a succeeded attempt (issue #150).\n * Distinct from a transient host failure, which merely refuses the replay\n * and stays retryable.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface BlobCorruption {\n /** The run that observed the corruption; the journaling target. */\n readonly runId: string\n readonly keyDigest: string\n readonly path: string\n readonly recordedDigest: string\n readonly measuredDigest: string\n /**\n * Provenance of the corrupt evidence when it came from a shared cache row;\n * absent when it came from this run's own succeeded attempt row.\n */\n readonly recordedRunId?: string | undefined\n readonly recordedEventSeq?: number | undefined\n}\n\n/**\n * Inconsistency receiver operations.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Service {\n readonly note: (event: CacheConflict) => Effect.Effect<InconsistencyVerdict, Journal.JournalError>\n /**\n * Observes on-disk corruption of recorded boundary evidence (issue #150).\n * `\"fail\"` fails the dispatch; `\"tolerate\"` lets it fall back to a real\n * execution (which re-captures and heals the corrupt address).\n */\n readonly noteCorruption: (event: BlobCorruption) => Effect.Effect<InconsistencyVerdict, Journal.JournalError>\n}\n\n/**\n * Service tag for the engine inconsistency receiver.\n *\n * @category services\n * @since 0.1.0\n */\nexport class Inconsistency extends Context.Service<Inconsistency, Service>()(\"@smthrs/engine-store/Inconsistency\") {}\n\n/**\n * Options for constructing a journaling inconsistency receiver.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface MakeOptions {\n readonly journal: Journal.Service\n readonly verdict: InconsistencyVerdict\n /**\n * The owner the conflict record is fenced to: a reclaimed run cannot append\n * hermeticity evidence it no longer owns. Required \u2014 every composer of this\n * receiver is a run's owner.\n */\n readonly owner: Ownership.OwnerId\n}\n\n/**\n * Builds a receiver that journals every conflict under the run that attempted\n * the write and returns a fixed verdict.\n *\n * The record goes through the journal's durable channel: a cache conflict is\n * hermeticity evidence, and a `tolerate` verdict that silently dropped its\n * only record would wire the detector to /dev/null (issue #10).\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = (options: MakeOptions): Service => ({\n note: Effect.fn(\"Inconsistency.note\")((event) =>\n Effect.as(\n options.journal.emitDurable(\n JournalRecords.cacheConflict(\n {\n runId: event.attempted.recordedRunId,\n sourceId: \"flows/engine-store/inconsistency\",\n lineageId: FlowEngine.Lineage.root(event.attempted.recordedRunId)\n },\n {\n // `@smthrs/journal` redacts the credential word `key`. A durable\n // content identity must use the specific, non-credential name.\n cacheKey: event.key,\n verdict: options.verdict,\n existing: event.existing === undefined ? null : {\n recordedRunId: event.existing.recordedRunId,\n recordedEventSeq: event.existing.recordedEventSeq,\n createdAtMs: event.existing.createdAtMs\n },\n attempted: {\n recordedRunId: event.attempted.recordedRunId,\n recordedEventSeq: event.attempted.recordedEventSeq,\n createdAtMs: event.attempted.createdAtMs\n }\n }\n ),\n options.owner\n ),\n options.verdict\n )\n ),\n noteCorruption: Effect.fn(\"Inconsistency.noteCorruption\")((event) =>\n Effect.as(\n options.journal.emitDurable(\n JournalRecords.cacheCorruption(\n {\n runId: event.runId,\n lineageId: FlowEngine.Lineage.root(event.runId),\n // The producer identity is the corruption evidence plus the\n // recorded row generation (issues #156, #172): a caller\n // re-observing the same corrupt row re-emits an exact producer\n // duplicate the journal collapses, while an identically corrupt\n // row recorded after eviction and healing has new provenance and\n // lands as a distinct incident. Attempt-row evidence has no cache\n // provenance, so its explicit local markers retain same-row\n // convergence. A record already landed by another lineage under\n // this identity surfaces as an idempotency conflict; either way\n // evidence for that row generation exists exactly once.\n sourceId:\n `flows/engine-store/inconsistency:corruption:${event.keyDigest}:${event.path}:${event.recordedDigest}:${event.measuredDigest}:${\n event.recordedRunId ?? \"local\"\n }:${event.recordedEventSeq ?? \"local\"}`,\n sourceSeq: 0\n },\n {\n keyDigest: event.keyDigest,\n verdict: options.verdict,\n path: event.path,\n recordedDigest: event.recordedDigest,\n measuredDigest: event.measuredDigest,\n recordedRunId: event.recordedRunId ?? null,\n recordedEventSeq: event.recordedEventSeq ?? null\n }\n ),\n options.owner\n ).pipe(\n Effect.catch((error) => error.code === \"idempotency_conflict\" ? Effect.succeed(undefined) : Effect.fail(error))\n ),\n options.verdict\n )\n )\n})\n\n/**\n * Builds a receiver from overrides. The default notes nothing and tolerates.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service => ({\n note: Effect.fn(\"Inconsistency.note\")(() => Effect.succeed(\"tolerate\" as const)),\n noteCorruption: Effect.fn(\"Inconsistency.noteCorruption\")(() => Effect.succeed(\"tolerate\" as const)),\n ...overrides\n})\n\n/**\n * Provides a receiver that never journals and always tolerates.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<Inconsistency> =>\n Layer.succeed(Inconsistency)(makeNoop(overrides))\n\n/**\n * Journals every cache conflict and fails the run. This is the default\n * receiver for engine wiring: a non-hermetic sealed hard-boundary action is\n * a defect, not a condition to paper over.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerStrict = (owner: Ownership.OwnerId): Layer.Layer<Inconsistency, never, Journal.Journal> =>\n Layer.effect(Inconsistency)(\n Effect.gen(function*() {\n const journal = yield* Journal.Journal\n return make({ journal, verdict: \"fail\", owner })\n })\n )\n\n/**\n * Journals every cache conflict and continues, preserving the first-recorded\n * row (Skyframe's tolerant production configuration).\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerTolerant = (owner: Ownership.OwnerId): Layer.Layer<Inconsistency, never, Journal.Journal> =>\n Layer.effect(Inconsistency)(\n Effect.gen(function*() {\n const journal = yield* Journal.Journal\n return make({ journal, verdict: \"tolerate\", owner })\n })\n )\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|