@agentxm/workspace-operations 0.28.5 → 0.28.6

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.
@@ -16,7 +16,7 @@ export { AtomicityClassSchema, OperationOutcomeSchema, OperationPhaseSchema, Uni
16
16
  export type { AtomicityClass, MakeOperationResolutionArgs, OperationAtomicity, OperationBlock, OperationFootprintEntry, OperationInterruption, OperationOutcome, OperationPhase, OperationRecovery, OperationResolution, ResolvedUnit, UnitDisposition, UnitState, UnitStateCounts, } from "./plan/operation-resolution.js";
17
17
  export { OperationJournal, appendResolvedUnit, appendStartedUnit, recordJournalPhase, getOperationJournal, makeOperationJournal, recordOperationJournal, updateOperationJournal, type OperationJournalService, type OperationJournalState, } from "./plan/operation-journal.js";
18
18
  export { applyPlan, type ApplyPlanOptions, type OperationHandler } from "./plan/apply-plan.js";
19
- export { OperationLifecycle, makeOperationLifecycle, publishLifecycleEvent, publishPhaseStarted, subscribeToLifecycle, type OperationLifecycleEvent, type OperationLifecycleService, } from "./plan/operation-events.js";
19
+ export { CurrentOperationUnit, OperationEventSchema, OperationLifecycle, OperationModeSchema, ProgressUnitSchema, SettledOutcomeSchema, awaitDrained, lifecycleEvents, makeOperationLifecycle, makeThrottledUnitProgress, observeUnit, publishOperationEvent, publishPhaseStarted, publishUnitProgress, publishWaitEnded, publishWaiting, settleOperation, subscribeLossless, type ObservedUnit, type OperationEvent, type OperationEventEncoded, type OperationEventInput, type OperationLifecycleService, type OperationMode, type ProgressUnit, type SettledOutcome, } from "./plan/operation-events.js";
20
20
  export { ApprovalRecoveryMissing, CandidateFingerprintFailed, OPERATION_ERROR_CATEGORIES, OperationErrorCategorySchema, PlanInteractionFailed, STALE_CANDIDATE_DETAIL, StaleExecutionCandidate, StepFailure, type OperationErrorCategory, } from "./plan/errors.js";
21
21
  export { previewOrApplyPlan } from "./plan/resolve-plan.js";
22
22
  export { ResolvePlanInteraction, type ApplyConfirmation, type ResolvePlanInteractionService, } from "./plan/resolve-plan-interaction.js";
package/dist/src/index.js CHANGED
@@ -19,7 +19,8 @@ export { AtomicityClassSchema, OperationOutcomeSchema, OperationPhaseSchema, Uni
19
19
  export { OperationJournal, appendResolvedUnit, appendStartedUnit, recordJournalPhase, getOperationJournal, makeOperationJournal, recordOperationJournal, updateOperationJournal, } from "./plan/operation-journal.js";
20
20
  // Apply plan + operation handler registry
21
21
  export { applyPlan } from "./plan/apply-plan.js";
22
- export { OperationLifecycle, makeOperationLifecycle, publishLifecycleEvent, publishPhaseStarted, subscribeToLifecycle, } from "./plan/operation-events.js";
22
+ // Operation lifecycle events the live contract every observer subscribes to.
23
+ export { CurrentOperationUnit, OperationEventSchema, OperationLifecycle, OperationModeSchema, ProgressUnitSchema, SettledOutcomeSchema, awaitDrained, lifecycleEvents, makeOperationLifecycle, makeThrottledUnitProgress, observeUnit, publishOperationEvent, publishPhaseStarted, publishUnitProgress, publishWaitEnded, publishWaiting, settleOperation, subscribeLossless, } from "./plan/operation-events.js";
23
24
  // Serialized error vocabulary and the plan-family tagged errors.
24
25
  export { ApprovalRecoveryMissing, CandidateFingerprintFailed, OPERATION_ERROR_CATEGORIES, OperationErrorCategorySchema, PlanInteractionFailed, STALE_CANDIDATE_DETAIL, StaleExecutionCandidate, StepFailure, } from "./plan/errors.js";
25
26
  // Interactive preview/apply orchestration over the workspace read model.
@@ -1,62 +1,272 @@
1
1
  /**
2
- * Operation lifecycle events.
2
+ * Operation lifecycle events — the one live contract between a running
3
+ * operation and everything that observes it.
3
4
  *
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.
5
+ * An operation publishes schema-backed, typed lifecycle events (operation and
6
+ * phase transitions, unit start, progress, and settlement, waiting reasons, and
7
+ * exactly one terminal settled event) to an invocation-scoped broadcast.
8
+ * Observers the human live frame, the machine event writer, telemetry —
9
+ * subscribe independently; none of them controls execution or keeps a second
10
+ * account of what happened. Events carry identifiers, labels, counts, and
11
+ * states, never presentation phrases; wording belongs to the observer.
10
12
  *
11
- * @experimental This API is unstable and may change without notice.
13
+ * Broadcast policy: the publisher is unbounded and never applies backpressure.
14
+ * Lifecycle events are discrete state transitions, so their count is bounded
15
+ * by the planned units; continuous measurements such as downloaded bytes are
16
+ * throttled at the producer and never published per chunk. Every operation
17
+ * ends with a settled event, and settled output waits on the drain latch that
18
+ * lossless subscribers complete. Publishing is a no-op when no broadcast is
19
+ * provided.
12
20
  */
13
21
  import * as Effect from "effect/Effect";
22
+ import * as Latch from "effect/Latch";
14
23
  import * as PubSub from "effect/PubSub";
24
+ import * as Schema from "effect/Schema";
15
25
  import type * as Scope from "effect/Scope";
16
26
  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
- };
27
+ import * as Stream from "effect/Stream";
28
+ import { BlockingClassSchema } from "./plan.js";
29
+ import { OperationPhaseSchema } from "./operation-resolution.js";
30
+ export declare const OperationModeSchema: Schema.Literals<readonly ["preview", "apply"]>;
31
+ export type OperationMode = typeof OperationModeSchema.Type;
32
+ /**
33
+ * Terminal outcome carried by the settled event: a plan-family outcome, or
34
+ * `completed` for an operation without a plan-family resolution (a read, an
35
+ * upgrade step sequence, an authentication flow) that finished successfully.
36
+ */
37
+ export declare const SettledOutcomeSchema: Schema.Literals<readonly ["previewed", "applied", "no-op", "partial", "failed", "blocked", "cancelled", "interrupted", "completed"]>;
38
+ export type SettledOutcome = typeof SettledOutcomeSchema.Type;
39
+ export declare const ProgressUnitSchema: Schema.Literals<readonly ["bytes", "files", "items"]>;
40
+ export type ProgressUnit = typeof ProgressUnitSchema.Type;
41
+ export declare const OperationStartedEventSchema: Schema.Struct<{
42
+ readonly _tag: Schema.tag<"OperationStarted">;
43
+ readonly operationId: Schema.String;
44
+ readonly name: Schema.String;
45
+ readonly mode: Schema.Literals<readonly ["preview", "apply"]>;
46
+ readonly seq: Schema.Number;
47
+ readonly atMs: Schema.Number;
48
+ }>;
49
+ export declare const PhaseStartedEventSchema: Schema.Struct<{
50
+ readonly _tag: Schema.tag<"PhaseStarted">;
51
+ readonly phase: Schema.Literals<readonly ["resolution", "planning", "preview", "confirmation", "validation", "apply", "restoration"]>;
52
+ readonly seq: Schema.Number;
53
+ readonly atMs: Schema.Number;
54
+ }>;
55
+ export declare const UnitStartedEventSchema: Schema.Struct<{
56
+ readonly _tag: Schema.tag<"UnitStarted">;
57
+ readonly unitId: Schema.String;
58
+ readonly label: Schema.String;
59
+ readonly index: Schema.Number;
60
+ readonly total: Schema.optional<Schema.Number>;
61
+ readonly parentUnitId: Schema.optional<Schema.String>;
62
+ readonly seq: Schema.Number;
63
+ readonly atMs: Schema.Number;
64
+ }>;
65
+ export declare const UnitProgressEventSchema: Schema.Struct<{
66
+ readonly _tag: Schema.tag<"UnitProgress">;
67
+ readonly unitId: Schema.String;
68
+ readonly done: Schema.Number;
69
+ readonly total: Schema.optional<Schema.Number>;
70
+ readonly unit: Schema.Literals<readonly ["bytes", "files", "items"]>;
71
+ readonly seq: Schema.Number;
72
+ readonly atMs: Schema.Number;
73
+ }>;
74
+ export declare const UnitResolvedEventSchema: Schema.Struct<{
75
+ readonly _tag: Schema.tag<"UnitResolved">;
76
+ readonly unitId: Schema.String;
77
+ readonly label: Schema.String;
78
+ readonly state: Schema.Literals<readonly ["planned", "ready", "committed", "unchanged", "failed", "rolled-back", "blocked", "skipped", "cancelled", "interrupted"]>;
79
+ readonly index: Schema.Number;
80
+ readonly total: Schema.optional<Schema.Number>;
81
+ readonly seq: Schema.Number;
82
+ readonly atMs: Schema.Number;
83
+ }>;
84
+ export declare const WaitingEventSchema: Schema.Struct<{
85
+ readonly _tag: Schema.tag<"Waiting">;
86
+ readonly blockingClass: Schema.Literals<readonly ["approval-required", "override-required", "precondition-unmet", "dependency-failed", "dependency-cycle", "stale-candidate", "policy-excluded", "resource-conflict", "external-blocked", "operation-aborted"]>;
87
+ readonly subject: Schema.String;
88
+ readonly detail: Schema.String;
89
+ readonly seq: Schema.Number;
90
+ readonly atMs: Schema.Number;
91
+ }>;
92
+ export declare const WaitEndedEventSchema: Schema.Struct<{
93
+ readonly _tag: Schema.tag<"WaitEnded">;
94
+ readonly subject: Schema.String;
95
+ readonly seq: Schema.Number;
96
+ readonly atMs: Schema.Number;
97
+ }>;
98
+ export declare const OperationSettledEventSchema: Schema.Struct<{
99
+ readonly _tag: Schema.tag<"OperationSettled">;
100
+ readonly outcome: Schema.Literals<readonly ["previewed", "applied", "no-op", "partial", "failed", "blocked", "cancelled", "interrupted", "completed"]>;
101
+ readonly seq: Schema.Number;
102
+ readonly atMs: Schema.Number;
103
+ }>;
104
+ /**
105
+ * The published lifecycle event union. `seq` is strictly increasing within one
106
+ * operation; `atMs` is wall-clock milliseconds. Exactly one `OperationSettled`
107
+ * event ends every operation.
108
+ */
109
+ export declare const OperationEventSchema: Schema.Union<readonly [Schema.Struct<{
110
+ readonly _tag: Schema.tag<"OperationStarted">;
111
+ readonly operationId: Schema.String;
112
+ readonly name: Schema.String;
113
+ readonly mode: Schema.Literals<readonly ["preview", "apply"]>;
114
+ readonly seq: Schema.Number;
115
+ readonly atMs: Schema.Number;
116
+ }>, Schema.Struct<{
117
+ readonly _tag: Schema.tag<"PhaseStarted">;
118
+ readonly phase: Schema.Literals<readonly ["resolution", "planning", "preview", "confirmation", "validation", "apply", "restoration"]>;
119
+ readonly seq: Schema.Number;
120
+ readonly atMs: Schema.Number;
121
+ }>, Schema.Struct<{
122
+ readonly _tag: Schema.tag<"UnitStarted">;
123
+ readonly unitId: Schema.String;
124
+ readonly label: Schema.String;
125
+ readonly index: Schema.Number;
126
+ readonly total: Schema.optional<Schema.Number>;
127
+ readonly parentUnitId: Schema.optional<Schema.String>;
128
+ readonly seq: Schema.Number;
129
+ readonly atMs: Schema.Number;
130
+ }>, Schema.Struct<{
131
+ readonly _tag: Schema.tag<"UnitProgress">;
132
+ readonly unitId: Schema.String;
133
+ readonly done: Schema.Number;
134
+ readonly total: Schema.optional<Schema.Number>;
135
+ readonly unit: Schema.Literals<readonly ["bytes", "files", "items"]>;
136
+ readonly seq: Schema.Number;
137
+ readonly atMs: Schema.Number;
138
+ }>, Schema.Struct<{
139
+ readonly _tag: Schema.tag<"UnitResolved">;
140
+ readonly unitId: Schema.String;
141
+ readonly label: Schema.String;
142
+ readonly state: Schema.Literals<readonly ["planned", "ready", "committed", "unchanged", "failed", "rolled-back", "blocked", "skipped", "cancelled", "interrupted"]>;
143
+ readonly index: Schema.Number;
144
+ readonly total: Schema.optional<Schema.Number>;
145
+ readonly seq: Schema.Number;
146
+ readonly atMs: Schema.Number;
147
+ }>, Schema.Struct<{
148
+ readonly _tag: Schema.tag<"Waiting">;
149
+ readonly blockingClass: Schema.Literals<readonly ["approval-required", "override-required", "precondition-unmet", "dependency-failed", "dependency-cycle", "stale-candidate", "policy-excluded", "resource-conflict", "external-blocked", "operation-aborted"]>;
150
+ readonly subject: Schema.String;
151
+ readonly detail: Schema.String;
152
+ readonly seq: Schema.Number;
153
+ readonly atMs: Schema.Number;
154
+ }>, Schema.Struct<{
155
+ readonly _tag: Schema.tag<"WaitEnded">;
156
+ readonly subject: Schema.String;
157
+ readonly seq: Schema.Number;
158
+ readonly atMs: Schema.Number;
159
+ }>, Schema.Struct<{
160
+ readonly _tag: Schema.tag<"OperationSettled">;
161
+ readonly outcome: Schema.Literals<readonly ["previewed", "applied", "no-op", "partial", "failed", "blocked", "cancelled", "interrupted", "completed"]>;
162
+ readonly seq: Schema.Number;
163
+ readonly atMs: Schema.Number;
164
+ }>]>;
165
+ export type OperationEvent = typeof OperationEventSchema.Type;
166
+ export type OperationEventEncoded = typeof OperationEventSchema.Encoded;
167
+ /** Fields the service fills in for every event it publishes. */
168
+ export type OperationEventInput = (seq: number, atMs: number) => OperationEvent;
45
169
  export interface OperationLifecycleService {
46
- readonly mode: "preview" | "apply";
47
- readonly pubsub: PubSub.PubSub<OperationLifecycleEvent>;
170
+ readonly operationId: string;
171
+ readonly name: string;
172
+ readonly mode: OperationMode;
173
+ /** Unbounded, invocation-scoped broadcast; subscribe before the operation runs. */
174
+ readonly events: PubSub.PubSub<OperationEvent>;
175
+ /** Publish one event; the service assigns `seq` and `atMs` atomically. */
176
+ readonly publish: (make: OperationEventInput) => Effect.Effect<void>;
177
+ /** Next index for a unit whose caller has no planned index. */
178
+ readonly nextUnitIndex: Effect.Effect<number>;
179
+ /** Publish the terminal event once; later calls are no-ops. */
180
+ readonly settle: (outcome: SettledOutcome) => Effect.Effect<void>;
181
+ /** Whether the terminal event was published. */
182
+ readonly settled: Effect.Effect<boolean>;
183
+ /** Opens when the operation settled and every registered lossless subscriber acknowledged. */
184
+ readonly drained: Latch.Latch;
185
+ /**
186
+ * Register a subscriber that must observe every event through settlement.
187
+ * Returns the acknowledgement; run it under `Effect.ensuring` so
188
+ * interruption also acknowledges.
189
+ */
190
+ readonly registerLossless: Effect.Effect<Effect.Effect<void>>;
48
191
  }
49
192
  declare const OperationLifecycle_base: ServiceMap.ServiceClass<OperationLifecycle, "@agentxm/workspace-operations/plan/operation-events/OperationLifecycle", OperationLifecycleService>;
50
193
  export declare class OperationLifecycle extends OperationLifecycle_base {
51
194
  }
195
+ export declare const makeOperationLifecycle: (args: {
196
+ readonly name: string;
197
+ readonly mode: OperationMode;
198
+ }) => Effect.Effect<OperationLifecycleService>;
52
199
  /** 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>;
200
+ export declare const publishOperationEvent: (make: OperationEventInput) => Effect.Effect<void>;
201
+ export declare const publishPhaseStarted: (phase: typeof OperationPhaseSchema.Type) => Effect.Effect<void>;
202
+ export declare const publishWaiting: (wait: {
203
+ readonly blockingClass: typeof BlockingClassSchema.Type;
204
+ readonly subject: string;
205
+ readonly detail: string;
206
+ }) => Effect.Effect<void>;
207
+ export declare const publishWaitEnded: (subject: string) => Effect.Effect<void>;
208
+ declare const CurrentOperationUnit_base: ServiceMap.ServiceClass<CurrentOperationUnit, "@agentxm/workspace-operations/plan/operation-events/CurrentOperationUnit", {
209
+ readonly unitId: string;
210
+ }>;
211
+ /**
212
+ * The unit whose run is in progress, so nested producers (a download inside a
213
+ * step) can attribute continuous progress without threading identifiers.
214
+ */
215
+ export declare class CurrentOperationUnit extends CurrentOperationUnit_base {
216
+ }
217
+ /**
218
+ * Publish a continuous measurement for the current unit. Producers throttle
219
+ * before calling: a download publishes tens of events, never one per chunk.
220
+ * No-op without a broadcast or without a current unit.
221
+ */
222
+ export declare const publishUnitProgress: (progress: {
223
+ readonly done: number;
224
+ readonly total?: number | undefined;
225
+ readonly unit: ProgressUnit;
226
+ }) => Effect.Effect<void>;
227
+ /**
228
+ * Time-gated progress publisher for a producer loop: at most one event per
229
+ * `intervalMs` of wall clock, plus the final measurement when `done` reaches
230
+ * `total`. Keeps continuous measurements to tens of events per unit.
231
+ */
232
+ export declare const makeThrottledUnitProgress: (options: {
233
+ readonly unit: ProgressUnit;
234
+ readonly intervalMs?: number | undefined;
235
+ }) => Effect.Effect<(done: number, total?: number | undefined) => Effect.Effect<void>>;
236
+ export interface ObservedUnit {
237
+ readonly id: string;
238
+ readonly label: string;
239
+ /** Planned position; assigned by the service when the caller has none. */
240
+ readonly index?: number | undefined;
241
+ readonly total?: number | undefined;
242
+ readonly parentUnitId?: string | undefined;
243
+ }
244
+ /**
245
+ * Run one unit of work under the lifecycle: `UnitStarted` before, then
246
+ * `UnitResolved` with the state the exit proves (`committed`, `failed`, or
247
+ * `interrupted`). The unit identity is provided to the run so nested producers
248
+ * can publish progress. No-op wrapper without a broadcast.
249
+ */
250
+ export declare const observeUnit: <A, E, R>(unit: ObservedUnit, effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
251
+ /** Publish the terminal event once. No-op without a broadcast. */
252
+ export declare const settleOperation: (outcome: SettledOutcome) => Effect.Effect<void>;
253
+ /**
254
+ * Wait until every lossless subscriber has consumed the terminal event.
255
+ * Returns immediately when no broadcast is provided or the operation has not
256
+ * settled, so callers cannot deadlock on an operation that never ends.
257
+ */
258
+ export declare const awaitDrained: Effect.Effect<void>;
259
+ /**
260
+ * Subscribe to the broadcast within the current scope. The subscription is
261
+ * created immediately — before this effect returns — so events published
262
+ * afterwards are never missed; the stream ends with the terminal event.
263
+ */
264
+ export declare const lifecycleEvents: (service: OperationLifecycleService) => Effect.Effect<Stream.Stream<OperationEvent>, never, Scope.Scope>;
55
265
  /**
56
- * Fork a subscriber that observes every lifecycle event published while the
57
- * scope lives. No-op (returns immediately) when no broadcast is provided.
266
+ * Fork a subscriber that observes every event through settlement and holds
267
+ * the drain latch until it has processed the terminal event. The subscription
268
+ * attaches before this effect returns. No-op when no broadcast is provided.
58
269
  */
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>;
270
+ export declare const subscribeLossless: (service: OperationLifecycleService, observe: (event: OperationEvent) => Effect.Effect<void>) => Effect.Effect<void, never, Scope.Scope>;
61
271
  export {};
62
272
  //# sourceMappingURL=operation-events.d.ts.map
@@ -1,41 +1,296 @@
1
1
  /**
2
- * Operation lifecycle events.
2
+ * Operation lifecycle events — the one live contract between a running
3
+ * operation and everything that observes it.
3
4
  *
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.
5
+ * An operation publishes schema-backed, typed lifecycle events (operation and
6
+ * phase transitions, unit start, progress, and settlement, waiting reasons, and
7
+ * exactly one terminal settled event) to an invocation-scoped broadcast.
8
+ * Observers the human live frame, the machine event writer, telemetry —
9
+ * subscribe independently; none of them controls execution or keeps a second
10
+ * account of what happened. Events carry identifiers, labels, counts, and
11
+ * states, never presentation phrases; wording belongs to the observer.
10
12
  *
11
- * @experimental This API is unstable and may change without notice.
13
+ * Broadcast policy: the publisher is unbounded and never applies backpressure.
14
+ * Lifecycle events are discrete state transitions, so their count is bounded
15
+ * by the planned units; continuous measurements such as downloaded bytes are
16
+ * throttled at the producer and never published per chunk. Every operation
17
+ * ends with a settled event, and settled output waits on the drain latch that
18
+ * lossless subscribers complete. Publishing is a no-op when no broadcast is
19
+ * provided.
12
20
  */
21
+ import * as Cause from "effect/Cause";
13
22
  import * as Clock from "effect/Clock";
14
23
  import * as Effect from "effect/Effect";
24
+ import * as Exit from "effect/Exit";
25
+ import * as Latch from "effect/Latch";
26
+ import * as MutableRef from "effect/MutableRef";
15
27
  import * as Option from "effect/Option";
16
28
  import * as PubSub from "effect/PubSub";
29
+ import * as Ref from "effect/Ref";
30
+ import * as Schema from "effect/Schema";
17
31
  import * as ServiceMap from "effect/Context";
32
+ import * as Stream from "effect/Stream";
33
+ import { BlockingClassSchema } from "./plan.js";
34
+ import { OperationPhaseSchema, UnitStateSchema } from "./operation-resolution.js";
35
+ // -----------------------------------------------------------------------------
36
+ // Event schema
37
+ // -----------------------------------------------------------------------------
38
+ export const OperationModeSchema = Schema.Literals(["preview", "apply"]).annotate({
39
+ identifier: "OperationMode",
40
+ title: "Operation Mode",
41
+ description: "Whether the operation previews or applies its plan.",
42
+ });
43
+ /**
44
+ * Terminal outcome carried by the settled event: a plan-family outcome, or
45
+ * `completed` for an operation without a plan-family resolution (a read, an
46
+ * upgrade step sequence, an authentication flow) that finished successfully.
47
+ */
48
+ export const SettledOutcomeSchema = Schema.Literals([
49
+ "previewed",
50
+ "applied",
51
+ "no-op",
52
+ "partial",
53
+ "failed",
54
+ "blocked",
55
+ "cancelled",
56
+ "interrupted",
57
+ "completed",
58
+ ]).annotate({
59
+ identifier: "SettledOutcome",
60
+ title: "Settled Outcome",
61
+ description: "Terminal outcome of an observed operation.",
62
+ });
63
+ export const ProgressUnitSchema = Schema.Literals(["bytes", "files", "items"]).annotate({
64
+ identifier: "ProgressUnit",
65
+ title: "Progress Unit",
66
+ description: "Unit of a continuous progress measurement.",
67
+ });
68
+ /** Per-operation monotonic order and wall-clock time, on every event. */
69
+ const EventBase = {
70
+ seq: Schema.Number,
71
+ atMs: Schema.Number,
72
+ };
73
+ export const OperationStartedEventSchema = Schema.TaggedStruct("OperationStarted", {
74
+ ...EventBase,
75
+ operationId: Schema.String,
76
+ name: Schema.String,
77
+ mode: OperationModeSchema,
78
+ }).annotate({ identifier: "OperationStartedEvent" });
79
+ export const PhaseStartedEventSchema = Schema.TaggedStruct("PhaseStarted", {
80
+ ...EventBase,
81
+ phase: OperationPhaseSchema,
82
+ }).annotate({ identifier: "PhaseStartedEvent" });
83
+ export const UnitStartedEventSchema = Schema.TaggedStruct("UnitStarted", {
84
+ ...EventBase,
85
+ unitId: Schema.String,
86
+ label: Schema.String,
87
+ index: Schema.Number,
88
+ total: Schema.optional(Schema.Number),
89
+ parentUnitId: Schema.optional(Schema.String),
90
+ }).annotate({ identifier: "UnitStartedEvent" });
91
+ export const UnitProgressEventSchema = Schema.TaggedStruct("UnitProgress", {
92
+ ...EventBase,
93
+ unitId: Schema.String,
94
+ done: Schema.Number,
95
+ total: Schema.optional(Schema.Number),
96
+ unit: ProgressUnitSchema,
97
+ }).annotate({ identifier: "UnitProgressEvent" });
98
+ export const UnitResolvedEventSchema = Schema.TaggedStruct("UnitResolved", {
99
+ ...EventBase,
100
+ unitId: Schema.String,
101
+ label: Schema.String,
102
+ state: UnitStateSchema,
103
+ index: Schema.Number,
104
+ total: Schema.optional(Schema.Number),
105
+ }).annotate({ identifier: "UnitResolvedEvent" });
106
+ export const WaitingEventSchema = Schema.TaggedStruct("Waiting", {
107
+ ...EventBase,
108
+ blockingClass: BlockingClassSchema,
109
+ subject: Schema.String,
110
+ detail: Schema.String,
111
+ }).annotate({ identifier: "WaitingEvent" });
112
+ export const WaitEndedEventSchema = Schema.TaggedStruct("WaitEnded", {
113
+ ...EventBase,
114
+ subject: Schema.String,
115
+ }).annotate({ identifier: "WaitEndedEvent" });
116
+ export const OperationSettledEventSchema = Schema.TaggedStruct("OperationSettled", {
117
+ ...EventBase,
118
+ outcome: SettledOutcomeSchema,
119
+ }).annotate({ identifier: "OperationSettledEvent" });
120
+ /**
121
+ * The published lifecycle event union. `seq` is strictly increasing within one
122
+ * operation; `atMs` is wall-clock milliseconds. Exactly one `OperationSettled`
123
+ * event ends every operation.
124
+ */
125
+ export const OperationEventSchema = Schema.Union([
126
+ OperationStartedEventSchema,
127
+ PhaseStartedEventSchema,
128
+ UnitStartedEventSchema,
129
+ UnitProgressEventSchema,
130
+ UnitResolvedEventSchema,
131
+ WaitingEventSchema,
132
+ WaitEndedEventSchema,
133
+ OperationSettledEventSchema,
134
+ ]).annotate({
135
+ identifier: "OperationEvent",
136
+ title: "Operation Lifecycle Event",
137
+ description: "One typed lifecycle event of a running AXM operation: operation, phase, unit, waiting, or settlement transition.",
138
+ });
18
139
  export class OperationLifecycle extends ServiceMap.Service()("@agentxm/workspace-operations/plan/operation-events/OperationLifecycle") {
19
140
  }
141
+ const operationCounter = MutableRef.make(0);
142
+ export const makeOperationLifecycle = (args) => Effect.gen(function* () {
143
+ const events = yield* PubSub.unbounded();
144
+ const sequence = yield* Ref.make(0);
145
+ const unitIndex = yield* Ref.make(0);
146
+ const drain = yield* Ref.make({ settled: false, pending: 0 });
147
+ const drained = yield* Latch.make(false);
148
+ const operationId = `operation-${String(MutableRef.incrementAndGet(operationCounter))}`;
149
+ const openWhenDrained = (state) => state.settled && state.pending === 0 ? Effect.asVoid(drained.open) : Effect.void;
150
+ const publish = (make) => Effect.gen(function* () {
151
+ const seq = yield* Ref.modify(sequence, (current) => [current + 1, current + 1]);
152
+ const atMs = yield* Clock.currentTimeMillis;
153
+ yield* PubSub.publish(events, make(seq, atMs));
154
+ });
155
+ const settle = (outcome) => Effect.gen(function* () {
156
+ const first = yield* Ref.modify(drain, (state) => state.settled
157
+ ? [false, state]
158
+ : [true, { ...state, settled: true }]);
159
+ if (!first)
160
+ return;
161
+ yield* publish((seq, atMs) => ({ _tag: "OperationSettled", seq, atMs, outcome }));
162
+ yield* Effect.flatMap(Ref.get(drain), openWhenDrained);
163
+ });
164
+ const registerLossless = Ref.update(drain, (state) => ({
165
+ ...state,
166
+ pending: state.pending + 1,
167
+ })).pipe(Effect.as(Ref.modify(drain, (state) => {
168
+ const next = { ...state, pending: Math.max(0, state.pending - 1) };
169
+ return [next, next];
170
+ }).pipe(Effect.flatMap(openWhenDrained))));
171
+ return {
172
+ operationId,
173
+ name: args.name,
174
+ mode: args.mode,
175
+ events,
176
+ publish,
177
+ nextUnitIndex: Ref.modify(unitIndex, (current) => [current, current + 1]),
178
+ settle,
179
+ settled: Effect.map(Ref.get(drain), (state) => state.settled),
180
+ drained,
181
+ registerLossless,
182
+ };
183
+ });
184
+ // -----------------------------------------------------------------------------
185
+ // Producers (no-ops without a broadcast)
186
+ // -----------------------------------------------------------------------------
187
+ const withLifecycle = (onSome, onNone) => Effect.flatMap(Effect.serviceOption(OperationLifecycle), (service) => Option.isNone(service) ? onNone() : onSome(service.value));
20
188
  /** 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))
189
+ export const publishOperationEvent = (make) => withLifecycle((service) => service.publish(make), () => Effect.void);
190
+ export const publishPhaseStarted = (phase) => publishOperationEvent((seq, atMs) => ({ _tag: "PhaseStarted", seq, atMs, phase }));
191
+ export const publishWaiting = (wait) => publishOperationEvent((seq, atMs) => ({ _tag: "Waiting", seq, atMs, ...wait }));
192
+ export const publishWaitEnded = (subject) => publishOperationEvent((seq, atMs) => ({ _tag: "WaitEnded", seq, atMs, subject }));
193
+ /**
194
+ * The unit whose run is in progress, so nested producers (a download inside a
195
+ * step) can attribute continuous progress without threading identifiers.
196
+ */
197
+ export class CurrentOperationUnit extends ServiceMap.Service()("@agentxm/workspace-operations/plan/operation-events/CurrentOperationUnit") {
198
+ }
199
+ /**
200
+ * Publish a continuous measurement for the current unit. Producers throttle
201
+ * before calling: a download publishes tens of events, never one per chunk.
202
+ * No-op without a broadcast or without a current unit.
203
+ */
204
+ export const publishUnitProgress = (progress) => Effect.flatMap(Effect.serviceOption(CurrentOperationUnit), (unit) => Option.isNone(unit)
205
+ ? Effect.void
206
+ : publishOperationEvent((seq, atMs) => ({
207
+ _tag: "UnitProgress",
208
+ seq,
209
+ atMs,
210
+ unitId: unit.value.unitId,
211
+ done: progress.done,
212
+ ...(progress.total === undefined ? {} : { total: progress.total }),
213
+ unit: progress.unit,
214
+ })));
215
+ /**
216
+ * Time-gated progress publisher for a producer loop: at most one event per
217
+ * `intervalMs` of wall clock, plus the final measurement when `done` reaches
218
+ * `total`. Keeps continuous measurements to tens of events per unit.
219
+ */
220
+ export const makeThrottledUnitProgress = (options) => Effect.map(Ref.make(-Infinity), (last) => (done, total) => Effect.gen(function* () {
221
+ const now = yield* Clock.currentTimeMillis;
222
+ const previous = yield* Ref.get(last);
223
+ const final = total !== undefined && done >= total;
224
+ if (!final && now - previous < (options.intervalMs ?? 100))
24
225
  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 }));
226
+ yield* Ref.set(last, now);
227
+ yield* publishUnitProgress({ done, unit: options.unit, total });
228
+ }));
229
+ const unitStateForExit = (exit) => Exit.isSuccess(exit)
230
+ ? "committed"
231
+ : Cause.hasInterruptsOnly(exit.cause)
232
+ ? "interrupted"
233
+ : "failed";
29
234
  /**
30
- * Fork a subscriber that observes every lifecycle event published while the
31
- * scope lives. No-op (returns immediately) when no broadcast is provided.
235
+ * Run one unit of work under the lifecycle: `UnitStarted` before, then
236
+ * `UnitResolved` with the state the exit proves (`committed`, `failed`, or
237
+ * `interrupted`). The unit identity is provided to the run so nested producers
238
+ * can publish progress. No-op wrapper without a broadcast.
32
239
  */
