omk-agent-core 0.98.1 → 0.98.3
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/CHANGELOG.md +644 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +3 -5
- package/dist/agent.js.map +1 -1
- package/dist/effects/effect-journal.d.ts +43 -0
- package/dist/effects/effect-journal.d.ts.map +1 -0
- package/dist/effects/effect-journal.js +186 -0
- package/dist/effects/effect-journal.js.map +1 -0
- package/dist/effects/effect-recovery.d.ts +70 -0
- package/dist/effects/effect-recovery.d.ts.map +1 -0
- package/dist/effects/effect-recovery.js +120 -0
- package/dist/effects/effect-recovery.js.map +1 -0
- package/dist/effects/effect-transitions.d.ts +34 -0
- package/dist/effects/effect-transitions.d.ts.map +1 -0
- package/dist/effects/effect-transitions.js +148 -0
- package/dist/effects/effect-transitions.js.map +1 -0
- package/dist/effects/effect-types.d.ts +135 -0
- package/dist/effects/effect-types.d.ts.map +1 -0
- package/dist/effects/effect-types.js +32 -0
- package/dist/effects/effect-types.js.map +1 -0
- package/dist/harness/abort-delivery.d.ts +26 -0
- package/dist/harness/abort-delivery.d.ts.map +1 -0
- package/dist/harness/abort-delivery.js +36 -0
- package/dist/harness/abort-delivery.js.map +1 -0
- package/dist/harness/agent-harness.d.ts +38 -6
- package/dist/harness/agent-harness.d.ts.map +1 -1
- package/dist/harness/agent-harness.js +327 -325
- package/dist/harness/agent-harness.js.map +1 -1
- package/dist/harness/canonical-digest.d.ts +32 -0
- package/dist/harness/canonical-digest.d.ts.map +1 -0
- package/dist/harness/canonical-digest.js +164 -0
- package/dist/harness/canonical-digest.js.map +1 -0
- package/dist/harness/compaction/operation.d.ts +7 -0
- package/dist/harness/compaction/operation.d.ts.map +1 -1
- package/dist/harness/compaction/operation.js.map +1 -1
- package/dist/harness/deferred-commands.d.ts +53 -0
- package/dist/harness/deferred-commands.d.ts.map +1 -0
- package/dist/harness/deferred-commands.js +96 -0
- package/dist/harness/deferred-commands.js.map +1 -0
- package/dist/harness/harness-session.d.ts +5 -58
- package/dist/harness/harness-session.d.ts.map +1 -1
- package/dist/harness/harness-session.js +15 -18
- package/dist/harness/harness-session.js.map +1 -1
- package/dist/harness/operation-lifecycle-controller.d.ts +68 -0
- package/dist/harness/operation-lifecycle-controller.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-controller.js +199 -0
- package/dist/harness/operation-lifecycle-controller.js.map +1 -0
- package/dist/harness/operation-lifecycle-reducer.d.ts +19 -0
- package/dist/harness/operation-lifecycle-reducer.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-reducer.js +201 -0
- package/dist/harness/operation-lifecycle-reducer.js.map +1 -0
- package/dist/harness/operation-lifecycle-types.d.ts +130 -0
- package/dist/harness/operation-lifecycle-types.d.ts.map +1 -0
- package/dist/harness/operation-lifecycle-types.js +34 -0
- package/dist/harness/operation-lifecycle-types.js.map +1 -0
- package/dist/harness/operation-outcome.d.ts +79 -0
- package/dist/harness/operation-outcome.d.ts.map +1 -0
- package/dist/harness/operation-outcome.js +167 -0
- package/dist/harness/operation-outcome.js.map +1 -0
- package/dist/harness/operation-trace-divergence.d.ts +60 -0
- package/dist/harness/operation-trace-divergence.d.ts.map +1 -0
- package/dist/harness/operation-trace-divergence.js +199 -0
- package/dist/harness/operation-trace-divergence.js.map +1 -0
- package/dist/harness/operation-trace.d.ts +134 -0
- package/dist/harness/operation-trace.d.ts.map +1 -0
- package/dist/harness/operation-trace.js +161 -0
- package/dist/harness/operation-trace.js.map +1 -0
- package/dist/harness/session-write-coordinator.d.ts +76 -0
- package/dist/harness/session-write-coordinator.d.ts.map +1 -0
- package/dist/harness/session-write-coordinator.js +126 -0
- package/dist/harness/session-write-coordinator.js.map +1 -0
- package/dist/harness/subscriber-fanout.d.ts +50 -0
- package/dist/harness/subscriber-fanout.d.ts.map +1 -0
- package/dist/harness/subscriber-fanout.js +92 -0
- package/dist/harness/subscriber-fanout.js.map +1 -0
- package/dist/harness/tree-navigation.d.ts +45 -0
- package/dist/harness/tree-navigation.d.ts.map +1 -0
- package/dist/harness/tree-navigation.js +59 -0
- package/dist/harness/tree-navigation.js.map +1 -0
- package/dist/harness/types.d.ts +34 -3
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/listener-delivery.d.ts +22 -0
- package/dist/listener-delivery.d.ts.map +1 -0
- package/dist/listener-delivery.js +36 -0
- package/dist/listener-delivery.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-lifecycle-controller.d.ts","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAKH,OAAO,KAAK,EACX,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,uBAAuB,EACvB,4BAA4B,EAC5B,qBAAqB,EAErB,oBAAoB,EACpB,uBAAuB,EACvB,mBAAmB,EACnB,MAAM,gCAAgC,CAAC;AAExC,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,SAAS,EAAE,mBAAmB,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,mGAAmG;IACnG,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,uBAAuB,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC;AA4BD,qBAAa,4BAA4B;IACxC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA+B;IACpD,OAAO,CAAC,KAAK,CAA+E;IAC5F,OAAO,CAAC,OAAO,CAAC,CAAkB;IAClC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IAErD,YAAY,IAAI,EAAE,4BAA4B,EAE7C;IAED,WAAW,IAAI,QAAQ,CAAC,qBAAqB,CAAC,CAE7C;IAED,mBAAmB,IAAI,mBAAmB,GAAG,SAAS,CAErD;IAED,KAAK,CAAC,IAAI,EAAE,oBAAoB,GAAG,cAAc,CAmBhD;IAED,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,OAAO,CAAC,uBAAuB,EAAE;QAAE,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,OAAO,CAAC,GAAG,IAAI,CAGzG;IAED,YAAY,CAAC,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,GAAG,YAAY,CAarF;IAED,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,qBAAqB,GAAG,IAAI,CAIhG;IAED,kGAAkG;IAClG,YAAY,IAAI,mBAAmB,CAWlC;IAED;;;;OAIG;IACG,MAAM,CACX,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,uBAAuB,EAChC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAC3B,OAAO,CAAC,uBAAuB,CAAC,CA0BlC;IAED,2EAA2E;IAC3E,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,qBAAqB,EAAE,CAW3E;IAED,qEAAqE;IACrE,iBAAiB,CAAC,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,GAAG,qBAAqB,CAOrF;IAEK,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC,CAKjC;IAED,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,cAAc;CAGtB","sourcesContent":["/**\n * Owns the single mutable copy of the harness lifecycle state.\n *\n * The controller is the only writer: it builds commands, runs them through\n * the pure reducer, and performs the side effects the reducer is forbidden\n * from touching — clock reads, ID allocation, abort-signal delivery, and\n * settlement of the operation's `settled` promise.\n *\n * Lease discipline: every mutating method requires the `OperationLease`\n * returned by `begin`. A lease that is not the current record fails closed,\n * so a stale async continuation from a finished operation can never mutate\n * the state of a newer one. `settle()` is the only path from `active` to\n * `idle`, runs the caller's finalizer inside the `settling` barrier, and is\n * guarded against double invocation at both controller and reducer level.\n *\n * The `settled` promise resolves with the recorded outcome and never\n * rejects; finalizer failures surface only through the `settle()` rejection,\n * so callers cannot strand an unhandled rejection by ignoring `lease.settled`.\n */\n\nimport { createImmutableSnapshot } from \"../plain-data.ts\";\nimport { AgentHarnessError } from \"./errors.ts\";\nimport { initialHarnessLifecycleState, reduceHarnessLifecycle } from \"./operation-lifecycle-reducer.ts\";\nimport type {\n\tHarnessAttemptOutcome,\n\tHarnessAttemptRef,\n\tHarnessAttemptSummary,\n\tHarnessLifecycleCommand,\n\tHarnessLifecycleDependencies,\n\tHarnessLifecycleState,\n\tHarnessLifecycleViolation,\n\tHarnessOperationKind,\n\tHarnessOperationOutcome,\n\tHarnessOperationRef,\n} from \"./operation-lifecycle-types.ts\";\n\nexport interface OperationLease {\n\treadonly operation: HarnessOperationRef;\n\treadonly signal: AbortSignal;\n\t/** Resolves once with the recorded outcome when the operation finishes settling. Never rejects. */\n\treadonly settled: Promise<HarnessOperationOutcome>;\n}\n\nexport interface AttemptLease {\n\treadonly attempt: HarnessAttemptRef;\n\t/** The owning operation's signal. Attempts never own a separate AbortController. */\n\treadonly signal: AbortSignal;\n}\n\nexport interface HarnessAbortCapture {\n\treadonly target?: OperationLease;\n\treadonly signalDelivered: boolean;\n}\n\ninterface OperationRecord {\n\treadonly lease: OperationLease;\n\treadonly controller: AbortController;\n\treadonly attemptFinishTimes: Map<string, number>;\n\tsettleStarted: boolean;\n\tresolveSettled: (outcome: HarnessOperationOutcome) => void;\n}\n\n/**\n * Deep-freeze a reducer-produced state in place.\n *\n * Operation and attempt refs are already frozen at creation, and the reducer\n * allocates fresh state objects per transition, so this is O(attempts) per\n * transition rather than a clone. That keeps `getSnapshot()` free to hand out\n * the live reference: `readonly` is compile-time only, but a frozen object\n * resists `Reflect.set` from extension code at runtime too.\n */\nfunction freezeLifecycleState(state: HarnessLifecycleState): HarnessLifecycleState {\n\tif (state.tag !== \"idle\") {\n\t\tfor (const settled of state.attempts) Object.freeze(settled);\n\t\tObject.freeze(state.attempts);\n\t}\n\tif (state.tag === \"settling\") Object.freeze(state.outcome);\n\treturn Object.freeze(state);\n}\n\nexport class OperationLifecycleController {\n\tprivate readonly deps: HarnessLifecycleDependencies;\n\tprivate state: HarnessLifecycleState = freezeLifecycleState(initialHarnessLifecycleState());\n\tprivate current?: OperationRecord;\n\tprivate readonly idleWaiters = new Set<() => void>();\n\n\tconstructor(deps: HarnessLifecycleDependencies) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetSnapshot(): Readonly<HarnessLifecycleState> {\n\t\treturn this.state;\n\t}\n\n\tgetCurrentOperation(): HarnessOperationRef | undefined {\n\t\treturn this.state.tag === \"idle\" ? undefined : this.state.operation;\n\t}\n\n\tbegin(kind: HarnessOperationKind): OperationLease {\n\t\tconst sequence = this.state.tag === \"idle\" ? this.state.lastSequence + 1 : -1;\n\t\t// Frozen once here so every downstream copy — lease, getters, and event\n\t\t// payloads — shares one identity no listener can rewrite.\n\t\tconst operation = createImmutableSnapshot<HarnessOperationRef>({\n\t\t\toperationId: this.deps.createOperationId(),\n\t\t\tsequence,\n\t\t\tkind,\n\t\t\tstartedAtMs: this.deps.now(),\n\t\t});\n\t\tthis.apply({ type: \"begin\", operation });\n\t\tconst controller = new AbortController();\n\t\tlet resolveSettled: (outcome: HarnessOperationOutcome) => void = () => undefined;\n\t\tconst settled = new Promise<HarnessOperationOutcome>((resolve) => {\n\t\t\tresolveSettled = resolve;\n\t\t});\n\t\tconst lease: OperationLease = { operation, signal: controller.signal, settled };\n\t\tthis.current = { lease, controller, attemptFinishTimes: new Map(), settleStarted: false, resolveSettled };\n\t\treturn lease;\n\t}\n\n\tsetStage(lease: OperationLease, stage: Extract<HarnessLifecycleCommand, { type: \"stage\" }>[\"stage\"]): void {\n\t\tthis.expectCurrentLease(lease);\n\t\tthis.apply({ type: \"stage\", operationId: lease.operation.operationId, stage });\n\t}\n\n\tbeginAttempt(lease: OperationLease, reason: HarnessAttemptRef[\"reason\"]): AttemptLease {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tconst active = this.expectActive();\n\t\tconst attempt = createImmutableSnapshot<HarnessAttemptRef>({\n\t\t\toperationId: lease.operation.operationId,\n\t\t\tattemptId: `${lease.operation.operationId}:a${active.attempts.length}`,\n\t\t\tindex: active.attempts.length,\n\t\t\treason,\n\t\t\tstartedAtMs: this.deps.now(),\n\t\t});\n\t\tthis.apply({ type: \"attempt_begin\", attempt });\n\t\trecord.attemptFinishTimes.delete(attempt.attemptId);\n\t\treturn { attempt, signal: lease.signal };\n\t}\n\n\tfinishAttempt(lease: OperationLease, attempt: AttemptLease, outcome: HarnessAttemptOutcome): void {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tthis.apply({ type: \"attempt_end\", attemptId: attempt.attempt.attemptId, outcome });\n\t\trecord.attemptFinishTimes.set(attempt.attempt.attemptId, this.deps.now());\n\t}\n\n\t/** Captures the current operation as the abort target; never targets operations started later. */\n\trequestAbort(): HarnessAbortCapture {\n\t\tconst record = this.current;\n\t\tif (record === undefined || this.state.tag === \"idle\") {\n\t\t\treturn { signalDelivered: false };\n\t\t}\n\t\tif (this.state.tag === \"settling\") {\n\t\t\treturn { target: record.lease, signalDelivered: false };\n\t\t}\n\t\tthis.apply({ type: \"abort_request\", operationId: record.lease.operation.operationId });\n\t\tif (!record.controller.signal.aborted) record.controller.abort();\n\t\treturn { target: record.lease, signalDelivered: true };\n\t}\n\n\t/**\n\t * Sole active -> idle path. Runs `finalize` inside the settling barrier, then\n\t * releases state and resolves the lease. A finalizer failure still completes\n\t * the state release and resolves `lease.settled`, then rejects this call.\n\t */\n\tasync settle(\n\t\tlease: OperationLease,\n\t\toutcome: HarnessOperationOutcome,\n\t\tfinalize: () => Promise<void>,\n\t): Promise<HarnessOperationOutcome> {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tif (record.settleStarted) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Operation ${lease.operation.operationId} is already settling`);\n\t\t}\n\t\t// A rejected settle_begin (e.g. an attempt still open) is not a settlement.\n\t\t// Marking it as one would wedge the operation: every later settle() would\n\t\t// report \"already settling\" while the state never left `active`, and\n\t\t// `lease.settled` plus every waitForIdle() waiter would hang forever.\n\t\tthis.apply({ type: \"settle_begin\", operationId: lease.operation.operationId, outcome });\n\t\trecord.settleStarted = true;\n\t\tlet finalizeError: unknown;\n\t\ttry {\n\t\t\tawait finalize();\n\t\t} catch (error) {\n\t\t\tfinalizeError = error;\n\t\t}\n\t\tthis.apply({ type: \"settle_finish\", operationId: lease.operation.operationId });\n\t\tthis.current = undefined;\n\t\trecord.resolveSettled(outcome);\n\t\tfor (const release of [...this.idleWaiters]) {\n\t\t\tthis.idleWaiters.delete(release);\n\t\t\trelease();\n\t\t}\n\t\tif (finalizeError !== undefined) throw finalizeError;\n\t\treturn outcome;\n\t}\n\n\t/** Bounded attempt summaries for the current operation; no transcripts. */\n\tgetAttemptSummaries(lease: OperationLease): readonly HarnessAttemptSummary[] {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tconst attempts = this.state.tag === \"idle\" ? [] : this.state.attempts;\n\t\treturn attempts.map(({ attempt, outcome }) => ({\n\t\t\tattemptId: attempt.attemptId,\n\t\t\tindex: attempt.index,\n\t\t\treason: attempt.reason,\n\t\t\toutcome,\n\t\t\tstartedAtMs: attempt.startedAtMs,\n\t\t\tfinishedAtMs: record.attemptFinishTimes.get(attempt.attemptId) ?? attempt.startedAtMs,\n\t\t}));\n\t}\n\n\t/** The recorded summary for one attempt of the current operation. */\n\tgetAttemptSummary(lease: OperationLease, attempt: AttemptLease): HarnessAttemptSummary {\n\t\tconst attemptId = attempt.attempt.attemptId;\n\t\tconst summary = this.getAttemptSummaries(lease).find((candidate) => candidate.attemptId === attemptId);\n\t\tif (summary === undefined) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `No summary recorded for attempt ${attemptId}`);\n\t\t}\n\t\treturn summary;\n\t}\n\n\tasync waitForIdle(): Promise<void> {\n\t\tif (this.state.tag === \"idle\") return;\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tthis.idleWaiters.add(resolve);\n\t\t});\n\t}\n\n\tprivate expectActive(): Extract<HarnessLifecycleState, { tag: \"active\" }> {\n\t\tif (this.state.tag !== \"active\") {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Expected an active operation, state is ${this.state.tag}`);\n\t\t}\n\t\treturn this.state;\n\t}\n\n\tprivate expectCurrentLease(lease: OperationLease): OperationRecord {\n\t\tconst record = this.current;\n\t\tif (record === undefined || record.lease !== lease) {\n\t\t\tthrow new AgentHarnessError(\n\t\t\t\t\"invalid_state\",\n\t\t\t\t`Stale operation lease for ${lease.operation.operationId}; the operation no longer owns lifecycle state`,\n\t\t\t);\n\t\t}\n\t\treturn record;\n\t}\n\n\tprivate apply(command: HarnessLifecycleCommand): void {\n\t\tconst next = reduceHarnessLifecycle(this.state, command);\n\t\tif (!next.ok) throw this.asHarnessError(next.error);\n\t\tthis.state = freezeLifecycleState(next.value);\n\t}\n\n\tprivate asHarnessError(violation: HarnessLifecycleViolation): AgentHarnessError {\n\t\treturn new AgentHarnessError(violation.code === \"busy\" ? \"busy\" : \"invalid_state\", violation.message, violation);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns the single mutable copy of the harness lifecycle state.
|
|
3
|
+
*
|
|
4
|
+
* The controller is the only writer: it builds commands, runs them through
|
|
5
|
+
* the pure reducer, and performs the side effects the reducer is forbidden
|
|
6
|
+
* from touching — clock reads, ID allocation, abort-signal delivery, and
|
|
7
|
+
* settlement of the operation's `settled` promise.
|
|
8
|
+
*
|
|
9
|
+
* Lease discipline: every mutating method requires the `OperationLease`
|
|
10
|
+
* returned by `begin`. A lease that is not the current record fails closed,
|
|
11
|
+
* so a stale async continuation from a finished operation can never mutate
|
|
12
|
+
* the state of a newer one. `settle()` is the only path from `active` to
|
|
13
|
+
* `idle`, runs the caller's finalizer inside the `settling` barrier, and is
|
|
14
|
+
* guarded against double invocation at both controller and reducer level.
|
|
15
|
+
*
|
|
16
|
+
* The `settled` promise resolves with the recorded outcome and never
|
|
17
|
+
* rejects; finalizer failures surface only through the `settle()` rejection,
|
|
18
|
+
* so callers cannot strand an unhandled rejection by ignoring `lease.settled`.
|
|
19
|
+
*/
|
|
20
|
+
import { createImmutableSnapshot } from "../plain-data.js";
|
|
21
|
+
import { AgentHarnessError } from "./errors.js";
|
|
22
|
+
import { initialHarnessLifecycleState, reduceHarnessLifecycle } from "./operation-lifecycle-reducer.js";
|
|
23
|
+
/**
|
|
24
|
+
* Deep-freeze a reducer-produced state in place.
|
|
25
|
+
*
|
|
26
|
+
* Operation and attempt refs are already frozen at creation, and the reducer
|
|
27
|
+
* allocates fresh state objects per transition, so this is O(attempts) per
|
|
28
|
+
* transition rather than a clone. That keeps `getSnapshot()` free to hand out
|
|
29
|
+
* the live reference: `readonly` is compile-time only, but a frozen object
|
|
30
|
+
* resists `Reflect.set` from extension code at runtime too.
|
|
31
|
+
*/
|
|
32
|
+
function freezeLifecycleState(state) {
|
|
33
|
+
if (state.tag !== "idle") {
|
|
34
|
+
for (const settled of state.attempts)
|
|
35
|
+
Object.freeze(settled);
|
|
36
|
+
Object.freeze(state.attempts);
|
|
37
|
+
}
|
|
38
|
+
if (state.tag === "settling")
|
|
39
|
+
Object.freeze(state.outcome);
|
|
40
|
+
return Object.freeze(state);
|
|
41
|
+
}
|
|
42
|
+
export class OperationLifecycleController {
|
|
43
|
+
deps;
|
|
44
|
+
state = freezeLifecycleState(initialHarnessLifecycleState());
|
|
45
|
+
current;
|
|
46
|
+
idleWaiters = new Set();
|
|
47
|
+
constructor(deps) {
|
|
48
|
+
this.deps = deps;
|
|
49
|
+
}
|
|
50
|
+
getSnapshot() {
|
|
51
|
+
return this.state;
|
|
52
|
+
}
|
|
53
|
+
getCurrentOperation() {
|
|
54
|
+
return this.state.tag === "idle" ? undefined : this.state.operation;
|
|
55
|
+
}
|
|
56
|
+
begin(kind) {
|
|
57
|
+
const sequence = this.state.tag === "idle" ? this.state.lastSequence + 1 : -1;
|
|
58
|
+
// Frozen once here so every downstream copy — lease, getters, and event
|
|
59
|
+
// payloads — shares one identity no listener can rewrite.
|
|
60
|
+
const operation = createImmutableSnapshot({
|
|
61
|
+
operationId: this.deps.createOperationId(),
|
|
62
|
+
sequence,
|
|
63
|
+
kind,
|
|
64
|
+
startedAtMs: this.deps.now(),
|
|
65
|
+
});
|
|
66
|
+
this.apply({ type: "begin", operation });
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
let resolveSettled = () => undefined;
|
|
69
|
+
const settled = new Promise((resolve) => {
|
|
70
|
+
resolveSettled = resolve;
|
|
71
|
+
});
|
|
72
|
+
const lease = { operation, signal: controller.signal, settled };
|
|
73
|
+
this.current = { lease, controller, attemptFinishTimes: new Map(), settleStarted: false, resolveSettled };
|
|
74
|
+
return lease;
|
|
75
|
+
}
|
|
76
|
+
setStage(lease, stage) {
|
|
77
|
+
this.expectCurrentLease(lease);
|
|
78
|
+
this.apply({ type: "stage", operationId: lease.operation.operationId, stage });
|
|
79
|
+
}
|
|
80
|
+
beginAttempt(lease, reason) {
|
|
81
|
+
const record = this.expectCurrentLease(lease);
|
|
82
|
+
const active = this.expectActive();
|
|
83
|
+
const attempt = createImmutableSnapshot({
|
|
84
|
+
operationId: lease.operation.operationId,
|
|
85
|
+
attemptId: `${lease.operation.operationId}:a${active.attempts.length}`,
|
|
86
|
+
index: active.attempts.length,
|
|
87
|
+
reason,
|
|
88
|
+
startedAtMs: this.deps.now(),
|
|
89
|
+
});
|
|
90
|
+
this.apply({ type: "attempt_begin", attempt });
|
|
91
|
+
record.attemptFinishTimes.delete(attempt.attemptId);
|
|
92
|
+
return { attempt, signal: lease.signal };
|
|
93
|
+
}
|
|
94
|
+
finishAttempt(lease, attempt, outcome) {
|
|
95
|
+
const record = this.expectCurrentLease(lease);
|
|
96
|
+
this.apply({ type: "attempt_end", attemptId: attempt.attempt.attemptId, outcome });
|
|
97
|
+
record.attemptFinishTimes.set(attempt.attempt.attemptId, this.deps.now());
|
|
98
|
+
}
|
|
99
|
+
/** Captures the current operation as the abort target; never targets operations started later. */
|
|
100
|
+
requestAbort() {
|
|
101
|
+
const record = this.current;
|
|
102
|
+
if (record === undefined || this.state.tag === "idle") {
|
|
103
|
+
return { signalDelivered: false };
|
|
104
|
+
}
|
|
105
|
+
if (this.state.tag === "settling") {
|
|
106
|
+
return { target: record.lease, signalDelivered: false };
|
|
107
|
+
}
|
|
108
|
+
this.apply({ type: "abort_request", operationId: record.lease.operation.operationId });
|
|
109
|
+
if (!record.controller.signal.aborted)
|
|
110
|
+
record.controller.abort();
|
|
111
|
+
return { target: record.lease, signalDelivered: true };
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Sole active -> idle path. Runs `finalize` inside the settling barrier, then
|
|
115
|
+
* releases state and resolves the lease. A finalizer failure still completes
|
|
116
|
+
* the state release and resolves `lease.settled`, then rejects this call.
|
|
117
|
+
*/
|
|
118
|
+
async settle(lease, outcome, finalize) {
|
|
119
|
+
const record = this.expectCurrentLease(lease);
|
|
120
|
+
if (record.settleStarted) {
|
|
121
|
+
throw new AgentHarnessError("invalid_state", `Operation ${lease.operation.operationId} is already settling`);
|
|
122
|
+
}
|
|
123
|
+
// A rejected settle_begin (e.g. an attempt still open) is not a settlement.
|
|
124
|
+
// Marking it as one would wedge the operation: every later settle() would
|
|
125
|
+
// report "already settling" while the state never left `active`, and
|
|
126
|
+
// `lease.settled` plus every waitForIdle() waiter would hang forever.
|
|
127
|
+
this.apply({ type: "settle_begin", operationId: lease.operation.operationId, outcome });
|
|
128
|
+
record.settleStarted = true;
|
|
129
|
+
let finalizeError;
|
|
130
|
+
try {
|
|
131
|
+
await finalize();
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
finalizeError = error;
|
|
135
|
+
}
|
|
136
|
+
this.apply({ type: "settle_finish", operationId: lease.operation.operationId });
|
|
137
|
+
this.current = undefined;
|
|
138
|
+
record.resolveSettled(outcome);
|
|
139
|
+
for (const release of [...this.idleWaiters]) {
|
|
140
|
+
this.idleWaiters.delete(release);
|
|
141
|
+
release();
|
|
142
|
+
}
|
|
143
|
+
if (finalizeError !== undefined)
|
|
144
|
+
throw finalizeError;
|
|
145
|
+
return outcome;
|
|
146
|
+
}
|
|
147
|
+
/** Bounded attempt summaries for the current operation; no transcripts. */
|
|
148
|
+
getAttemptSummaries(lease) {
|
|
149
|
+
const record = this.expectCurrentLease(lease);
|
|
150
|
+
const attempts = this.state.tag === "idle" ? [] : this.state.attempts;
|
|
151
|
+
return attempts.map(({ attempt, outcome }) => ({
|
|
152
|
+
attemptId: attempt.attemptId,
|
|
153
|
+
index: attempt.index,
|
|
154
|
+
reason: attempt.reason,
|
|
155
|
+
outcome,
|
|
156
|
+
startedAtMs: attempt.startedAtMs,
|
|
157
|
+
finishedAtMs: record.attemptFinishTimes.get(attempt.attemptId) ?? attempt.startedAtMs,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/** The recorded summary for one attempt of the current operation. */
|
|
161
|
+
getAttemptSummary(lease, attempt) {
|
|
162
|
+
const attemptId = attempt.attempt.attemptId;
|
|
163
|
+
const summary = this.getAttemptSummaries(lease).find((candidate) => candidate.attemptId === attemptId);
|
|
164
|
+
if (summary === undefined) {
|
|
165
|
+
throw new AgentHarnessError("invalid_state", `No summary recorded for attempt ${attemptId}`);
|
|
166
|
+
}
|
|
167
|
+
return summary;
|
|
168
|
+
}
|
|
169
|
+
async waitForIdle() {
|
|
170
|
+
if (this.state.tag === "idle")
|
|
171
|
+
return;
|
|
172
|
+
await new Promise((resolve) => {
|
|
173
|
+
this.idleWaiters.add(resolve);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
expectActive() {
|
|
177
|
+
if (this.state.tag !== "active") {
|
|
178
|
+
throw new AgentHarnessError("invalid_state", `Expected an active operation, state is ${this.state.tag}`);
|
|
179
|
+
}
|
|
180
|
+
return this.state;
|
|
181
|
+
}
|
|
182
|
+
expectCurrentLease(lease) {
|
|
183
|
+
const record = this.current;
|
|
184
|
+
if (record === undefined || record.lease !== lease) {
|
|
185
|
+
throw new AgentHarnessError("invalid_state", `Stale operation lease for ${lease.operation.operationId}; the operation no longer owns lifecycle state`);
|
|
186
|
+
}
|
|
187
|
+
return record;
|
|
188
|
+
}
|
|
189
|
+
apply(command) {
|
|
190
|
+
const next = reduceHarnessLifecycle(this.state, command);
|
|
191
|
+
if (!next.ok)
|
|
192
|
+
throw this.asHarnessError(next.error);
|
|
193
|
+
this.state = freezeLifecycleState(next.value);
|
|
194
|
+
}
|
|
195
|
+
asHarnessError(violation) {
|
|
196
|
+
return new AgentHarnessError(violation.code === "busy" ? "busy" : "invalid_state", violation.message, violation);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=operation-lifecycle-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-lifecycle-controller.js","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Owns the single mutable copy of the harness lifecycle state.\n *\n * The controller is the only writer: it builds commands, runs them through\n * the pure reducer, and performs the side effects the reducer is forbidden\n * from touching — clock reads, ID allocation, abort-signal delivery, and\n * settlement of the operation's `settled` promise.\n *\n * Lease discipline: every mutating method requires the `OperationLease`\n * returned by `begin`. A lease that is not the current record fails closed,\n * so a stale async continuation from a finished operation can never mutate\n * the state of a newer one. `settle()` is the only path from `active` to\n * `idle`, runs the caller's finalizer inside the `settling` barrier, and is\n * guarded against double invocation at both controller and reducer level.\n *\n * The `settled` promise resolves with the recorded outcome and never\n * rejects; finalizer failures surface only through the `settle()` rejection,\n * so callers cannot strand an unhandled rejection by ignoring `lease.settled`.\n */\n\nimport { createImmutableSnapshot } from \"../plain-data.ts\";\nimport { AgentHarnessError } from \"./errors.ts\";\nimport { initialHarnessLifecycleState, reduceHarnessLifecycle } from \"./operation-lifecycle-reducer.ts\";\nimport type {\n\tHarnessAttemptOutcome,\n\tHarnessAttemptRef,\n\tHarnessAttemptSummary,\n\tHarnessLifecycleCommand,\n\tHarnessLifecycleDependencies,\n\tHarnessLifecycleState,\n\tHarnessLifecycleViolation,\n\tHarnessOperationKind,\n\tHarnessOperationOutcome,\n\tHarnessOperationRef,\n} from \"./operation-lifecycle-types.ts\";\n\nexport interface OperationLease {\n\treadonly operation: HarnessOperationRef;\n\treadonly signal: AbortSignal;\n\t/** Resolves once with the recorded outcome when the operation finishes settling. Never rejects. */\n\treadonly settled: Promise<HarnessOperationOutcome>;\n}\n\nexport interface AttemptLease {\n\treadonly attempt: HarnessAttemptRef;\n\t/** The owning operation's signal. Attempts never own a separate AbortController. */\n\treadonly signal: AbortSignal;\n}\n\nexport interface HarnessAbortCapture {\n\treadonly target?: OperationLease;\n\treadonly signalDelivered: boolean;\n}\n\ninterface OperationRecord {\n\treadonly lease: OperationLease;\n\treadonly controller: AbortController;\n\treadonly attemptFinishTimes: Map<string, number>;\n\tsettleStarted: boolean;\n\tresolveSettled: (outcome: HarnessOperationOutcome) => void;\n}\n\n/**\n * Deep-freeze a reducer-produced state in place.\n *\n * Operation and attempt refs are already frozen at creation, and the reducer\n * allocates fresh state objects per transition, so this is O(attempts) per\n * transition rather than a clone. That keeps `getSnapshot()` free to hand out\n * the live reference: `readonly` is compile-time only, but a frozen object\n * resists `Reflect.set` from extension code at runtime too.\n */\nfunction freezeLifecycleState(state: HarnessLifecycleState): HarnessLifecycleState {\n\tif (state.tag !== \"idle\") {\n\t\tfor (const settled of state.attempts) Object.freeze(settled);\n\t\tObject.freeze(state.attempts);\n\t}\n\tif (state.tag === \"settling\") Object.freeze(state.outcome);\n\treturn Object.freeze(state);\n}\n\nexport class OperationLifecycleController {\n\tprivate readonly deps: HarnessLifecycleDependencies;\n\tprivate state: HarnessLifecycleState = freezeLifecycleState(initialHarnessLifecycleState());\n\tprivate current?: OperationRecord;\n\tprivate readonly idleWaiters = new Set<() => void>();\n\n\tconstructor(deps: HarnessLifecycleDependencies) {\n\t\tthis.deps = deps;\n\t}\n\n\tgetSnapshot(): Readonly<HarnessLifecycleState> {\n\t\treturn this.state;\n\t}\n\n\tgetCurrentOperation(): HarnessOperationRef | undefined {\n\t\treturn this.state.tag === \"idle\" ? undefined : this.state.operation;\n\t}\n\n\tbegin(kind: HarnessOperationKind): OperationLease {\n\t\tconst sequence = this.state.tag === \"idle\" ? this.state.lastSequence + 1 : -1;\n\t\t// Frozen once here so every downstream copy — lease, getters, and event\n\t\t// payloads — shares one identity no listener can rewrite.\n\t\tconst operation = createImmutableSnapshot<HarnessOperationRef>({\n\t\t\toperationId: this.deps.createOperationId(),\n\t\t\tsequence,\n\t\t\tkind,\n\t\t\tstartedAtMs: this.deps.now(),\n\t\t});\n\t\tthis.apply({ type: \"begin\", operation });\n\t\tconst controller = new AbortController();\n\t\tlet resolveSettled: (outcome: HarnessOperationOutcome) => void = () => undefined;\n\t\tconst settled = new Promise<HarnessOperationOutcome>((resolve) => {\n\t\t\tresolveSettled = resolve;\n\t\t});\n\t\tconst lease: OperationLease = { operation, signal: controller.signal, settled };\n\t\tthis.current = { lease, controller, attemptFinishTimes: new Map(), settleStarted: false, resolveSettled };\n\t\treturn lease;\n\t}\n\n\tsetStage(lease: OperationLease, stage: Extract<HarnessLifecycleCommand, { type: \"stage\" }>[\"stage\"]): void {\n\t\tthis.expectCurrentLease(lease);\n\t\tthis.apply({ type: \"stage\", operationId: lease.operation.operationId, stage });\n\t}\n\n\tbeginAttempt(lease: OperationLease, reason: HarnessAttemptRef[\"reason\"]): AttemptLease {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tconst active = this.expectActive();\n\t\tconst attempt = createImmutableSnapshot<HarnessAttemptRef>({\n\t\t\toperationId: lease.operation.operationId,\n\t\t\tattemptId: `${lease.operation.operationId}:a${active.attempts.length}`,\n\t\t\tindex: active.attempts.length,\n\t\t\treason,\n\t\t\tstartedAtMs: this.deps.now(),\n\t\t});\n\t\tthis.apply({ type: \"attempt_begin\", attempt });\n\t\trecord.attemptFinishTimes.delete(attempt.attemptId);\n\t\treturn { attempt, signal: lease.signal };\n\t}\n\n\tfinishAttempt(lease: OperationLease, attempt: AttemptLease, outcome: HarnessAttemptOutcome): void {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tthis.apply({ type: \"attempt_end\", attemptId: attempt.attempt.attemptId, outcome });\n\t\trecord.attemptFinishTimes.set(attempt.attempt.attemptId, this.deps.now());\n\t}\n\n\t/** Captures the current operation as the abort target; never targets operations started later. */\n\trequestAbort(): HarnessAbortCapture {\n\t\tconst record = this.current;\n\t\tif (record === undefined || this.state.tag === \"idle\") {\n\t\t\treturn { signalDelivered: false };\n\t\t}\n\t\tif (this.state.tag === \"settling\") {\n\t\t\treturn { target: record.lease, signalDelivered: false };\n\t\t}\n\t\tthis.apply({ type: \"abort_request\", operationId: record.lease.operation.operationId });\n\t\tif (!record.controller.signal.aborted) record.controller.abort();\n\t\treturn { target: record.lease, signalDelivered: true };\n\t}\n\n\t/**\n\t * Sole active -> idle path. Runs `finalize` inside the settling barrier, then\n\t * releases state and resolves the lease. A finalizer failure still completes\n\t * the state release and resolves `lease.settled`, then rejects this call.\n\t */\n\tasync settle(\n\t\tlease: OperationLease,\n\t\toutcome: HarnessOperationOutcome,\n\t\tfinalize: () => Promise<void>,\n\t): Promise<HarnessOperationOutcome> {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tif (record.settleStarted) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Operation ${lease.operation.operationId} is already settling`);\n\t\t}\n\t\t// A rejected settle_begin (e.g. an attempt still open) is not a settlement.\n\t\t// Marking it as one would wedge the operation: every later settle() would\n\t\t// report \"already settling\" while the state never left `active`, and\n\t\t// `lease.settled` plus every waitForIdle() waiter would hang forever.\n\t\tthis.apply({ type: \"settle_begin\", operationId: lease.operation.operationId, outcome });\n\t\trecord.settleStarted = true;\n\t\tlet finalizeError: unknown;\n\t\ttry {\n\t\t\tawait finalize();\n\t\t} catch (error) {\n\t\t\tfinalizeError = error;\n\t\t}\n\t\tthis.apply({ type: \"settle_finish\", operationId: lease.operation.operationId });\n\t\tthis.current = undefined;\n\t\trecord.resolveSettled(outcome);\n\t\tfor (const release of [...this.idleWaiters]) {\n\t\t\tthis.idleWaiters.delete(release);\n\t\t\trelease();\n\t\t}\n\t\tif (finalizeError !== undefined) throw finalizeError;\n\t\treturn outcome;\n\t}\n\n\t/** Bounded attempt summaries for the current operation; no transcripts. */\n\tgetAttemptSummaries(lease: OperationLease): readonly HarnessAttemptSummary[] {\n\t\tconst record = this.expectCurrentLease(lease);\n\t\tconst attempts = this.state.tag === \"idle\" ? [] : this.state.attempts;\n\t\treturn attempts.map(({ attempt, outcome }) => ({\n\t\t\tattemptId: attempt.attemptId,\n\t\t\tindex: attempt.index,\n\t\t\treason: attempt.reason,\n\t\t\toutcome,\n\t\t\tstartedAtMs: attempt.startedAtMs,\n\t\t\tfinishedAtMs: record.attemptFinishTimes.get(attempt.attemptId) ?? attempt.startedAtMs,\n\t\t}));\n\t}\n\n\t/** The recorded summary for one attempt of the current operation. */\n\tgetAttemptSummary(lease: OperationLease, attempt: AttemptLease): HarnessAttemptSummary {\n\t\tconst attemptId = attempt.attempt.attemptId;\n\t\tconst summary = this.getAttemptSummaries(lease).find((candidate) => candidate.attemptId === attemptId);\n\t\tif (summary === undefined) {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `No summary recorded for attempt ${attemptId}`);\n\t\t}\n\t\treturn summary;\n\t}\n\n\tasync waitForIdle(): Promise<void> {\n\t\tif (this.state.tag === \"idle\") return;\n\t\tawait new Promise<void>((resolve) => {\n\t\t\tthis.idleWaiters.add(resolve);\n\t\t});\n\t}\n\n\tprivate expectActive(): Extract<HarnessLifecycleState, { tag: \"active\" }> {\n\t\tif (this.state.tag !== \"active\") {\n\t\t\tthrow new AgentHarnessError(\"invalid_state\", `Expected an active operation, state is ${this.state.tag}`);\n\t\t}\n\t\treturn this.state;\n\t}\n\n\tprivate expectCurrentLease(lease: OperationLease): OperationRecord {\n\t\tconst record = this.current;\n\t\tif (record === undefined || record.lease !== lease) {\n\t\t\tthrow new AgentHarnessError(\n\t\t\t\t\"invalid_state\",\n\t\t\t\t`Stale operation lease for ${lease.operation.operationId}; the operation no longer owns lifecycle state`,\n\t\t\t);\n\t\t}\n\t\treturn record;\n\t}\n\n\tprivate apply(command: HarnessLifecycleCommand): void {\n\t\tconst next = reduceHarnessLifecycle(this.state, command);\n\t\tif (!next.ok) throw this.asHarnessError(next.error);\n\t\tthis.state = freezeLifecycleState(next.value);\n\t}\n\n\tprivate asHarnessError(violation: HarnessLifecycleViolation): AgentHarnessError {\n\t\treturn new AgentHarnessError(violation.code === \"busy\" ? \"busy\" : \"invalid_state\", violation.message, violation);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure transition function for the harness operation lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Given the current state and one command, returns the next state or a
|
|
5
|
+
* classified violation. It performs no side effects: it does not read a
|
|
6
|
+
* clock, allocate identifiers, emit events, flush sessions, or touch abort
|
|
7
|
+
* signals. The controller supplies all of those around it.
|
|
8
|
+
*
|
|
9
|
+
* The transition table encodes the plan's legal moves plus one documented
|
|
10
|
+
* conservative addition: `structural_running -> committing` marks the single
|
|
11
|
+
* declared commit point of a structural operation. Every other transition not
|
|
12
|
+
* listed here is rejected, and rejection never mutates the input state.
|
|
13
|
+
*/
|
|
14
|
+
import { type HarnessLifecycleCommand, type HarnessLifecycleResult, type HarnessLifecycleState } from "./operation-lifecycle-types.ts";
|
|
15
|
+
type Result = HarnessLifecycleResult<HarnessLifecycleState>;
|
|
16
|
+
export declare function initialHarnessLifecycleState(): HarnessLifecycleState;
|
|
17
|
+
export declare function reduceHarnessLifecycle(state: HarnessLifecycleState, command: HarnessLifecycleCommand): Result;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=operation-lifecycle-reducer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-lifecycle-reducer.d.ts","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-reducer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAK1B,MAAM,gCAAgC,CAAC;AAIxC,KAAK,MAAM,GAAG,sBAAsB,CAAC,qBAAqB,CAAC,CAAC;AAE5D,wBAAgB,4BAA4B,IAAI,qBAAqB,CAEpE;AAkOD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,qBAAqB,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAqB7G","sourcesContent":["/**\n * Pure transition function for the harness operation lifecycle.\n *\n * Given the current state and one command, returns the next state or a\n * classified violation. It performs no side effects: it does not read a\n * clock, allocate identifiers, emit events, flush sessions, or touch abort\n * signals. The controller supplies all of those around it.\n *\n * The transition table encodes the plan's legal moves plus one documented\n * conservative addition: `structural_running -> committing` marks the single\n * declared commit point of a structural operation. Every other transition not\n * listed here is rejected, and rejection never mutates the input state.\n */\n\nimport {\n\ttype HarnessLifecycleCommand,\n\ttype HarnessLifecycleResult,\n\ttype HarnessLifecycleState,\n\tHarnessLifecycleViolation,\n\ttype HarnessLifecycleViolationCode,\n\ttype HarnessOperationKind,\n\tPROMPT_FAMILY_KINDS,\n} from \"./operation-lifecycle-types.ts\";\n\ntype ActiveState = Extract<HarnessLifecycleState, { tag: \"active\" }>;\ntype OwnedState = Extract<HarnessLifecycleState, { tag: \"active\" | \"settling\" }>;\ntype Result = HarnessLifecycleResult<HarnessLifecycleState>;\n\nexport function initialHarnessLifecycleState(): HarnessLifecycleState {\n\treturn { tag: \"idle\", lastSequence: 0 };\n}\n\nfunction reject(\n\tcode: HarnessLifecycleViolationCode,\n\tmessage: string,\n\tstate: HarnessLifecycleState,\n\tcommand: HarnessLifecycleCommand,\n): HarnessLifecycleResult<never> {\n\treturn { ok: false, error: new HarnessLifecycleViolation(code, message, state, command) };\n}\n\nfunction isPromptFamily(kind: HarnessOperationKind): boolean {\n\treturn PROMPT_FAMILY_KINDS.includes(kind);\n}\n\nfunction notIdle(state: HarnessLifecycleState, command: HarnessLifecycleCommand, action: string): Result {\n\treturn reject(\"invalid_transition\", `Cannot ${action} without an active operation`, state, command);\n}\n\n/** Commands carrying an operationId must name the current operation. */\nfunction expectCurrentOperation(\n\tstate: OwnedState,\n\toperationId: string,\n\tcommand: HarnessLifecycleCommand,\n): HarnessLifecycleResult<OwnedState> {\n\tif (state.operation.operationId !== operationId) {\n\t\treturn reject(\n\t\t\t\"stale_operation\",\n\t\t\t`Command targets operation ${operationId} but current operation is ${state.operation.operationId}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: state };\n}\n\nfunction reduceBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"begin\" }>,\n): Result {\n\tif (state.tag !== \"idle\") {\n\t\treturn reject(\"busy\", \"Cannot begin a new operation while another is active or settling\", state, command);\n\t}\n\tif (command.operation.sequence !== state.lastSequence + 1) {\n\t\treturn reject(\n\t\t\t\"sequence_violation\",\n\t\t\t`Operation sequence ${command.operation.sequence} is not lastSequence + 1 (${state.lastSequence + 1})`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn {\n\t\tok: true,\n\t\tvalue: { tag: \"active\", operation: command.operation, stage: \"preparing\", attempts: [], abortRequested: false },\n\t};\n}\n\n/**\n * Overflow recovery answers an observed overflow. It is legal only when the\n * most recently closed attempt ended with `overflow`; otherwise the operation\n * would sit in a stage from which no attempt can begin (index 0 must start\n * from `preparing`, index > 0 needs a prior overflow) and could only settle.\n */\nfunction lastAttemptOverflowed(state: ActiveState): boolean {\n\treturn state.attempts.at(-1)?.outcome === \"overflow\";\n}\n\nfunction isLegalStageMove(state: ActiveState, to: ActiveState[\"stage\"]): boolean {\n\tconst from = state.stage;\n\tconst kind = state.operation.kind;\n\tif (from === \"attempt_running\" && to === \"save_point\") return state.attempt !== undefined;\n\tif (from === \"save_point\" && to === \"attempt_running\") return state.attempt !== undefined;\n\tif (from === \"preparing\" && to === \"recovering_overflow\") {\n\t\treturn isPromptFamily(kind) && state.attempt === undefined && lastAttemptOverflowed(state);\n\t}\n\tif (from === \"preparing\" && to === \"structural_running\") return !isPromptFamily(kind);\n\treturn from === \"structural_running\" && to === \"committing\";\n}\n\nfunction reduceStage(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"stage\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"change stage\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot change stage while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\tif (!isLegalStageMove(state, command.stage)) {\n\t\treturn reject(\n\t\t\t\"invalid_transition\",\n\t\t\t`Illegal stage transition ${state.stage} -> ${command.stage} for ${state.operation.kind}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: { ...state, stage: command.stage } };\n}\n\nfunction reduceAttemptBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"attempt_begin\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"begin an attempt\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot begin an attempt while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.attempt.operationId, command);\n\tif (!current.ok) return current;\n\tif (state.attempt !== undefined) {\n\t\treturn reject(\"attempt_mismatch\", `Attempt ${state.attempt.attemptId} is still active`, state, command);\n\t}\n\tif (!isPromptFamily(state.operation.kind)) {\n\t\treturn reject(\"invalid_transition\", `${state.operation.kind} operations do not run attempts`, state, command);\n\t}\n\tif (command.attempt.index !== state.attempts.length) {\n\t\treturn reject(\n\t\t\t\"sequence_violation\",\n\t\t\t`Attempt index ${command.attempt.index} is not the next index ${state.attempts.length}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\tconst fromPreparing =\n\t\tstate.stage === \"preparing\" && command.attempt.index === 0 && command.attempt.reason === \"initial\";\n\tconst fromRecovery =\n\t\tstate.stage === \"recovering_overflow\" &&\n\t\tcommand.attempt.index > 0 &&\n\t\tcommand.attempt.reason === \"context_overflow_recovery\";\n\tif (!fromPreparing && !fromRecovery) {\n\t\treturn reject(\n\t\t\t\"invalid_transition\",\n\t\t\t`Cannot begin attempt ${command.attempt.attemptId} (${command.attempt.reason}) from stage ${state.stage}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: { ...state, stage: \"attempt_running\", attempt: command.attempt } };\n}\n\nfunction reduceAttemptEnd(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"attempt_end\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"end an attempt\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot end an attempt while settling\", state, command);\n\tif (state.attempt === undefined) return reject(\"attempt_mismatch\", \"No active attempt to end\", state, command);\n\tif (state.attempt.attemptId !== command.attemptId) {\n\t\treturn reject(\n\t\t\t\"attempt_mismatch\",\n\t\t\t`Attempt end names ${command.attemptId} but active attempt is ${state.attempt.attemptId}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\t// save_point is an intra-attempt stage between provider turns; the attempt\n\t// remains logically running there, so it may end from either stage.\n\tif (state.stage !== \"attempt_running\" && state.stage !== \"save_point\") {\n\t\treturn reject(\"invalid_transition\", `Cannot end an attempt from stage ${state.stage}`, state, command);\n\t}\n\tconst { attempt } = state;\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\t...state,\n\t\t\tstage: \"preparing\",\n\t\t\tattempt: undefined,\n\t\t\tattempts: [...state.attempts, { attempt, outcome: command.outcome }],\n\t\t},\n\t};\n}\n\nfunction reduceAbortRequest(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"abort_request\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"abort\");\n\tif (state.tag === \"settling\") return reject(\"invalid_transition\", \"Cannot abort while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\treturn { ok: true, value: { ...state, abortRequested: true } };\n}\n\nfunction reduceSettleBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"settle_begin\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"settle\");\n\tif (state.tag === \"settling\") return reject(\"invalid_transition\", \"Operation is already settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\t// Last line of defence against a silently orphaned attempt: an integration\n\t// bug that drops `attempt_end` must fail loudly here, not settle an\n\t// operation whose summary is missing a started attempt.\n\tif (state.attempt !== undefined) {\n\t\treturn reject(\n\t\t\t\"attempt_mismatch\",\n\t\t\t`Cannot settle operation ${state.operation.operationId} while attempt ${state.attempt.attemptId} is active`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\ttag: \"settling\",\n\t\t\toperation: state.operation,\n\t\t\toutcome: command.outcome,\n\t\t\tattempts: state.attempts,\n\t\t\tabortRequested: state.abortRequested,\n\t\t},\n\t};\n}\n\nfunction reduceSettleFinish(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"settle_finish\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"finish settling\");\n\tif (state.tag === \"active\")\n\t\treturn reject(\"invalid_transition\", \"Operation must pass through settling before finishing\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\treturn { ok: true, value: { tag: \"idle\", lastSequence: state.operation.sequence } };\n}\n\nexport function reduceHarnessLifecycle(state: HarnessLifecycleState, command: HarnessLifecycleCommand): Result {\n\tswitch (command.type) {\n\t\tcase \"begin\":\n\t\t\treturn reduceBegin(state, command);\n\t\tcase \"stage\":\n\t\t\treturn reduceStage(state, command);\n\t\tcase \"attempt_begin\":\n\t\t\treturn reduceAttemptBegin(state, command);\n\t\tcase \"attempt_end\":\n\t\t\treturn reduceAttemptEnd(state, command);\n\t\tcase \"abort_request\":\n\t\t\treturn reduceAbortRequest(state, command);\n\t\tcase \"settle_begin\":\n\t\t\treturn reduceSettleBegin(state, command);\n\t\tcase \"settle_finish\":\n\t\t\treturn reduceSettleFinish(state, command);\n\t\tdefault: {\n\t\t\tconst unknownCommand: never = command;\n\t\t\tthrow new HarnessLifecycleViolation(\"invalid_transition\", \"Unknown lifecycle command\", state, unknownCommand);\n\t\t}\n\t}\n}\n"]}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure transition function for the harness operation lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Given the current state and one command, returns the next state or a
|
|
5
|
+
* classified violation. It performs no side effects: it does not read a
|
|
6
|
+
* clock, allocate identifiers, emit events, flush sessions, or touch abort
|
|
7
|
+
* signals. The controller supplies all of those around it.
|
|
8
|
+
*
|
|
9
|
+
* The transition table encodes the plan's legal moves plus one documented
|
|
10
|
+
* conservative addition: `structural_running -> committing` marks the single
|
|
11
|
+
* declared commit point of a structural operation. Every other transition not
|
|
12
|
+
* listed here is rejected, and rejection never mutates the input state.
|
|
13
|
+
*/
|
|
14
|
+
import { HarnessLifecycleViolation, PROMPT_FAMILY_KINDS, } from "./operation-lifecycle-types.js";
|
|
15
|
+
export function initialHarnessLifecycleState() {
|
|
16
|
+
return { tag: "idle", lastSequence: 0 };
|
|
17
|
+
}
|
|
18
|
+
function reject(code, message, state, command) {
|
|
19
|
+
return { ok: false, error: new HarnessLifecycleViolation(code, message, state, command) };
|
|
20
|
+
}
|
|
21
|
+
function isPromptFamily(kind) {
|
|
22
|
+
return PROMPT_FAMILY_KINDS.includes(kind);
|
|
23
|
+
}
|
|
24
|
+
function notIdle(state, command, action) {
|
|
25
|
+
return reject("invalid_transition", `Cannot ${action} without an active operation`, state, command);
|
|
26
|
+
}
|
|
27
|
+
/** Commands carrying an operationId must name the current operation. */
|
|
28
|
+
function expectCurrentOperation(state, operationId, command) {
|
|
29
|
+
if (state.operation.operationId !== operationId) {
|
|
30
|
+
return reject("stale_operation", `Command targets operation ${operationId} but current operation is ${state.operation.operationId}`, state, command);
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, value: state };
|
|
33
|
+
}
|
|
34
|
+
function reduceBegin(state, command) {
|
|
35
|
+
if (state.tag !== "idle") {
|
|
36
|
+
return reject("busy", "Cannot begin a new operation while another is active or settling", state, command);
|
|
37
|
+
}
|
|
38
|
+
if (command.operation.sequence !== state.lastSequence + 1) {
|
|
39
|
+
return reject("sequence_violation", `Operation sequence ${command.operation.sequence} is not lastSequence + 1 (${state.lastSequence + 1})`, state, command);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ok: true,
|
|
43
|
+
value: { tag: "active", operation: command.operation, stage: "preparing", attempts: [], abortRequested: false },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Overflow recovery answers an observed overflow. It is legal only when the
|
|
48
|
+
* most recently closed attempt ended with `overflow`; otherwise the operation
|
|
49
|
+
* would sit in a stage from which no attempt can begin (index 0 must start
|
|
50
|
+
* from `preparing`, index > 0 needs a prior overflow) and could only settle.
|
|
51
|
+
*/
|
|
52
|
+
function lastAttemptOverflowed(state) {
|
|
53
|
+
return state.attempts.at(-1)?.outcome === "overflow";
|
|
54
|
+
}
|
|
55
|
+
function isLegalStageMove(state, to) {
|
|
56
|
+
const from = state.stage;
|
|
57
|
+
const kind = state.operation.kind;
|
|
58
|
+
if (from === "attempt_running" && to === "save_point")
|
|
59
|
+
return state.attempt !== undefined;
|
|
60
|
+
if (from === "save_point" && to === "attempt_running")
|
|
61
|
+
return state.attempt !== undefined;
|
|
62
|
+
if (from === "preparing" && to === "recovering_overflow") {
|
|
63
|
+
return isPromptFamily(kind) && state.attempt === undefined && lastAttemptOverflowed(state);
|
|
64
|
+
}
|
|
65
|
+
if (from === "preparing" && to === "structural_running")
|
|
66
|
+
return !isPromptFamily(kind);
|
|
67
|
+
return from === "structural_running" && to === "committing";
|
|
68
|
+
}
|
|
69
|
+
function reduceStage(state, command) {
|
|
70
|
+
if (state.tag === "idle")
|
|
71
|
+
return notIdle(state, command, "change stage");
|
|
72
|
+
if (state.tag === "settling")
|
|
73
|
+
return reject("invalid_transition", "Cannot change stage while settling", state, command);
|
|
74
|
+
const current = expectCurrentOperation(state, command.operationId, command);
|
|
75
|
+
if (!current.ok)
|
|
76
|
+
return current;
|
|
77
|
+
if (!isLegalStageMove(state, command.stage)) {
|
|
78
|
+
return reject("invalid_transition", `Illegal stage transition ${state.stage} -> ${command.stage} for ${state.operation.kind}`, state, command);
|
|
79
|
+
}
|
|
80
|
+
return { ok: true, value: { ...state, stage: command.stage } };
|
|
81
|
+
}
|
|
82
|
+
function reduceAttemptBegin(state, command) {
|
|
83
|
+
if (state.tag === "idle")
|
|
84
|
+
return notIdle(state, command, "begin an attempt");
|
|
85
|
+
if (state.tag === "settling")
|
|
86
|
+
return reject("invalid_transition", "Cannot begin an attempt while settling", state, command);
|
|
87
|
+
const current = expectCurrentOperation(state, command.attempt.operationId, command);
|
|
88
|
+
if (!current.ok)
|
|
89
|
+
return current;
|
|
90
|
+
if (state.attempt !== undefined) {
|
|
91
|
+
return reject("attempt_mismatch", `Attempt ${state.attempt.attemptId} is still active`, state, command);
|
|
92
|
+
}
|
|
93
|
+
if (!isPromptFamily(state.operation.kind)) {
|
|
94
|
+
return reject("invalid_transition", `${state.operation.kind} operations do not run attempts`, state, command);
|
|
95
|
+
}
|
|
96
|
+
if (command.attempt.index !== state.attempts.length) {
|
|
97
|
+
return reject("sequence_violation", `Attempt index ${command.attempt.index} is not the next index ${state.attempts.length}`, state, command);
|
|
98
|
+
}
|
|
99
|
+
const fromPreparing = state.stage === "preparing" && command.attempt.index === 0 && command.attempt.reason === "initial";
|
|
100
|
+
const fromRecovery = state.stage === "recovering_overflow" &&
|
|
101
|
+
command.attempt.index > 0 &&
|
|
102
|
+
command.attempt.reason === "context_overflow_recovery";
|
|
103
|
+
if (!fromPreparing && !fromRecovery) {
|
|
104
|
+
return reject("invalid_transition", `Cannot begin attempt ${command.attempt.attemptId} (${command.attempt.reason}) from stage ${state.stage}`, state, command);
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, value: { ...state, stage: "attempt_running", attempt: command.attempt } };
|
|
107
|
+
}
|
|
108
|
+
function reduceAttemptEnd(state, command) {
|
|
109
|
+
if (state.tag === "idle")
|
|
110
|
+
return notIdle(state, command, "end an attempt");
|
|
111
|
+
if (state.tag === "settling")
|
|
112
|
+
return reject("invalid_transition", "Cannot end an attempt while settling", state, command);
|
|
113
|
+
if (state.attempt === undefined)
|
|
114
|
+
return reject("attempt_mismatch", "No active attempt to end", state, command);
|
|
115
|
+
if (state.attempt.attemptId !== command.attemptId) {
|
|
116
|
+
return reject("attempt_mismatch", `Attempt end names ${command.attemptId} but active attempt is ${state.attempt.attemptId}`, state, command);
|
|
117
|
+
}
|
|
118
|
+
// save_point is an intra-attempt stage between provider turns; the attempt
|
|
119
|
+
// remains logically running there, so it may end from either stage.
|
|
120
|
+
if (state.stage !== "attempt_running" && state.stage !== "save_point") {
|
|
121
|
+
return reject("invalid_transition", `Cannot end an attempt from stage ${state.stage}`, state, command);
|
|
122
|
+
}
|
|
123
|
+
const { attempt } = state;
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
value: {
|
|
127
|
+
...state,
|
|
128
|
+
stage: "preparing",
|
|
129
|
+
attempt: undefined,
|
|
130
|
+
attempts: [...state.attempts, { attempt, outcome: command.outcome }],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function reduceAbortRequest(state, command) {
|
|
135
|
+
if (state.tag === "idle")
|
|
136
|
+
return notIdle(state, command, "abort");
|
|
137
|
+
if (state.tag === "settling")
|
|
138
|
+
return reject("invalid_transition", "Cannot abort while settling", state, command);
|
|
139
|
+
const current = expectCurrentOperation(state, command.operationId, command);
|
|
140
|
+
if (!current.ok)
|
|
141
|
+
return current;
|
|
142
|
+
return { ok: true, value: { ...state, abortRequested: true } };
|
|
143
|
+
}
|
|
144
|
+
function reduceSettleBegin(state, command) {
|
|
145
|
+
if (state.tag === "idle")
|
|
146
|
+
return notIdle(state, command, "settle");
|
|
147
|
+
if (state.tag === "settling")
|
|
148
|
+
return reject("invalid_transition", "Operation is already settling", state, command);
|
|
149
|
+
const current = expectCurrentOperation(state, command.operationId, command);
|
|
150
|
+
if (!current.ok)
|
|
151
|
+
return current;
|
|
152
|
+
// Last line of defence against a silently orphaned attempt: an integration
|
|
153
|
+
// bug that drops `attempt_end` must fail loudly here, not settle an
|
|
154
|
+
// operation whose summary is missing a started attempt.
|
|
155
|
+
if (state.attempt !== undefined) {
|
|
156
|
+
return reject("attempt_mismatch", `Cannot settle operation ${state.operation.operationId} while attempt ${state.attempt.attemptId} is active`, state, command);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
ok: true,
|
|
160
|
+
value: {
|
|
161
|
+
tag: "settling",
|
|
162
|
+
operation: state.operation,
|
|
163
|
+
outcome: command.outcome,
|
|
164
|
+
attempts: state.attempts,
|
|
165
|
+
abortRequested: state.abortRequested,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function reduceSettleFinish(state, command) {
|
|
170
|
+
if (state.tag === "idle")
|
|
171
|
+
return notIdle(state, command, "finish settling");
|
|
172
|
+
if (state.tag === "active")
|
|
173
|
+
return reject("invalid_transition", "Operation must pass through settling before finishing", state, command);
|
|
174
|
+
const current = expectCurrentOperation(state, command.operationId, command);
|
|
175
|
+
if (!current.ok)
|
|
176
|
+
return current;
|
|
177
|
+
return { ok: true, value: { tag: "idle", lastSequence: state.operation.sequence } };
|
|
178
|
+
}
|
|
179
|
+
export function reduceHarnessLifecycle(state, command) {
|
|
180
|
+
switch (command.type) {
|
|
181
|
+
case "begin":
|
|
182
|
+
return reduceBegin(state, command);
|
|
183
|
+
case "stage":
|
|
184
|
+
return reduceStage(state, command);
|
|
185
|
+
case "attempt_begin":
|
|
186
|
+
return reduceAttemptBegin(state, command);
|
|
187
|
+
case "attempt_end":
|
|
188
|
+
return reduceAttemptEnd(state, command);
|
|
189
|
+
case "abort_request":
|
|
190
|
+
return reduceAbortRequest(state, command);
|
|
191
|
+
case "settle_begin":
|
|
192
|
+
return reduceSettleBegin(state, command);
|
|
193
|
+
case "settle_finish":
|
|
194
|
+
return reduceSettleFinish(state, command);
|
|
195
|
+
default: {
|
|
196
|
+
const unknownCommand = command;
|
|
197
|
+
throw new HarnessLifecycleViolation("invalid_transition", "Unknown lifecycle command", state, unknownCommand);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
//# sourceMappingURL=operation-lifecycle-reducer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operation-lifecycle-reducer.js","sourceRoot":"","sources":["../../src/harness/operation-lifecycle-reducer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Pure transition function for the harness operation lifecycle.\n *\n * Given the current state and one command, returns the next state or a\n * classified violation. It performs no side effects: it does not read a\n * clock, allocate identifiers, emit events, flush sessions, or touch abort\n * signals. The controller supplies all of those around it.\n *\n * The transition table encodes the plan's legal moves plus one documented\n * conservative addition: `structural_running -> committing` marks the single\n * declared commit point of a structural operation. Every other transition not\n * listed here is rejected, and rejection never mutates the input state.\n */\n\nimport {\n\ttype HarnessLifecycleCommand,\n\ttype HarnessLifecycleResult,\n\ttype HarnessLifecycleState,\n\tHarnessLifecycleViolation,\n\ttype HarnessLifecycleViolationCode,\n\ttype HarnessOperationKind,\n\tPROMPT_FAMILY_KINDS,\n} from \"./operation-lifecycle-types.ts\";\n\ntype ActiveState = Extract<HarnessLifecycleState, { tag: \"active\" }>;\ntype OwnedState = Extract<HarnessLifecycleState, { tag: \"active\" | \"settling\" }>;\ntype Result = HarnessLifecycleResult<HarnessLifecycleState>;\n\nexport function initialHarnessLifecycleState(): HarnessLifecycleState {\n\treturn { tag: \"idle\", lastSequence: 0 };\n}\n\nfunction reject(\n\tcode: HarnessLifecycleViolationCode,\n\tmessage: string,\n\tstate: HarnessLifecycleState,\n\tcommand: HarnessLifecycleCommand,\n): HarnessLifecycleResult<never> {\n\treturn { ok: false, error: new HarnessLifecycleViolation(code, message, state, command) };\n}\n\nfunction isPromptFamily(kind: HarnessOperationKind): boolean {\n\treturn PROMPT_FAMILY_KINDS.includes(kind);\n}\n\nfunction notIdle(state: HarnessLifecycleState, command: HarnessLifecycleCommand, action: string): Result {\n\treturn reject(\"invalid_transition\", `Cannot ${action} without an active operation`, state, command);\n}\n\n/** Commands carrying an operationId must name the current operation. */\nfunction expectCurrentOperation(\n\tstate: OwnedState,\n\toperationId: string,\n\tcommand: HarnessLifecycleCommand,\n): HarnessLifecycleResult<OwnedState> {\n\tif (state.operation.operationId !== operationId) {\n\t\treturn reject(\n\t\t\t\"stale_operation\",\n\t\t\t`Command targets operation ${operationId} but current operation is ${state.operation.operationId}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: state };\n}\n\nfunction reduceBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"begin\" }>,\n): Result {\n\tif (state.tag !== \"idle\") {\n\t\treturn reject(\"busy\", \"Cannot begin a new operation while another is active or settling\", state, command);\n\t}\n\tif (command.operation.sequence !== state.lastSequence + 1) {\n\t\treturn reject(\n\t\t\t\"sequence_violation\",\n\t\t\t`Operation sequence ${command.operation.sequence} is not lastSequence + 1 (${state.lastSequence + 1})`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn {\n\t\tok: true,\n\t\tvalue: { tag: \"active\", operation: command.operation, stage: \"preparing\", attempts: [], abortRequested: false },\n\t};\n}\n\n/**\n * Overflow recovery answers an observed overflow. It is legal only when the\n * most recently closed attempt ended with `overflow`; otherwise the operation\n * would sit in a stage from which no attempt can begin (index 0 must start\n * from `preparing`, index > 0 needs a prior overflow) and could only settle.\n */\nfunction lastAttemptOverflowed(state: ActiveState): boolean {\n\treturn state.attempts.at(-1)?.outcome === \"overflow\";\n}\n\nfunction isLegalStageMove(state: ActiveState, to: ActiveState[\"stage\"]): boolean {\n\tconst from = state.stage;\n\tconst kind = state.operation.kind;\n\tif (from === \"attempt_running\" && to === \"save_point\") return state.attempt !== undefined;\n\tif (from === \"save_point\" && to === \"attempt_running\") return state.attempt !== undefined;\n\tif (from === \"preparing\" && to === \"recovering_overflow\") {\n\t\treturn isPromptFamily(kind) && state.attempt === undefined && lastAttemptOverflowed(state);\n\t}\n\tif (from === \"preparing\" && to === \"structural_running\") return !isPromptFamily(kind);\n\treturn from === \"structural_running\" && to === \"committing\";\n}\n\nfunction reduceStage(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"stage\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"change stage\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot change stage while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\tif (!isLegalStageMove(state, command.stage)) {\n\t\treturn reject(\n\t\t\t\"invalid_transition\",\n\t\t\t`Illegal stage transition ${state.stage} -> ${command.stage} for ${state.operation.kind}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: { ...state, stage: command.stage } };\n}\n\nfunction reduceAttemptBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"attempt_begin\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"begin an attempt\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot begin an attempt while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.attempt.operationId, command);\n\tif (!current.ok) return current;\n\tif (state.attempt !== undefined) {\n\t\treturn reject(\"attempt_mismatch\", `Attempt ${state.attempt.attemptId} is still active`, state, command);\n\t}\n\tif (!isPromptFamily(state.operation.kind)) {\n\t\treturn reject(\"invalid_transition\", `${state.operation.kind} operations do not run attempts`, state, command);\n\t}\n\tif (command.attempt.index !== state.attempts.length) {\n\t\treturn reject(\n\t\t\t\"sequence_violation\",\n\t\t\t`Attempt index ${command.attempt.index} is not the next index ${state.attempts.length}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\tconst fromPreparing =\n\t\tstate.stage === \"preparing\" && command.attempt.index === 0 && command.attempt.reason === \"initial\";\n\tconst fromRecovery =\n\t\tstate.stage === \"recovering_overflow\" &&\n\t\tcommand.attempt.index > 0 &&\n\t\tcommand.attempt.reason === \"context_overflow_recovery\";\n\tif (!fromPreparing && !fromRecovery) {\n\t\treturn reject(\n\t\t\t\"invalid_transition\",\n\t\t\t`Cannot begin attempt ${command.attempt.attemptId} (${command.attempt.reason}) from stage ${state.stage}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn { ok: true, value: { ...state, stage: \"attempt_running\", attempt: command.attempt } };\n}\n\nfunction reduceAttemptEnd(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"attempt_end\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"end an attempt\");\n\tif (state.tag === \"settling\")\n\t\treturn reject(\"invalid_transition\", \"Cannot end an attempt while settling\", state, command);\n\tif (state.attempt === undefined) return reject(\"attempt_mismatch\", \"No active attempt to end\", state, command);\n\tif (state.attempt.attemptId !== command.attemptId) {\n\t\treturn reject(\n\t\t\t\"attempt_mismatch\",\n\t\t\t`Attempt end names ${command.attemptId} but active attempt is ${state.attempt.attemptId}`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\t// save_point is an intra-attempt stage between provider turns; the attempt\n\t// remains logically running there, so it may end from either stage.\n\tif (state.stage !== \"attempt_running\" && state.stage !== \"save_point\") {\n\t\treturn reject(\"invalid_transition\", `Cannot end an attempt from stage ${state.stage}`, state, command);\n\t}\n\tconst { attempt } = state;\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\t...state,\n\t\t\tstage: \"preparing\",\n\t\t\tattempt: undefined,\n\t\t\tattempts: [...state.attempts, { attempt, outcome: command.outcome }],\n\t\t},\n\t};\n}\n\nfunction reduceAbortRequest(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"abort_request\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"abort\");\n\tif (state.tag === \"settling\") return reject(\"invalid_transition\", \"Cannot abort while settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\treturn { ok: true, value: { ...state, abortRequested: true } };\n}\n\nfunction reduceSettleBegin(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"settle_begin\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"settle\");\n\tif (state.tag === \"settling\") return reject(\"invalid_transition\", \"Operation is already settling\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\t// Last line of defence against a silently orphaned attempt: an integration\n\t// bug that drops `attempt_end` must fail loudly here, not settle an\n\t// operation whose summary is missing a started attempt.\n\tif (state.attempt !== undefined) {\n\t\treturn reject(\n\t\t\t\"attempt_mismatch\",\n\t\t\t`Cannot settle operation ${state.operation.operationId} while attempt ${state.attempt.attemptId} is active`,\n\t\t\tstate,\n\t\t\tcommand,\n\t\t);\n\t}\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\ttag: \"settling\",\n\t\t\toperation: state.operation,\n\t\t\toutcome: command.outcome,\n\t\t\tattempts: state.attempts,\n\t\t\tabortRequested: state.abortRequested,\n\t\t},\n\t};\n}\n\nfunction reduceSettleFinish(\n\tstate: HarnessLifecycleState,\n\tcommand: Extract<HarnessLifecycleCommand, { type: \"settle_finish\" }>,\n): Result {\n\tif (state.tag === \"idle\") return notIdle(state, command, \"finish settling\");\n\tif (state.tag === \"active\")\n\t\treturn reject(\"invalid_transition\", \"Operation must pass through settling before finishing\", state, command);\n\tconst current = expectCurrentOperation(state, command.operationId, command);\n\tif (!current.ok) return current;\n\treturn { ok: true, value: { tag: \"idle\", lastSequence: state.operation.sequence } };\n}\n\nexport function reduceHarnessLifecycle(state: HarnessLifecycleState, command: HarnessLifecycleCommand): Result {\n\tswitch (command.type) {\n\t\tcase \"begin\":\n\t\t\treturn reduceBegin(state, command);\n\t\tcase \"stage\":\n\t\t\treturn reduceStage(state, command);\n\t\tcase \"attempt_begin\":\n\t\t\treturn reduceAttemptBegin(state, command);\n\t\tcase \"attempt_end\":\n\t\t\treturn reduceAttemptEnd(state, command);\n\t\tcase \"abort_request\":\n\t\t\treturn reduceAbortRequest(state, command);\n\t\tcase \"settle_begin\":\n\t\t\treturn reduceSettleBegin(state, command);\n\t\tcase \"settle_finish\":\n\t\t\treturn reduceSettleFinish(state, command);\n\t\tdefault: {\n\t\t\tconst unknownCommand: never = command;\n\t\t\tthrow new HarnessLifecycleViolation(\"invalid_transition\", \"Unknown lifecycle command\", state, unknownCommand);\n\t\t}\n\t}\n}\n"]}
|