@agentxm/workspace-operations 0.28.4-bootstrap.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +12 -0
  3. package/dist/src/index.d.ts +31 -0
  4. package/dist/src/index.js +42 -0
  5. package/dist/src/live.d.ts +13 -0
  6. package/dist/src/live.js +12 -0
  7. package/dist/src/operations/augment-plan.d.ts +25 -0
  8. package/dist/src/operations/augment-plan.js +51 -0
  9. package/dist/src/operations/load-workspace.d.ts +43 -0
  10. package/dist/src/operations/load-workspace.js +77 -0
  11. package/dist/src/operations/scan-plan-readiness.d.ts +22 -0
  12. package/dist/src/operations/scan-plan-readiness.js +41 -0
  13. package/dist/src/operations/transaction.d.ts +58 -0
  14. package/dist/src/operations/transaction.js +359 -0
  15. package/dist/src/operations/transition-lock.d.ts +66 -0
  16. package/dist/src/operations/transition-lock.js +291 -0
  17. package/dist/src/plan/apply-plan.d.ts +45 -0
  18. package/dist/src/plan/apply-plan.js +238 -0
  19. package/dist/src/plan/errors.d.ts +85 -0
  20. package/dist/src/plan/errors.js +101 -0
  21. package/dist/src/plan/execution-candidate.d.ts +20 -0
  22. package/dist/src/plan/execution-candidate.js +95 -0
  23. package/dist/src/plan/interruption-signal.d.ts +18 -0
  24. package/dist/src/plan/interruption-signal.js +13 -0
  25. package/dist/src/plan/job-step-message.d.ts +7 -0
  26. package/dist/src/plan/job-step-message.js +7 -0
  27. package/dist/src/plan/operation-events.d.ts +62 -0
  28. package/dist/src/plan/operation-events.js +41 -0
  29. package/dist/src/plan/operation-journal.d.ts +69 -0
  30. package/dist/src/plan/operation-journal.js +52 -0
  31. package/dist/src/plan/operation-resolution.d.ts +219 -0
  32. package/dist/src/plan/operation-resolution.js +324 -0
  33. package/dist/src/plan/plan-execution.d.ts +75 -0
  34. package/dist/src/plan/plan-execution.js +117 -0
  35. package/dist/src/plan/plan.d.ts +248 -0
  36. package/dist/src/plan/plan.js +95 -0
  37. package/dist/src/plan/resolve-plan-interaction.d.ts +79 -0
  38. package/dist/src/plan/resolve-plan-interaction.js +52 -0
  39. package/dist/src/plan/resolve-plan.d.ts +42 -0
  40. package/dist/src/plan/resolve-plan.js +694 -0
  41. package/dist/src/plan/step-failure-conversions.d.ts +33 -0
  42. package/dist/src/plan/step-failure-conversions.js +177 -0
  43. package/dist/src/testing.d.ts +11 -0
  44. package/dist/src/testing.js +11 -0
  45. package/package.json +61 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Error category vocabulary for serialized plan and step data.