33
- export const subscribeToLifecycle = (observe) => Effect.gen(function* () {
34
- const service = yield* Effect.serviceOption(OperationLifecycle);
240
+ export const observeUnit = (unit, effect) => Effect.flatMap(Effect.serviceOption(OperationLifecycle), (service) => {
35
241
  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))));
242
+ return effect;
243
+ const lifecycle = service.value;
244
+ return Effect.gen(function* () {
245
+ const index = unit.index ?? (yield* lifecycle.nextUnitIndex);
246
+ const total = unit.total === undefined ? {} : { total: unit.total };
247
+ yield* lifecycle.publish((seq, atMs) => ({
248
+ _tag: "UnitStarted",
249
+ seq,
250
+ atMs,
251
+ unitId: unit.id,
252
+ label: unit.label,
253
+ index,
254
+ ...total,
255
+ ...(unit.parentUnitId === undefined ? {} : { parentUnitId: unit.parentUnitId }),
256
+ }));
257
+ return yield* effect.pipe(Effect.provideService(CurrentOperationUnit, { unitId: unit.id }), Effect.onExit((exit) => lifecycle.publish((seq, atMs) => ({
258
+ _tag: "UnitResolved",
259
+ seq,
260
+ atMs,
261
+ unitId: unit.id,
262
+ label: unit.label,
263
+ state: unitStateForExit(exit),
264
+ index,
265
+ ...total,
266
+ }))));
267
+ });
268
+ });
269
+ /** Publish the terminal event once. No-op without a broadcast. */
270
+ export const settleOperation = (outcome) => withLifecycle((service) => service.settle(outcome), () => Effect.void);
271
+ /**
272
+ * Wait until every lossless subscriber has consumed the terminal event.
273
+ * Returns immediately when no broadcast is provided or the operation has not
274
+ * settled, so callers cannot deadlock on an operation that never ends.
275
+ */
276
+ export const awaitDrained = withLifecycle((service) => Effect.flatMap(service.settled, (settled) => (settled ? service.drained.await : Effect.void)), () => Effect.void);
277
+ // -----------------------------------------------------------------------------
278
+ // Subscribers
279
+ // -----------------------------------------------------------------------------
280
+ /**
281
+ * Subscribe to the broadcast within the current scope. The subscription is
282
+ * created immediately — before this effect returns — so events published
283
+ * afterwards are never missed; the stream ends with the terminal event.
284
+ */
285
+ export const lifecycleEvents = (service) => Effect.map(PubSub.subscribe(service.events), (subscription) => Stream.fromSubscription(subscription).pipe(Stream.takeUntil((event) => event._tag === "OperationSettled")));
286
+ /**
287
+ * Fork a subscriber that observes every event through settlement and holds
288
+ * the drain latch until it has processed the terminal event. The subscription
289
+ * attaches before this effect returns. No-op when no broadcast is provided.
290
+ */
291
+ export const subscribeLossless = (service, observe) => Effect.gen(function* () {
292
+ const ack = yield* service.registerLossless;
293
+ const stream = yield* lifecycleEvents(service);
294
+ yield* stream.pipe(Stream.runForEach(observe), Effect.ensuring(ack), Effect.forkScoped);
39
295
  });
40
- export const makeOperationLifecycle = (mode) => PubSub.unbounded().pipe(Effect.map((pubsub) => ({ mode, pubsub })));
41
296
  //# sourceMappingURL=operation-events.js.map
@@ -27,7 +27,7 @@ import type { ConfiguredAgentOutcome } from "@agentxm/workspace-state";
27
27
  export declare const UnitStateSchema: Schema.Literals<readonly ["planned", "ready", "committed", "unchanged", "failed", "rolled-back", "blocked", "skipped", "cancelled", "interrupted"]>;
28
28
  export type UnitState = typeof UnitStateSchema.Type;
29
29
  /** Lifecycle phase in which an operation event (blocking, waiting) occurred. */
30
- export declare const OperationPhaseSchema: Schema.Literals<readonly ["planning", "preview", "confirmation", "validation", "apply", "restoration"]>;
30
+ export declare const OperationPhaseSchema: Schema.Literals<readonly ["resolution", "planning", "preview", "confirmation", "validation", "apply", "restoration"]>;
31
31
  export type OperationPhase = typeof OperationPhaseSchema.Type;
32
32
  /** Canonical operation terminal outcomes. */
33
33
  export declare const OperationOutcomeSchema: Schema.Literals<readonly ["previewed", "applied", "no-op", "partial", "failed", "blocked", "cancelled", "interrupted"]>;