3
+ *
4
+ * The categories are the same strings as the CLI's `AppErrorCode` so machine
5
+ * output stays byte-identical across the package boundary; the conversion
6
+ * boundary beside the CLI error vocabulary asserts the parity at compile
7
+ * time. The kernel owns the vocabulary because plans, journals, and machine
8
+ * output serialize it; it never owns titles, exit codes, or rendering.
9
+ *
10
+ * @experimental This API is unstable and may change without notice.
11
+ */
12
+ import * as Schema from "effect/Schema";
13
+ /** Every category a plan, step result, or risk condition may serialize. */
14
+ export const OPERATION_ERROR_CATEGORIES = [
15
+ "issues",
16
+ "usage",
17
+ "not_found",
18
+ "auth",
19
+ "forbidden",
20
+ "conflict",
21
+ "rate_limit",
22
+ "network",
23
+ "validation",
24
+ "internal",
25
+ "unavailable",
26
+ "quota",
27
+ "auth_required",
28
+ "auth_expired",
29
+ "auth_denied",
30
+ "timeout",
31
+ ];
32
+ export const OperationErrorCategorySchema = Schema.Literals(OPERATION_ERROR_CATEGORIES).annotate({
33
+ identifier: "OperationErrorCategory",
34
+ });
35
+ /**
36
+ * The `SuggestedAction` contract shape, without the safe-command runtime
37
+ * filter: a step failure carries whatever suggestion its producer chose, and
38
+ * the CLI boundary sanitizes suggested commands before rendering them.
39
+ */
40
+ const CarriedSuggestedActionSchema = Schema.Struct({
41
+ description: Schema.String,
42
+ cmd: Schema.optional(Schema.String),
43
+ url: Schema.optional(Schema.String),
44
+ });
45
+ const _suggestedActionParity = (value) => value;
46
+ void _suggestedActionParity;
47
+ /**
48
+ * The one serializable failure a plan step settles with. Step authors own the
49
+ * category choice and the user-facing detail sentence; `suggestions` carries
50
+ * only display data the boundary cannot reconstruct from fields, and `cause`
51
+ * carries the typed feature error or raw cause for diagnostic chains. The CLI
52
+ * boundary owns rendering, exit codes, and the AppError envelope.
53
+ */
54
+ export class StepFailure extends Schema.TaggedError()("StepFailure", {
55
+ category: OperationErrorCategorySchema,
56
+ detail: Schema.String,
57
+ suggestions: Schema.optional(Schema.Array(CarriedSuggestedActionSchema)),
58
+ cause: Schema.optional(Schema.Unknown),
59
+ }) {
60
+ }
61
+ /**
62
+ * Detail sentence for a stale execution candidate; the CLI conversion emits
63
+ * it verbatim so blocked output stays byte-identical.
64
+ */
65
+ export const STALE_CANDIDATE_DETAIL = "The execution candidate became stale before apply.";
66
+ /**
67
+ * The frozen execution candidate's material preimages changed between
68
+ * validation and apply. Detected by tag, never by detail-string comparison.
69
+ */
70
+ export class StaleExecutionCandidate extends Schema.TaggedError()("StaleExecutionCandidate", {
71
+ /** The plan name of the candidate that went stale. */
72
+ candidate: Schema.String,
73
+ }) {
74
+ }
75
+ /** Fingerprinting one execution-material path failed. */
76
+ export class CandidateFingerprintFailed extends Schema.TaggedError()("CandidateFingerprintFailed", {
77
+ /** The material path whose preimage could not be read. */
78
+ target: Schema.String,
79
+ cause: Schema.Unknown,
80
+ }) {
81
+ }
82
+ /**
83
+ * An apply-mode execution reached the plan pipeline without approval
84
+ * recovery metadata: a caller violated the `PlanExecution` contract. The CLI
85
+ * boundary owns the rendering.
86
+ */
87
+ export class ApprovalRecoveryMissing extends Schema.TaggedError()("ApprovalRecoveryMissing", {}) {
88
+ }
89
+ /**
90
+ * The plan interaction implementation could not complete a presentation or
91
+ * confirmation exchange. The implementation owns wording and category choice;
92
+ * the kernel only transports the failure to the boundary that renders it.
93
+ */
94
+ export class PlanInteractionFailed extends Schema.TaggedError()("PlanInteractionFailed", {
95
+ category: OperationErrorCategorySchema,
96
+ detail: Schema.String,
97
+ suggestions: Schema.optional(Schema.Array(CarriedSuggestedActionSchema)),
98
+ cause: Schema.optional(Schema.Unknown),
99
+ }) {
100
+ }
101
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,20 @@
1
+ import * as FileSystem from "effect/FileSystem";
2
+ import * as Path from "effect/Path";
3
+ import * as Effect from "effect/Effect";
4
+ import { CandidateFingerprintFailed } from "./errors.js";
5
+ import type { Plan } from "./plan.js";
6
+ export interface ExecutionCandidate<Requirements = never, Output = never> {
7
+ readonly id: string;
8
+ readonly plan: Plan<Requirements, Output>;
9
+ readonly materialPaths: ReadonlyArray<string>;
10
+ readonly materialFingerprint: string;
11
+ /** Base the material fingerprint is relative to; freshness recomputes against it. */
12
+ readonly baseDir: string;
13
+ }
14
+ export declare const makeExecutionCandidate: <Requirements, Output>(plan: Plan<Requirements, Output>, paths: {
15
+ readonly settingsPath: string;
16
+ readonly lockPath: string;
17
+ readonly baseDir: string;
18
+ }) => Effect.Effect<ExecutionCandidate<Requirements, Output>, CandidateFingerprintFailed, FileSystem.FileSystem | Path.Path>;
19
+ export declare const isExecutionCandidateFresh: (candidate: ExecutionCandidate<unknown, unknown>) => Effect.Effect<boolean, CandidateFingerprintFailed, FileSystem.FileSystem | Path.Path>;
20
+ //# sourceMappingURL=execution-candidate.d.ts.map
@@ -0,0 +1,95 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as FileSystem from "effect/FileSystem";
3
+ import * as Path from "effect/Path";
4
+ import * as Effect from "effect/Effect";
5
+ import * as Option from "effect/Option";
6
+ import { CandidateFingerprintFailed } from "./errors.js";
7
+ const collectArtifactPaths = (plan) => plan.jobs.flatMap((job) => job.steps.flatMap((step) => {
8
+ if (step.artifact === undefined)
9
+ return [];
10
+ return [step.artifact.path, ...(step.artifact.targets ?? []).map((target) => target.path)];
11
+ }));
12
+ const resolveMaterialPaths = (plan, settingsPath, lockPath, baseDir, path) => Array.from(new Set([settingsPath, lockPath, ...(plan.materialPaths ?? []), ...collectArtifactPaths(plan)].map((candidate) => path.resolve(baseDir, candidate)))).sort();
13
+ const fingerprintPath = (target, label, fs, path) => Effect.gen(function* () {
14
+ const link = yield* fs.readLink(target).pipe(Effect.option);
15
+ if (Option.isSome(link))
16
+ return [label, "symlink", link.value];
17
+ const info = yield* fs.stat(target).pipe(Effect.option);
18
+ if (Option.isNone(info))
19
+ return [label, "absent"];
20
+ if (info.value.type === "File")
21
+ return [label, "file", yield* fs.readFile(target)];
22
+ if (info.value.type !== "Directory")
23
+ return [label, info.value.type];
24
+ const entries = [...(yield* fs.readDirectory(target, { recursive: true }))].sort();
25
+ const parts = [label, "directory"];
26
+ for (const entry of entries) {
27
+ const absolute = path.join(target, entry);
28
+ const entryInfo = yield* fs.stat(absolute);
29
+ parts.push(entry, entryInfo.type);
30
+ if (entryInfo.type === "File")
31
+ parts.push(yield* fs.readFile(absolute));
32
+ if (entryInfo.type === "SymbolicLink")
33
+ parts.push(yield* fs.readLink(absolute));
34
+ }
35
+ return parts;
36
+ }).pipe(Effect.mapError((cause) => new CandidateFingerprintFailed({ target, cause })));
37
+ // Materials hash under their base-relative names: candidate identity is a
38
+ // property of workspace content, not of where the workspace sits on disk.
39
+ const fingerprintMaterials = (materialPaths, baseDir, fs, path) => Effect.gen(function* () {
40
+ const hash = crypto.createHash("sha256");
41
+ for (const target of materialPaths) {
42
+ const parts = yield* fingerprintPath(target, path.relative(baseDir, target), fs, path);
43
+ for (const part of parts) {
44
+ hash.update(part);
45
+ hash.update("\0");
46
+ }
47
+ }
48
+ return hash.digest("hex");
49
+ });
50
+ // Absolute paths inside plan metadata relativize before hashing: candidate
51
+ // identity is a property of workspace content, not of where it sits on disk.
52
+ const planIdentity = (plan, baseDir, path) => JSON.stringify({
53
+ name: plan.name,
54
+ jobs: plan.jobs.map((job) => ({
55
+ concurrency: job.concurrency,
56
+ executionPolicy: job.executionPolicy,
57
+ steps: job.steps.map((step) => ({
58
+ key: step.key,
59
+ dependsOn: step.dependsOn,
60
+ label: step.label,
61
+ readiness: step.readiness,
62
+ artifact: step.artifact,
63
+ registryLifecycle: step.registryLifecycle,
64
+ })),
65
+ })),
66
+ preconditions: plan.preconditions,
67
+ riskConditions: plan.riskConditions,
68
+ releaseAge: plan.releaseAge,
69
+ }, (key, value) => {
70
+ if (key === "evaluatedAt")
71
+ return undefined;
72
+ return typeof value === "string" && path.isAbsolute(value)
73
+ ? path.relative(baseDir, value)
74
+ : value;
75
+ });
76
+ export const makeExecutionCandidate = (plan, paths) => Effect.gen(function* () {
77
+ const fs = yield* FileSystem.FileSystem;
78
+ const path = yield* Path.Path;
79
+ const materialPaths = resolveMaterialPaths(plan, paths.settingsPath, paths.lockPath, paths.baseDir, path);
80
+ const materialFingerprint = yield* fingerprintMaterials(materialPaths, paths.baseDir, fs, path);
81
+ const id = crypto
82
+ .createHash("sha256")
83
+ .update(planIdentity(plan, paths.baseDir, path))
84
+ .update("\0")
85
+ .update(materialFingerprint)
86
+ .digest("hex");
87
+ return { id, plan, materialPaths, materialFingerprint, baseDir: paths.baseDir };
88
+ });
89
+ export const isExecutionCandidateFresh = (candidate) => Effect.gen(function* () {
90
+ const fs = yield* FileSystem.FileSystem;
91
+ const path = yield* Path.Path;
92
+ const current = yield* fingerprintMaterials(candidate.materialPaths, candidate.baseDir, fs, path);
93
+ return current === candidate.materialFingerprint;
94
+ });
95
+ //# sourceMappingURL=execution-candidate.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Interruption-signal source port.
3
+ *
4
+ * The process-entry adapter records which signal requested termination; the
5
+ * kernel reads that fact through this port when it resolves an interrupted
6
+ * apply. When no source is provided, the kernel assumes SIGINT.
7
+ *
8
+ * @experimental This API is unstable and may change without notice.
9
+ */
10
+ import * as ServiceMap from "effect/Context";
11
+ export interface InterruptionSignalSourceService {
12
+ readonly requestedSignal: () => "SIGINT" | "SIGTERM" | undefined;
13
+ }
14
+ declare const InterruptionSignalSource_base: ServiceMap.ServiceClass<InterruptionSignalSource, "@agentxm/workspace-operations/plan/interruption-signal/InterruptionSignalSource", InterruptionSignalSourceService>;
15
+ export declare class InterruptionSignalSource extends InterruptionSignalSource_base {
16
+ }
17
+ export {};
18
+ //# sourceMappingURL=interruption-signal.d.ts.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Interruption-signal source port.
3
+ *
4
+ * The process-entry adapter records which signal requested termination; the
5
+ * kernel reads that fact through this port when it resolves an interrupted
6
+ * apply. When no source is provided, the kernel assumes SIGINT.
7
+ *
8
+ * @experimental This API is unstable and may change without notice.
9
+ */
10
+ import * as ServiceMap from "effect/Context";
11
+ export class InterruptionSignalSource extends ServiceMap.Service()("@agentxm/workspace-operations/plan/interruption-signal/InterruptionSignalSource") {
12
+ }
13
+ //# sourceMappingURL=interruption-signal.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Job-step message helpers.
3
+ *
4
+ * @experimental This API is unstable and may change without notice.
5
+ */
6
+ export declare const appendWarningsToMessage: (message: string, warnings: ReadonlyArray<string>) => string;
7
+ //# sourceMappingURL=job-step-message.d.ts.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Job-step message helpers.
3
+ *
4
+ * @experimental This API is unstable and may change without notice.
5
+ */
6
+ export const appendWarningsToMessage = (message, warnings) => (warnings.length === 0 ? message : `${message}; ${warnings.join("; ")}`);
7
+ //# sourceMappingURL=job-step-message.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Operation lifecycle events.
3
+ *
4
+ * Execution publishes typed lifecycle events — phase transitions, unit state
5
+ * transitions, and waiting reasons, each with a monotonic timestamp — to an
6
+ * invocation-scoped broadcast. Observers (renderers, machine progress,
7
+ * telemetry) subscribe; none of them controls execution or keeps a second
8
+ * account of what happened. Publishing is a no-op when no broadcast is
9
+ * provided.
10
+ *
11
+ * @experimental This API is unstable and may change without notice.
12
+ */
13
+ import * as Effect from "effect/Effect";
14
+ import * as PubSub from "effect/PubSub";
15
+ import type * as Scope from "effect/Scope";
16
+ import * as ServiceMap from "effect/Context";
17
+ import type { BlockingClass } from "./plan.js";
18
+ import type { OperationPhase, UnitState } from "./operation-resolution.js";
19
+ export type OperationLifecycleEvent = {
20
+ readonly _tag: "PhaseStarted";
21
+ readonly phase: OperationPhase;
22
+ readonly atNanos: bigint;
23
+ } | {
24
+ readonly _tag: "UnitStarted";
25
+ readonly unitId: string;
26
+ readonly label: string;
27
+ readonly index: number;
28
+ readonly total: number;
29
+ readonly atNanos: bigint;
30
+ } | {
31
+ readonly _tag: "UnitResolved";
32
+ readonly unitId: string;
33
+ readonly label: string;
34
+ readonly state: UnitState;
35
+ readonly index: number;
36
+ readonly total: number;
37
+ readonly atNanos: bigint;
38
+ } | {
39
+ readonly _tag: "Waiting";
40
+ readonly blockingClass: BlockingClass;
41
+ readonly subject: string;
42
+ readonly detail: string;
43
+ readonly atNanos: bigint;
44
+ };
45
+ export interface OperationLifecycleService {
46
+ readonly mode: "preview" | "apply";
47
+ readonly pubsub: PubSub.PubSub<OperationLifecycleEvent>;
48
+ }
49
+ declare const OperationLifecycle_base: ServiceMap.ServiceClass<OperationLifecycle, "@agentxm/workspace-operations/plan/operation-events/OperationLifecycle", OperationLifecycleService>;
50
+ export declare class OperationLifecycle extends OperationLifecycle_base {
51
+ }
52
+ /** Publish one lifecycle event. No-op when no broadcast is provided. */
53
+ export declare const publishLifecycleEvent: (make: (atNanos: bigint) => OperationLifecycleEvent) => Effect.Effect<void>;
54
+ export declare const publishPhaseStarted: (phase: OperationPhase) => Effect.Effect<void>;
55
+ /**
56
+ * Fork a subscriber that observes every lifecycle event published while the
57
+ * scope lives. No-op (returns immediately) when no broadcast is provided.
58
+ */
59
+ export declare const subscribeToLifecycle: (observe: (event: OperationLifecycleEvent) => Effect.Effect<void>) => Effect.Effect<void, never, Scope.Scope>;
60
+ export declare const makeOperationLifecycle: (mode: "preview" | "apply") => Effect.Effect<OperationLifecycleService>;
61
+ export {};
62
+ //# sourceMappingURL=operation-events.d.ts.map
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Operation lifecycle events.
3
+ *
4
+ * Execution publishes typed lifecycle events — phase transitions, unit state
5
+ * transitions, and waiting reasons, each with a monotonic timestamp — to an
6
+ * invocation-scoped broadcast. Observers (renderers, machine progress,
7
+ * telemetry) subscribe; none of them controls execution or keeps a second
8
+ * account of what happened. Publishing is a no-op when no broadcast is
9
+ * provided.
10
+ *
11
+ * @experimental This API is unstable and may change without notice.
12
+ */
13
+ import * as Clock from "effect/Clock";
14
+ import * as Effect from "effect/Effect";
15
+ import * as Option from "effect/Option";
16
+ import * as PubSub from "effect/PubSub";
17
+ import * as ServiceMap from "effect/Context";
18
+ export class OperationLifecycle extends ServiceMap.Service()("@agentxm/workspace-operations/plan/operation-events/OperationLifecycle") {
19
+ }
20
+ /** Publish one lifecycle event. No-op when no broadcast is provided. */
21
+ export const publishLifecycleEvent = (make) => Effect.gen(function* () {
22
+ const service = yield* Effect.serviceOption(OperationLifecycle);
23
+ if (Option.isNone(service))
24
+ return;
25
+ const atNanos = yield* Clock.currentTimeNanos;
26
+ yield* PubSub.publish(service.value.pubsub, make(atNanos));
27
+ });
28
+ export const publishPhaseStarted = (phase) => publishLifecycleEvent((atNanos) => ({ _tag: "PhaseStarted", phase, atNanos }));
29
+ /**
30
+ * Fork a subscriber that observes every lifecycle event published while the
31
+ * scope lives. No-op (returns immediately) when no broadcast is provided.
32
+ */
33
+ export const subscribeToLifecycle = (observe) => Effect.gen(function* () {
34
+ const service = yield* Effect.serviceOption(OperationLifecycle);
35
+ if (Option.isNone(service))
36
+ return;
37
+ const subscription = yield* PubSub.subscribe(service.value.pubsub);
38
+ yield* Effect.forkScoped(Effect.forever(PubSub.take(subscription).pipe(Effect.flatMap(observe))));
39
+ });
40
+ export const makeOperationLifecycle = (mode) => PubSub.unbounded().pipe(Effect.map((pubsub) => ({ mode, pubsub })));
41
+ //# sourceMappingURL=operation-events.js.map
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Operation journal — the invocation-scoped record of a plan-family
3
+ * operation's progress.
4
+ *
5
+ * The resolution boundary writes the frozen candidate's facts at planning,
6
+ * records each phase transition, and records per-unit started and resolved
7
+ * facts as execution reaches them — settlement is recorded before the next
8
+ * interruptible boundary. When an external termination request interrupts the
9
+ * invocation, the lifecycle wrapper reads this journal to resolve the
10
+ * interruption truthfully: which units settled, which were in flight, which
11
+ * were never attempted, and what the durable-state disposition of each is.
12
+ * The journal is invocation-local; nothing here persists past the process.
13
+ *
14
+ * @experimental This API is unstable and may change without notice.
15
+ */
16
+ import * as Effect from "effect/Effect";
17
+ import * as Option from "effect/Option";
18
+ import * as Ref from "effect/Ref";
19
+ import * as ServiceMap from "effect/Context";
20
+ import type { CompletedJobStep, OperationPresentation, PlanRiskCondition } from "./plan.js";
21
+ import type { ReleaseAgeOperationEvidence } from "@agentxm/registry-protocol/unstable/registry/release-age-policy";
22
+ import type { OperationPrecondition } from "./plan.js";
23
+ import type { OperationAtomicity, OperationPhase, ResolvedUnit } from "./operation-resolution.js";
24
+ export interface OperationJournalState {
25
+ readonly name: string;
26
+ readonly description: Option.Option<string>;
27
+ readonly mode: "preview" | "apply";
28
+ readonly candidateId?: string;
29
+ readonly atomicity: OperationAtomicity;
30
+ readonly presentation?: OperationPresentation;
31
+ readonly releaseAge?: ReleaseAgeOperationEvidence;
32
+ readonly preconditions?: ReadonlyArray<OperationPrecondition>;
33
+ readonly riskConditions?: ReadonlyArray<PlanRiskCondition>;
34
+ /** Units of the frozen candidate, in planned states. */
35
+ readonly plannedUnits: ReadonlyArray<ResolvedUnit<unknown>>;
36
+ /** The lifecycle phase the invocation had reached when last recorded. */
37
+ readonly phase: OperationPhase;
38
+ /** Ids of units whose run began, in start order. */
39
+ readonly startedUnitIds: ReadonlyArray<string>;
40
+ /**
41
+ * Settlement facts, recorded the moment each unit resolved — before any
42
+ * interruptible boundary — in execution order. A started unit missing here
43
+ * is in flight: its durable effects are restored by the closure's rollback
44
+ * or unknown, never "not attempted".
45
+ */
46
+ readonly resolved: ReadonlyArray<CompletedJobStep<unknown>>;
47
+ /** True while the apply runs inside a restoring (closure-atomic) guard. */
48
+ readonly restoresOnFailure: boolean;
49
+ }
50
+ export interface OperationJournalService {
51
+ readonly ref: Ref.Ref<Option.Option<OperationJournalState>>;
52
+ }
53
+ declare const OperationJournal_base: ServiceMap.ServiceClass<OperationJournal, "@agentxm/workspace-operations/plan/operation-journal/OperationJournal", OperationJournalService>;
54
+ export declare class OperationJournal extends OperationJournal_base {
55
+ }
56
+ /** Record the operation's frozen facts. No-op when no journal is provided. */
57
+ export declare const recordOperationJournal: (state: OperationJournalState) => Effect.Effect<void>;
58
+ /** Merge updates onto the recorded state. No-op when nothing was recorded. */
59
+ export declare const updateOperationJournal: (update: (state: OperationJournalState) => OperationJournalState) => Effect.Effect<void>;
60
+ /** Record the lifecycle phase the invocation has entered. */
61
+ export declare const recordJournalPhase: (phase: OperationPhase) => Effect.Effect<void>;
62
+ /** Record that a unit's run began. */
63
+ export declare const appendStartedUnit: (unitId: string) => Effect.Effect<void>;
64
+ /** Record one unit's settlement fact. */
65
+ export declare const appendResolvedUnit: (step: CompletedJobStep<unknown>) => Effect.Effect<void>;
66
+ export declare const getOperationJournal: Effect.Effect<Option.Option<OperationJournalState>>;
67
+ export declare const makeOperationJournal: Effect.Effect<OperationJournalService>;
68
+ export {};
69
+ //# sourceMappingURL=operation-journal.d.ts.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Operation journal — the invocation-scoped record of a plan-family
3
+ * operation's progress.
4
+ *
5
+ * The resolution boundary writes the frozen candidate's facts at planning,
6
+ * records each phase transition, and records per-unit started and resolved
7
+ * facts as execution reaches them — settlement is recorded before the next
8
+ * interruptible boundary. When an external termination request interrupts the
9
+ * invocation, the lifecycle wrapper reads this journal to resolve the
10
+ * interruption truthfully: which units settled, which were in flight, which
11
+ * were never attempted, and what the durable-state disposition of each is.
12
+ * The journal is invocation-local; nothing here persists past the process.
13
+ *
14
+ * @experimental This API is unstable and may change without notice.
15
+ */
16
+ import * as Effect from "effect/Effect";
17
+ import * as Option from "effect/Option";
18
+ import * as Ref from "effect/Ref";
19
+ import * as ServiceMap from "effect/Context";
20
+ export class OperationJournal extends ServiceMap.Service()("@agentxm/workspace-operations/plan/operation-journal/OperationJournal") {
21
+ }
22
+ /** Record the operation's frozen facts. No-op when no journal is provided. */
23
+ export const recordOperationJournal = (state) => Effect.gen(function* () {
24
+ const service = yield* Effect.serviceOption(OperationJournal);
25
+ if (Option.isNone(service))
26
+ return;
27
+ yield* Ref.set(service.value.ref, Option.some(state));
28
+ });
29
+ /** Merge updates onto the recorded state. No-op when nothing was recorded. */
30
+ export const updateOperationJournal = (update) => Effect.gen(function* () {
31
+ const service = yield* Effect.serviceOption(OperationJournal);
32
+ if (Option.isNone(service))
33
+ return;
34
+ yield* Ref.update(service.value.ref, Option.map(update));
35
+ });
36
+ /** Record the lifecycle phase the invocation has entered. */
37
+ export const recordJournalPhase = (phase) => updateOperationJournal((state) => ({ ...state, phase }));
38
+ /** Record that a unit's run began. */
39
+ export const appendStartedUnit = (unitId) => updateOperationJournal((state) => ({
40
+ ...state,
41
+ startedUnitIds: [...state.startedUnitIds, unitId],
42
+ }));
43
+ /** Record one unit's settlement fact. */
44
+ export const appendResolvedUnit = (step) => updateOperationJournal((state) => ({ ...state, resolved: [...state.resolved, step] }));
45
+ export const getOperationJournal = Effect.gen(function* () {
46
+ const service = yield* Effect.serviceOption(OperationJournal);
47
+ if (Option.isNone(service))
48
+ return Option.none();
49
+ return yield* Ref.get(service.value.ref);
50
+ });
51
+ export const makeOperationJournal = Ref.make(Option.none()).pipe(Effect.map((ref) => ({ ref })));
52
+ //# sourceMappingURL=operation-journal.js.map