@@ -41,6 +41,8 @@ export const UnitStateSchema = Schema.Literals([
41
41
  });
42
42
  /** Lifecycle phase in which an operation event (blocking, waiting) occurred. */
43
43
  export const OperationPhaseSchema = Schema.Literals([
44
+ // Resolving requested sources into concrete packages, before planning.
45
+ "resolution",
44
46
  "planning",
45
47
  "preview",
46
48
  "confirmation",
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Plan-resolution interaction port.
3
3
  *
4
- * `previewOrApplyPlan` presents candidates, reports progress, and obtains the
5
- * apply confirmation exclusively through this service. The CLI runtime
6
- * provides the renderer- and prompt-backed implementation; wording, verbosity
7
- * gating, and progress presentation belong to that implementation, never to
8
- * the kernel.
4
+ * `previewOrApplyPlan` presents candidates and obtains the apply confirmation
5
+ * exclusively through this service. The CLI runtime provides the renderer- and
6
+ * prompt-backed implementation; wording and verbosity gating belong to that
7
+ * implementation, never to the kernel. Progress is not an interaction: the
8
+ * kernel publishes typed lifecycle events (`plan/operation-events`) that
9
+ * observers render.
9
10
  *
10
11
  * @experimental This API is unstable and may change without notice.
11
12
  */
12
13
  import * as Effect from "effect/Effect";
13
14
  import * as Layer from "effect/Layer";
14
- import * as Option from "effect/Option";
15
15
  import * as ServiceMap from "effect/Context";
16
16
  import type { PlanInteractionFailed } from "./errors.js";
17
17
  import type { ConfirmationRecovery } from "./plan-execution.js";
@@ -34,19 +34,6 @@ export interface ResolvePlanInteractionService {
34
34
  readonly presentPlan: (plan: Plan<unknown, unknown>, options: {
35
35
  readonly mode: "preview" | "apply";
36
36
  }) => Effect.Effect<void>;
37
- /** Progress envelope for lockfile reconciliation. */
38
- readonly withPlanningProgress: <A, E, R>(planName: string, run: () => Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
39
- /**
40
- * Progress envelope for apply. The implementation subscribes to the
41
- * operation lifecycle stream itself (`plan/operation-events`) and maps unit
42
- * and restoration events to progress updates.
43
- */
44
- readonly withApplyProgress: <A, E, R>(planName: string, run: () => Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
45
- /** Transition-lock contention notice. */
46
- readonly noteTransitionWait: (holder: Option.Option<{
47
- readonly command: string;
48
- readonly pid: number;
49
- }>) => Effect.Effect<void>;
50
37
  }
51
38
  declare const ResolvePlanInteraction_base: ServiceMap.ServiceClass<ResolvePlanInteraction, "@agentxm/workspace-operations/plan/resolve-plan-interaction/ResolvePlanInteraction", ResolvePlanInteractionService>;
52
39
  export declare class ResolvePlanInteraction extends ResolvePlanInteraction_base {
@@ -57,9 +44,6 @@ export interface ResolvePlanInteractionTestState {
57
44
  readonly planName: string;
58
45
  readonly mode: "preview" | "apply";
59
46
  }>;
60
- readonly planningProgress: Array<string>;
61
- readonly applyProgress: Array<string>;
62
- transitionWaits: number;
63
47
  }
64
48
  export declare const ResolvePlanInteractionTest: (overrides?: {
65
49
  readonly isConfirmationAvailable?: boolean;
@@ -67,10 +51,6 @@ export declare const ResolvePlanInteractionTest: (overrides?: {
67
51
  readonly presentPlan?: (plan: Plan<unknown, unknown>, options: {
68
52
  readonly mode: "preview" | "apply";
69
53
  }) => Effect.Effect<void>;
70
- readonly noteTransitionWait?: (holder: Option.Option<{
71
- readonly command: string;
72
- readonly pid: number;
73
- }>) => Effect.Effect<void>;
74
54
  }) => {
75
55
  layer: Layer.Layer<ResolvePlanInteraction, never, never>;
76
56
  state: ResolvePlanInteractionTestState;
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Plan-resolution interaction port.
3
3
  *
4
- * `previewOrApplyPlan` presents candidates, reports progress, and obtains the
5
- * apply confirmation exclusively through this service. The CLI runtime
6
- * provides the renderer- and prompt-backed implementation; wording, verbosity
7
- * gating, and progress presentation belong to that implementation, never to
8
- * the kernel.
4
+ * `previewOrApplyPlan` presents candidates and obtains the apply confirmation
5
+ * exclusively through this service. The CLI runtime provides the renderer- and
6
+ * prompt-backed implementation; wording and verbosity gating belong to that
7
+ * implementation, never to the kernel. Progress is not an interaction: the
8
+ * kernel publishes typed lifecycle events (`plan/operation-events`) that
9
+ * observers render.
9
10
  *
10
11
  * @experimental This API is unstable and may change without notice.
11
12
  */
12
13
  import * as Effect from "effect/Effect";
13
14
  import * as Layer from "effect/Layer";
14
- import * as Option from "effect/Option";
15
15
  import * as ServiceMap from "effect/Context";
16
16
  export class ResolvePlanInteraction extends ServiceMap.Service()("@agentxm/workspace-operations/plan/resolve-plan-interaction/ResolvePlanInteraction") {
17
17
  }
@@ -19,9 +19,6 @@ export const ResolvePlanInteractionTest = (overrides) => {
19
19
  const state = {
20
20
  confirmApplyChangesCalls: [],
21
21
  presentPlanCalls: [],
22
- planningProgress: [],
23
- applyProgress: [],
24
- transitionWaits: 0,
25
22
  };
26
23
  const layer = Layer.succeed(ResolvePlanInteraction, {
27
24
  isConfirmationAvailable: Effect.succeed(overrides?.isConfirmationAvailable ?? false),
@@ -34,18 +31,6 @@ export const ResolvePlanInteractionTest = (overrides) => {
34
31
  state.presentPlanCalls.push({ planName: plan.name, mode: options.mode });
35
32
  yield* overrides?.presentPlan?.(plan, options) ?? Effect.void;
36
33
  }),
37
- withPlanningProgress: (planName, run) => Effect.suspend(() => {
38
- state.planningProgress.push(planName);
39
- return run();
40
- }),
41
- withApplyProgress: (planName, run) => Effect.suspend(() => {
42
- state.applyProgress.push(planName);
43
- return run();
44
- }),
45
- noteTransitionWait: (holder) => Effect.gen(function* () {
46
- state.transitionWaits += 1;
47
- yield* overrides?.noteTransitionWait?.(holder) ?? Effect.void;
48
- }),
49
34
  });
50
35
  return { layer, state };
51
36
  };
@@ -18,6 +18,7 @@ import * as Path from "effect/Path";
18
18
  import * as Effect from "effect/Effect";
19
19
  import * as Layer from "effect/Layer";
20
20
  import * as Option from "effect/Option";
21
+ import * as Ref from "effect/Ref";
21
22
  import { ApprovalRecoveryMissing, STALE_CANDIDATE_DETAIL, StaleExecutionCandidate, StepFailure, } from "./errors.js";
22
23
  import { applyPlan } from "./apply-plan.js";
23
24
  import { isExecutionCandidateFresh, makeExecutionCandidate, } from "./execution-candidate.js";
@@ -25,7 +26,7 @@ import { augmentPlanWithReconciliation } from "../operations/augment-plan.js";
25
26
  import { scanPlanReadiness } from "../operations/scan-plan-readiness.js";
26
27
  import { declaredAtomicity, executedUnits, makeOperationResolution, plannedUnits, unitIdOf, } from "./operation-resolution.js";
27
28
  import { appendResolvedUnit, appendStartedUnit, recordJournalPhase, recordOperationJournal, } from "./operation-journal.js";
28
- import { publishLifecycleEvent, publishPhaseStarted } from "./operation-events.js";
29
+ import { CurrentOperationUnit, observeUnit, publishOperationEvent, publishPhaseStarted, publishWaitEnded, publishWaiting, } from "./operation-events.js";
29
30
  import { WorkspaceMutations } from "@agentxm/workspace-state";
30
31
  import { readPendingClosureRestorationFailures, WorkspaceRestorationIncomplete, } from "@agentxm/workspace-state";
31
32
  import { rollbackWorkspaceClosure, settleWorkspaceClosure, withWorkspaceClosure, } from "../operations/transaction.js";
@@ -34,7 +35,6 @@ import { InterruptionSignalSource } from "./interruption-signal.js";
34
35
  import { ResolvePlanInteraction } from "./resolve-plan-interaction.js";
35
36
  import { confirmationRecoverySuggestions, namedPolicyRecoverySuggestions, } from "./plan-execution.js";
36
37
  import { ConfiguredAgentOutcomesProvider } from "@agentxm/workspace-state";
37
- import { isMcpServerApplicableToAgent } from "@agentxm/workspace-state";
38
38
  import { configuredAgentLifecycleOutcomes } from "@agentxm/workspace-state";
39
39
  import { candidateFingerprintFailedToStepFailure, configuredAgentOutcomesUnavailableToStepFailure, restorationIncompleteToStepFailure, workspaceStateReadFailureToStepFailure, workspaceTransactionFailureToStepFailure, } from "./step-failure-conversions.js";
40
40
  /** Publish a phase transition to the lifecycle stream and the journal. */
@@ -107,19 +107,15 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
107
107
  const fsLayer = Layer.mergeAll(Layer.succeed(FileSystem.FileSystem, fs), Layer.succeed(Path.Path, path));
108
108
  const mode = options.execution.request.mode;
109
109
  yield* publishPhaseStarted("planning");
110
- // Step 1: Lockfile reconciliation
111
- const augmented = yield* interaction.withPlanningProgress(plan.name, () => augmentPlanWithReconciliation(plan, () => ws.getLockfileState()));
110
+ // Step 1: Lockfile reconciliation, observed as one planning unit.
111
+ const augmented = yield* observeUnit({ id: "lockfile-reconciliation", label: "lockfile reconciliation" }, augmentPlanWithReconciliation(plan, () => ws.getLockfileState()));
112
112
  const operations = options.execution.configuredAgentOperations ?? [];
113
113
  const configuredAgents = operations.length === 0 ? [] : yield* ws.getConfiguredAgents();
114
- const configuredMcpServers = operations.some(({ extensionType }) => extensionType === "mcp-server")
115
- ? yield* ws.getConfiguredMcpServerEntries()
116
- : {};
117
114
  const outcomesProvider = yield* Effect.serviceOption(ConfiguredAgentOutcomesProvider);
118
115
  const outcomesOverrideFor = (extensionType) => Option.isSome(outcomesProvider)
119
116
  ? outcomesProvider.value.byExtensionType[extensionType]
120
117
  : undefined;
121
118
  const outcomesFor = (operation, state) => {
122
- const mcpEntry = operation.extensionType === "mcp-server" ? configuredMcpServers[operation.name] : undefined;
123
119
  const generic = configuredAgentLifecycleOutcomes({
124
120
  type: operation.extensionType,
125
121
  name: operation.name,
@@ -129,11 +125,6 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
129
125
  targetState: operation.plannedState,
130
126
  installed: state === "projected",
131
127
  observedAgentIds: state === "projected" ? configuredAgents : [],
132
- ...(mcpEntry === undefined
133
- ? {}
134
- : {
135
- applicableAgentIds: configuredAgents.filter((agentId) => isMcpServerApplicableToAgent(mcpEntry, agentId)),
136
- }),
137
128
  });
138
129
  const override = outcomesOverrideFor(operation.extensionType);
139
130
  if (operation.plannedState === "enabled" && override !== undefined) {
@@ -350,19 +341,23 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
350
341
  ...job,
351
342
  steps: job.steps.map((step) => step.readiness === "error"
352
343
  ? step
353
- : { ...step, run: withWorkspaceClosure(unitIdOf(step))(step.run) }),
344
+ : {
345
+ ...step,
346
+ run: withWorkspaceClosure(unitIdOf(step))(step.run).pipe(Effect.provideService(CurrentOperationUnit, { unitId: unitIdOf(step) })),
347
+ }),
354
348
  })),
355
349
  };
356
350
  return yield* applyPlan(closureScopedPlan, {
357
351
  // The started fact is journaled before the run's first effect, so an
358
352
  // interruption mid-run reports the unit in flight, never not attempted.
359
- onStepStarted: (step) => appendStartedUnit(unitIdOf(step)).pipe(Effect.andThen(publishLifecycleEvent((atNanos) => ({
353
+ onStepStarted: (step) => appendStartedUnit(unitIdOf(step)).pipe(Effect.andThen(publishOperationEvent((seq, atMs) => ({
360
354
  _tag: "UnitStarted",
355
+ seq,
356
+ atMs,
361
357
  unitId: unitIdOf(step),
362
358
  label: step.label,
363
359
  index: startedUnits++,
364
360
  total: totalUnits,
365
- atNanos,
366
361
  })))),
367
362
  // Settlement runs before the next interruptible boundary: the journal
368
363
  // fact and the closure's snapshot disposition are recorded together —
@@ -370,14 +365,15 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
370
365
  // itself, and later ready closures continue.
371
366
  onStepCompleted: (step) => appendResolvedUnit(step).pipe(Effect.andThen(step.result.result === "error"
372
367
  ? rollbackWorkspaceClosure(unitIdOf(step))
373
- : settleWorkspaceClosure(unitIdOf(step))), Effect.andThen(publishLifecycleEvent((atNanos) => ({
368
+ : settleWorkspaceClosure(unitIdOf(step))), Effect.andThen(publishOperationEvent((seq, atMs) => ({
374
369
  _tag: "UnitResolved",
370
+ seq,
371
+ atMs,
375
372
  unitId: unitIdOf(step),
376
373
  label: step.label,
377
374
  state: resolvedUnitState(step),
378
375
  index: resolvedUnits++,
379
376
  total: totalUnits,
380
- atNanos,
381
377
  })))),
382
378
  });
383
379
  });
@@ -504,19 +500,32 @@ export const previewOrApplyPlan = Effect.fn("previewOrApplyPlan")(function* (pla
504
500
  ? failure
505
501
  : { error: workspaceTransactionFailureToStepFailure(failure) };
506
502
  }));
503
+ const transitionSubject = "workspace-transition";
507
504
  const applyResult = yield* Effect.scoped(Effect.gen(function* () {
505
+ const waited = yield* Ref.make(false);
508
506
  const contention = yield* ws.acquireTransition({
509
507
  command: applyExecution.approvalRecovery.command.join(" "),
510
508
  candidateId: candidate.id,
511
- onWaiting: (holder) => interaction.noteTransitionWait(holder),
509
+ // Contention is a first-class lifecycle fact: observers render the
510
+ // wait and its holder; nothing here decides how it is worded.
511
+ onWaiting: (holder) => Ref.set(waited, true).pipe(Effect.andThen(publishWaiting({
512
+ blockingClass: "resource-conflict",
513
+ subject: transitionSubject,
514
+ detail: Option.match(holder, {
515
+ onNone: () => "another operation",
516
+ onSome: (value) => `${value.command} (pid ${String(value.pid)})`,
517
+ }),
518
+ }))),
512
519
  });
520
+ if (yield* Ref.get(waited))
521
+ yield* publishWaitEnded(transitionSubject);
513
522
  if (Option.isSome(contention)) {
514
523
  return { type: "contention", contention: contention.value };
515
524
  }
516
- return yield* interaction.withApplyProgress(candidatePlan.name, () => guardedApply.pipe(Effect.match({
525
+ return yield* guardedApply.pipe(Effect.match({
517
526
  onFailure: (error) => ({ type: "failure", error }),
518
527
  onSuccess: (value) => ({ type: "success", value }),
519
- })));
528
+ }));
520
529
  }));
521
530
  if (applyResult.type === "contention") {
522
531
  const reference = Option.match(applyResult.contention.holder, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentxm/workspace-operations",
3
- "version": "0.28.5",
3
+ "version": "0.28.6",
4
4
  "description": "AXM workspace operations kernel: plans, execution candidates, operation resolutions, and workspace transactions for the axm CLI. Unstable and unsupported — use the axm.sh CLI.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-MIT",
@@ -48,9 +48,9 @@
48
48
  "dependencies": {
49
49
  "effect": "4.0.0-rc.112",
50
50
  "proper-lockfile": "^4.1.2",
51
- "@agentxm/extension-model": "^0.28.5",
52
- "@agentxm/registry-protocol": "^0.28.5",
53
- "@agentxm/workspace-state": "^0.28.5"
51
+ "@agentxm/extension-model": "^0.28.6",
52
+ "@agentxm/workspace-state": "^0.28.6",
53
+ "@agentxm/registry-protocol": "^0.28.6"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@effect/platform-node": "4.0.0-rc.112",