@effect-agent/platform-node 0.1.0-beta.8 → 0.1.0-beta.81

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 (37) hide show
  1. package/dist/NodeDurableAgentRuntime-CFElFzNF.d.mts +179 -0
  2. package/dist/NodeDurableAgentRuntime.d.mts +2 -0
  3. package/dist/NodeDurableAgentRuntime.mjs +221 -0
  4. package/dist/NodeDurableAgentRuntime.mjs.map +1 -0
  5. package/dist/NodeDurableHost-BMSajuQn.mjs +194 -0
  6. package/dist/NodeDurableHost-BMSajuQn.mjs.map +1 -0
  7. package/dist/NodeDurableHost.d.mts +168 -0
  8. package/dist/NodeDurableHost.mjs +2 -0
  9. package/dist/NodeScheduling.d.mts +22 -0
  10. package/dist/NodeScheduling.mjs +59 -0
  11. package/dist/NodeScheduling.mjs.map +1 -0
  12. package/dist/NodeSubscriptions.d.mts +27 -0
  13. package/dist/NodeSubscriptions.mjs +74 -0
  14. package/dist/NodeSubscriptions.mjs.map +1 -0
  15. package/dist/NodeWakeScheduler-4ZeXYwPu.d.mts +27 -0
  16. package/dist/NodeWakeScheduler.d.mts +2 -0
  17. package/dist/NodeWakeScheduler.mjs +65 -0
  18. package/dist/NodeWakeScheduler.mjs.map +1 -0
  19. package/dist/NodeWorkflow.d.mts +27 -0
  20. package/dist/NodeWorkflow.mjs +159 -0
  21. package/dist/NodeWorkflow.mjs.map +1 -0
  22. package/dist/index.d.mts +6 -214
  23. package/dist/index.mjs +6 -286
  24. package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
  25. package/package.json +1 -45
  26. package/src/NodeDurableAgentRuntime.ts +596 -0
  27. package/src/NodeDurableHost.ts +361 -0
  28. package/src/NodeScheduling.ts +121 -0
  29. package/src/NodeSubscriptions.ts +162 -0
  30. package/src/{wake-scheduler.ts → NodeWakeScheduler.ts} +32 -17
  31. package/src/NodeWorkflow.ts +258 -0
  32. package/src/index.ts +5 -3
  33. package/src/internal/message-delivery.ts +53 -0
  34. package/src/internal/prepared-admission.ts +81 -0
  35. package/dist/index.mjs.map +0 -1
  36. package/src/host.ts +0 -215
  37. package/src/layers.ts +0 -381
@@ -1,6 +1,8 @@
1
- import type { ConversationId } from "@effect-agent/core";
2
- import { SubmissionLedger, WakeScheduler } from "@effect-agent/session";
3
- import { Context, Duration, Effect, Layer, PubSub, Stream } from "effect";
1
+ import { type ThreadId } from "@effect-agent/core/Identifiers";
2
+ import { SubmissionLedger } from "@effect-agent/thread/SubmissionLedger";
3
+ import { makeWakeSubscriptionHub, WakeScheduler } from "@effect-agent/thread/WakeScheduler";
4
+ import type { Duration } from "effect";
5
+ import { Context, Effect, Layer, PubSub, Stream } from "effect";
4
6
 
5
7
  /**
6
8
  * Bounded in-process wake buffer. Wake hints are droppable by contract (the ledger-scan fallback
@@ -12,7 +14,7 @@ const WAKE_BUFFER_CAPACITY = 1_024;
12
14
  export class NodeWakeSchedulerConfig extends Context.Service<
13
15
  NodeWakeSchedulerConfig,
14
16
  {
15
- /** Interval between ledger scans that re-emit every nonterminal Conversation lane. */
17
+ /** Interval between ledger scans that re-emit every nonterminal Thread lane. */
16
18
  readonly scanInterval: Duration.Duration;
17
19
  }
18
20
  >()("@effect-agent/platform-node/NodeWakeSchedulerConfig") {
@@ -26,49 +28,62 @@ export class NodeWakeSchedulerConfig extends Context.Service<
26
28
  const makeWakeScheduler = Effect.gen(function* () {
27
29
  const ledger = yield* SubmissionLedger;
28
30
  const config = yield* NodeWakeSchedulerConfig;
29
- const hints = yield* PubSub.sliding<ConversationId>(WAKE_BUFFER_CAPACITY);
31
+ const hints = yield* PubSub.sliding<ThreadId>(WAKE_BUFFER_CAPACITY);
32
+ const progress = yield* makeWakeSubscriptionHub;
33
+
30
34
  yield* Effect.addFinalizer(() => PubSub.shutdown(hints));
31
35
 
32
36
  /**
33
- * One fallback scan: every Conversation lane with nonterminal work, deduplicated. A scan
37
+ * One fallback scan: every Thread lane with nonterminal work, deduplicated. A scan
34
38
  * failure degrades to "no hints this round" — the wake channel has no error contract and the
35
39
  * next round retries — but is logged so a persistently failing ledger stays visible.
36
40
  */
37
- const scanOnce: Effect.Effect<ReadonlyArray<ConversationId>> = Stream.runCollect(
41
+ const scanOnce: Effect.Effect<ReadonlyArray<ThreadId>> = Stream.runCollect(
38
42
  ledger.scanNonterminal,
39
43
  ).pipe(
40
44
  Effect.map((snapshots) => {
41
- const lanes = new Set<ConversationId>();
45
+ const lanes = new Set<ThreadId>();
46
+
42
47
  for (const snapshot of snapshots) {
43
- lanes.add(snapshot.conversationId);
48
+ lanes.add(snapshot.threadId);
44
49
  }
50
+
45
51
  return [...lanes];
46
52
  }),
47
53
  Effect.catch((error) =>
48
54
  Effect.logWarning("NodeWakeScheduler fallback scan failed", error).pipe(
49
- Effect.as([] as ReadonlyArray<ConversationId>),
55
+ Effect.as([] as ReadonlyArray<ThreadId>),
50
56
  ),
51
57
  ),
52
58
  );
53
59
 
54
60
  /**
55
61
  * Deployment §3: correctness must not depend on in-memory notifications, so every `wakes` run
56
- * merges its PubSub subscription with a Clock-driven ledger-scan loop. Both live entirely in
57
- * the consuming run's Scope; no fiber outlives its subscriber.
62
+ * merges its PubSub subscription with shared Clock-driven ledger scans. Share whole snapshots:
63
+ * sliding individual lanes would strand the beginning of scans larger than the hint buffer.
64
+ * The scan starts with the first subscriber and stops when the last subscriber leaves.
58
65
  */
59
- const fallbackScans: Stream.Stream<ConversationId> = Stream.fromIterableEffectRepeat(
60
- Effect.sleep(config.scanInterval).pipe(Effect.andThen(scanOnce)),
66
+ const fallbackScans = yield* Stream.share(
67
+ Stream.fromEffectRepeat(Effect.sleep(config.scanInterval).pipe(Effect.andThen(scanOnce))),
68
+ { capacity: 1, strategy: "sliding" },
61
69
  );
62
70
 
63
71
  return WakeScheduler.of({
64
- notify: (conversationId) => PubSub.publish(hints, conversationId).pipe(Effect.asVoid),
65
- wakes: Stream.merge(Stream.fromPubSub(hints), fallbackScans),
72
+ notify: (threadId) =>
73
+ progress
74
+ .notify(threadId)
75
+ .pipe(Effect.andThen(PubSub.publish(hints, threadId)), Effect.asVoid),
76
+ subscribe: progress.subscribe,
77
+ wakes: Stream.merge(
78
+ Stream.fromPubSub(hints),
79
+ fallbackScans.pipe(Stream.flatMap(Stream.fromIterable)),
80
+ ),
66
81
  });
67
82
  });
68
83
 
69
84
  /**
70
85
  * In-process Node `WakeScheduler`: `notify` publishes to a bounded sliding PubSub for prompt
71
- * same-process wakeups, and every `wakes` subscription additionally runs a periodic
86
+ * same-process wakeups, and active `wakes` subscriptions share one periodic
72
87
  * `SubmissionLedger.scanNonterminal` fallback so a dropped, coalesced, or never-sent notification
73
88
  * can never strand accepted work (persistence §14). Delivery may duplicate; consumers already
74
89
  * treat wakes as pure liveness hints.
@@ -0,0 +1,258 @@
1
+ import {
2
+ WorkflowDispatchError,
3
+ WorkflowDispatchIntent,
4
+ WorkflowDispatchScan,
5
+ WorkflowDispatchStore,
6
+ WorkflowRepairTrigger,
7
+ } from "@effect-agent/workflow/WorkflowDispatch";
8
+ import { Cause, Duration, Effect, Layer, Option, Schema } from "effect";
9
+ import { SqlClient } from "effect/unstable/sql";
10
+
11
+ const StoredIntent = Schema.Struct({
12
+ deployment_id: Schema.String,
13
+ workflow_name: Schema.String,
14
+ execution_id: Schema.String,
15
+ intent_json: Schema.String,
16
+ });
17
+
18
+ const IntentJson = Schema.fromJsonString(WorkflowDispatchIntent);
19
+ const decodeIntent = Schema.decodeUnknownEffect(IntentJson, { onExcessProperty: "error" });
20
+ const encodeIntent = Schema.encodeEffect(IntentJson);
21
+
22
+ const dispatchError = (operation: string) => (cause: unknown) =>
23
+ Schema.is(WorkflowDispatchError)(cause)
24
+ ? cause
25
+ : new WorkflowDispatchError({
26
+ operation,
27
+ message: "Workflow dispatch storage failed or contains incompatible data",
28
+ cause,
29
+ });
30
+
31
+ const decodeRow = Effect.fn("SqlWorkflowDispatchStore.decodeRow")(function* (value: unknown) {
32
+ const row = yield* Schema.decodeUnknownEffect(StoredIntent, { onExcessProperty: "error" })(value);
33
+ const intent = yield* decodeIntent(row.intent_json);
34
+
35
+ if (
36
+ row.deployment_id !== intent.deploymentId ||
37
+ row.workflow_name !== intent.workflowName ||
38
+ row.execution_id !== intent.executionId
39
+ ) {
40
+ return yield* new WorkflowDispatchError({
41
+ operation: "decode",
42
+ message: "Stored Workflow dispatch identity disagrees with its intent",
43
+ });
44
+ }
45
+
46
+ return intent;
47
+ });
48
+
49
+ /**
50
+ * Durable dispatch outbox over an application-supplied SqlClient. This adapter uses
51
+ * SQLite/PostgreSQL SQL syntax and is certified with SQLite. It does not own an engine
52
+ * or a database connection. Agent admission, dispatch persistence, and native Workflow
53
+ * storage are separate commits; the registered repair trigger closes those gaps.
54
+ * Stored version or shape mismatches fail typed and require an explicit data reset.
55
+ */
56
+ export class SqlWorkflowDispatchStore {
57
+ static readonly layer: Layer.Layer<
58
+ WorkflowDispatchStore,
59
+ WorkflowDispatchError,
60
+ SqlClient.SqlClient
61
+ > = Layer.effect(WorkflowDispatchStore)(
62
+ Effect.gen(function* () {
63
+ const sql = (yield* SqlClient.SqlClient).withoutTransforms();
64
+
65
+ yield* sql`
66
+ CREATE TABLE IF NOT EXISTS effect_agent_workflow_dispatch (
67
+ workflow_name TEXT NOT NULL,
68
+ execution_id TEXT NOT NULL,
69
+ deployment_id TEXT NOT NULL,
70
+ intent_json TEXT NOT NULL,
71
+ PRIMARY KEY (workflow_name, execution_id)
72
+ )
73
+ `;
74
+ yield* sql`
75
+ CREATE INDEX IF NOT EXISTS effect_agent_workflow_dispatch_scan
76
+ ON effect_agent_workflow_dispatch (deployment_id, workflow_name, execution_id)
77
+ `;
78
+
79
+ const put = Effect.fn("SqlWorkflowDispatchStore.put")(
80
+ function* (input: WorkflowDispatchIntent) {
81
+ const intent = yield* Schema.decodeUnknownEffect(WorkflowDispatchIntent)(input);
82
+ const encoded = yield* encodeIntent(intent);
83
+
84
+ yield* sql`
85
+ INSERT INTO effect_agent_workflow_dispatch
86
+ (workflow_name, execution_id, deployment_id, intent_json)
87
+ VALUES (${intent.workflowName}, ${intent.executionId}, ${intent.deploymentId}, ${encoded})
88
+ ON CONFLICT (workflow_name, execution_id) DO NOTHING
89
+ `;
90
+
91
+ const rows = yield* sql`
92
+ SELECT deployment_id, workflow_name, execution_id, intent_json
93
+ FROM effect_agent_workflow_dispatch
94
+ WHERE workflow_name = ${intent.workflowName} AND execution_id = ${intent.executionId}
95
+ `;
96
+
97
+ const existing = yield* decodeRow(rows[0]);
98
+
99
+ const merged = new WorkflowDispatchIntent({
100
+ ...intent,
101
+ ...(existing.completionToken === undefined
102
+ ? {}
103
+ : { completionToken: existing.completionToken }),
104
+ });
105
+
106
+ if (
107
+ (yield* encodeIntent(
108
+ new WorkflowDispatchIntent({
109
+ ...existing,
110
+ ...(merged.completionToken === undefined
111
+ ? {}
112
+ : { completionToken: merged.completionToken }),
113
+ }),
114
+ )) !== (yield* encodeIntent(merged)) ||
115
+ (intent.completionToken !== undefined &&
116
+ existing.completionToken !== undefined &&
117
+ intent.completionToken !== existing.completionToken)
118
+ ) {
119
+ return yield* new WorkflowDispatchError({
120
+ operation: "put",
121
+ message: "Workflow dispatch identity already belongs to a different immutable intent",
122
+ });
123
+ }
124
+ const retained = yield* encodeIntent(merged);
125
+ const previous = yield* encodeIntent(existing);
126
+
127
+ if (retained !== previous) {
128
+ const updated = yield* sql`
129
+ UPDATE effect_agent_workflow_dispatch SET intent_json = ${retained}
130
+ WHERE workflow_name = ${intent.workflowName} AND execution_id = ${intent.executionId}
131
+ AND intent_json = ${previous}
132
+ RETURNING execution_id
133
+ `;
134
+
135
+ if (updated.length !== 1) {
136
+ return yield* new WorkflowDispatchError({
137
+ operation: "put",
138
+ message: "Dispatch intent changed while attaching its completion token",
139
+ });
140
+ }
141
+ }
142
+
143
+ return merged;
144
+ },
145
+ sql.withTransaction,
146
+ Effect.mapError(dispatchError("put")),
147
+ );
148
+
149
+ const scan = Effect.fn("SqlWorkflowDispatchStore.scan")(
150
+ function* (input: WorkflowDispatchScan) {
151
+ const request = yield* Schema.decodeUnknownEffect(WorkflowDispatchScan)(input);
152
+
153
+ const rows = yield* sql`
154
+ SELECT deployment_id, workflow_name, execution_id, intent_json
155
+ FROM effect_agent_workflow_dispatch
156
+ WHERE deployment_id = ${request.deploymentId}
157
+ AND workflow_name = ${request.workflowName}
158
+ AND execution_id > ${request.after ?? ""}
159
+ ORDER BY execution_id ASC
160
+ LIMIT ${request.limit}
161
+ `;
162
+
163
+ return yield* Effect.forEach(rows, decodeRow);
164
+ },
165
+ Effect.mapError(dispatchError("scan")),
166
+ );
167
+
168
+ const remove = Effect.fn("SqlWorkflowDispatchStore.remove")(
169
+ function* (input: WorkflowDispatchIntent) {
170
+ const intent = yield* Schema.decodeUnknownEffect(WorkflowDispatchIntent)(input);
171
+
172
+ const rows = yield* sql`
173
+ SELECT deployment_id, workflow_name, execution_id, intent_json
174
+ FROM effect_agent_workflow_dispatch
175
+ WHERE workflow_name = ${intent.workflowName} AND execution_id = ${intent.executionId}
176
+ `;
177
+
178
+ if (rows.length === 0) return;
179
+ const existing = yield* decodeRow(rows[0]);
180
+
181
+ if ((yield* encodeIntent(existing)) !== (yield* encodeIntent(intent))) {
182
+ return yield* new WorkflowDispatchError({
183
+ operation: "remove",
184
+ message: "Cannot remove a different immutable Workflow dispatch intent",
185
+ });
186
+ }
187
+
188
+ const removed = yield* sql`
189
+ DELETE FROM effect_agent_workflow_dispatch
190
+ WHERE workflow_name = ${intent.workflowName} AND execution_id = ${intent.executionId}
191
+ AND intent_json = ${yield* encodeIntent(intent)}
192
+ RETURNING execution_id
193
+ `;
194
+
195
+ if (removed.length !== 1) {
196
+ return yield* new WorkflowDispatchError({
197
+ operation: "remove",
198
+ message: "Dispatch intent changed before cleanup",
199
+ });
200
+ }
201
+ },
202
+ sql.withTransaction,
203
+ Effect.mapError(dispatchError("remove")),
204
+ );
205
+
206
+ return WorkflowDispatchStore.of({ put, scan, remove });
207
+ }).pipe(Effect.mapError(dispatchError("initialize"))),
208
+ );
209
+ }
210
+
211
+ export class NodeWorkflowRepairConfigError extends Schema.TaggedError<NodeWorkflowRepairConfigError>()(
212
+ "NodeWorkflowRepairConfigError",
213
+ { message: Schema.String },
214
+ ) {}
215
+
216
+ /** A host-scoped startup and polling trigger. No ordinary Node agent worker is started. */
217
+ export class NodeWorkflowRepairTrigger {
218
+ static layer(
219
+ options: { readonly interval?: Duration.Input } = {},
220
+ ): Layer.Layer<WorkflowRepairTrigger, NodeWorkflowRepairConfigError> {
221
+ return Layer.effect(WorkflowRepairTrigger)(
222
+ Effect.gen(function* () {
223
+ const interval = Duration.fromInput(options.interval ?? "1 second");
224
+
225
+ if (
226
+ Option.isNone(interval) ||
227
+ !Duration.isFinite(interval.value) ||
228
+ !Duration.isPositive(interval.value)
229
+ ) {
230
+ return yield* new NodeWorkflowRepairConfigError({
231
+ message: "Workflow repair interval must be finite and greater than zero",
232
+ });
233
+ }
234
+ const delay = interval.value;
235
+
236
+ return WorkflowRepairTrigger.of({
237
+ register: Effect.fn("NodeWorkflowRepairTrigger.register")(function* (repair) {
238
+ const attempt = repair.pipe(
239
+ Effect.catchCause((cause) =>
240
+ Cause.hasInterruptsOnly(cause)
241
+ ? Effect.failCause(cause)
242
+ : Effect.logError("Workflow repair trigger failed; next poll will retry", cause),
243
+ ),
244
+ );
245
+
246
+ yield* attempt;
247
+ yield* Effect.gen(function* () {
248
+ while (true) {
249
+ yield* Effect.sleep(delay);
250
+ yield* attempt;
251
+ }
252
+ }).pipe(Effect.forkScoped);
253
+ }),
254
+ });
255
+ }),
256
+ );
257
+ }
258
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
- export * from "./host.ts";
2
- export * from "./layers.ts";
3
- export * from "./wake-scheduler.ts";
1
+ export * as NodeDurableAgentRuntime from "./NodeDurableAgentRuntime.ts";
2
+ export * as NodeDurableHost from "./NodeDurableHost.ts";
3
+ export * as NodeScheduling from "./NodeScheduling.ts";
4
+ export * as NodeSubscriptions from "./NodeSubscriptions.ts";
5
+ export * as NodeWakeScheduler from "./NodeWakeScheduler.ts";
@@ -0,0 +1,53 @@
1
+ import {
2
+ MessageDeliveryDriver,
3
+ type MessageDeliveryFailure,
4
+ MessageDeliveryStore,
5
+ } from "@effect-agent/thread/MessageDelivery";
6
+ import { Cause, Clock, Effect, Exit, Option } from "effect";
7
+
8
+ const reportFailure = (cause: Cause.Cause<MessageDeliveryFailure>): Effect.Effect<void> =>
9
+ Cause.hasInterruptsOnly(cause)
10
+ ? Effect.interrupt
11
+ : Effect.logWarning("Node message delivery pass failed").pipe(
12
+ Effect.annotateLogs({
13
+ failureTag: Option.match(Cause.findErrorOption(cause), {
14
+ onNone: () => "Defect",
15
+ onSome: (failure) => failure._tag,
16
+ }),
17
+ }),
18
+ );
19
+
20
+ /** Caller-owned loop. Indexed scans repair absent hints even when both Threads have settled. */
21
+ export const runNodeMessageDeliveries = Effect.fn("NodeMessageDelivery.run")(function* (
22
+ scanInterval: number,
23
+ ) {
24
+ const driver = yield* MessageDeliveryDriver;
25
+ const store = yield* MessageDeliveryStore;
26
+
27
+ while (true) {
28
+ const pass = yield* driver.runDue().pipe(Effect.exit);
29
+
30
+ if (Exit.isFailure(pass)) {
31
+ yield* reportFailure(pass.cause);
32
+ yield* Effect.sleep(scanInterval);
33
+ continue;
34
+ }
35
+
36
+ const deadline = yield* store.nextDeadline().pipe(Effect.exit);
37
+
38
+ if (Exit.isFailure(deadline)) {
39
+ yield* reportFailure(deadline.cause);
40
+ yield* Effect.sleep(scanInterval);
41
+ continue;
42
+ }
43
+
44
+ const nowMillis = yield* Clock.currentTimeMillis;
45
+
46
+ const delay =
47
+ deadline.value === null
48
+ ? scanInterval
49
+ : Math.max(1, Math.min(deadline.value - nowMillis, scanInterval));
50
+
51
+ yield* Effect.sleep(delay);
52
+ }
53
+ });
@@ -0,0 +1,81 @@
1
+ import { type AgentId } from "@effect-agent/core/Identifiers";
2
+ import { type DurableSubmitAgent } from "@effect-agent/thread/DurableAgentRuntime";
3
+ import { PreparedInputAdmission } from "@effect-agent/thread/PreparedInputAdmission";
4
+ import { PersistedJson } from "@effect-agent/thread/Records";
5
+ import {
6
+ ScheduledInputRetryable,
7
+ ScheduledInputRefused,
8
+ ScheduleStorageError,
9
+ } from "@effect-agent/thread/Schedule";
10
+ import { Context, Effect } from "effect";
11
+
12
+ import { type NodeDurableHost } from "../NodeDurableHost.ts";
13
+
14
+ const passthroughSubmitAgent = (agentId: AgentId): DurableSubmitAgent<typeof PersistedJson> => ({
15
+ definition: { id: agentId, input: PersistedJson },
16
+ });
17
+
18
+ const ambiguous = (): ScheduledInputRetryable =>
19
+ ScheduledInputRetryable.make({ reason: "ambiguous" });
20
+
21
+ const corrupt = (operation: string): ScheduleStorageError =>
22
+ ScheduleStorageError.make({ operation, reason: "corrupt" });
23
+
24
+ /** Host-owned admission gate, available while the host's worker pool is being assembled. */
25
+ export class NodeAdmission extends Context.Service<
26
+ NodeAdmission,
27
+ Pick<NodeDurableHost["Service"], "submit" | "submissionStatus">
28
+ >()("@effect-agent/platform-node/internal/NodeAdmission") {}
29
+
30
+ /** Acquire the gated source once; worker callers cannot replace its admission authority. */
31
+ export const makeNodePreparedInputAdmission = Effect.gen(function* () {
32
+ const host = yield* NodeAdmission;
33
+
34
+ return PreparedInputAdmission.of({
35
+ submissionStatus: (receipt) =>
36
+ host
37
+ .submissionStatus(receipt)
38
+ .pipe(Effect.mapError(() => ScheduledInputRetryable.make({ reason: "storage" }))),
39
+ submit: (envelope) =>
40
+ host
41
+ .submit(passthroughSubmitAgent(envelope.agentId), envelope.input, {
42
+ threadId: envelope.threadId,
43
+ principal: envelope.deliveryPrincipal,
44
+ idempotencyKey: envelope.admissionKey,
45
+ ...(envelope.admissionGroup === undefined
46
+ ? {}
47
+ : { admissionGroup: envelope.admissionGroup }),
48
+ ...(envelope.admissionFence === undefined
49
+ ? {}
50
+ : { admissionFence: envelope.admissionFence }),
51
+ ...(envelope.workerAdmission === undefined
52
+ ? {}
53
+ : { workerAdmission: envelope.workerAdmission }),
54
+ ...(envelope.messageAdmission === undefined
55
+ ? {}
56
+ : { messageAdmission: envelope.messageAdmission }),
57
+ definitions: envelope.definitions,
58
+ })
59
+ .pipe(
60
+ Effect.catchTags({
61
+ AdmissionClosed: () =>
62
+ Effect.fail(ScheduledInputRetryable.make({ reason: "host-closed" })),
63
+ AgentInputError: () => Effect.fail(corrupt("prepared admission input")),
64
+ AdmissionConflict: () => Effect.fail(corrupt("prepared admission conflict")),
65
+ DigestError: () => Effect.fail(ambiguous()),
66
+ AdmissionPolicyError: (error) =>
67
+ error.reason === "refused"
68
+ ? ScheduledInputRefused.make({ code: error.code })
69
+ : ScheduledInputRetryable.make({
70
+ reason: error.reason === "occupied" ? "capacity" : "storage",
71
+ }),
72
+ LedgerError: () => ScheduledInputRetryable.make({ reason: "storage" }),
73
+ ThreadStoreError: () => Effect.fail(ambiguous()),
74
+ ThreadNotMaterialized: () => Effect.fail(ambiguous()),
75
+ AppendConflict: () => Effect.fail(ambiguous()),
76
+ FenceRejected: () => Effect.fail(ambiguous()),
77
+ DurableRuntimeFailpointError: () => Effect.fail(ambiguous()),
78
+ }),
79
+ ),
80
+ });
81
+ });
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/wake-scheduler.ts","../src/layers.ts","../src/host.ts"],"sourcesContent":["import type { ConversationId } from \"@effect-agent/core\";\nimport { SubmissionLedger, WakeScheduler } from \"@effect-agent/session\";\nimport { Context, Duration, Effect, Layer, PubSub, Stream } from \"effect\";\n\n/**\n * Bounded in-process wake buffer. Wake hints are droppable by contract (the ledger-scan fallback\n * keeps liveness), so a full buffer slides out the oldest hint instead of growing without bound.\n */\nconst WAKE_BUFFER_CAPACITY = 1_024;\n\n/** Cadence authority for the Node wake scheduler's ledger-scan fallback loop. */\nexport class NodeWakeSchedulerConfig extends Context.Service<\n NodeWakeSchedulerConfig,\n {\n /** Interval between ledger scans that re-emit every nonterminal Conversation lane. */\n readonly scanInterval: Duration.Duration;\n }\n>()(\"@effect-agent/platform-node/NodeWakeSchedulerConfig\") {\n static layer(options: {\n readonly scanInterval: Duration.Duration;\n }): Layer.Layer<NodeWakeSchedulerConfig> {\n return Layer.succeed(NodeWakeSchedulerConfig)({ scanInterval: options.scanInterval });\n }\n}\n\nconst makeWakeScheduler = Effect.gen(function* () {\n const ledger = yield* SubmissionLedger;\n const config = yield* NodeWakeSchedulerConfig;\n const hints = yield* PubSub.sliding<ConversationId>(WAKE_BUFFER_CAPACITY);\n yield* Effect.addFinalizer(() => PubSub.shutdown(hints));\n\n /**\n * One fallback scan: every Conversation lane with nonterminal work, deduplicated. A scan\n * failure degrades to \"no hints this round\" — the wake channel has no error contract and the\n * next round retries — but is logged so a persistently failing ledger stays visible.\n */\n const scanOnce: Effect.Effect<ReadonlyArray<ConversationId>> = Stream.runCollect(\n ledger.scanNonterminal,\n ).pipe(\n Effect.map((snapshots) => {\n const lanes = new Set<ConversationId>();\n for (const snapshot of snapshots) {\n lanes.add(snapshot.conversationId);\n }\n return [...lanes];\n }),\n Effect.catch((error) =>\n Effect.logWarning(\"NodeWakeScheduler fallback scan failed\", error).pipe(\n Effect.as([] as ReadonlyArray<ConversationId>),\n ),\n ),\n );\n\n /**\n * Deployment §3: correctness must not depend on in-memory notifications, so every `wakes` run\n * merges its PubSub subscription with a Clock-driven ledger-scan loop. Both live entirely in\n * the consuming run's Scope; no fiber outlives its subscriber.\n */\n const fallbackScans: Stream.Stream<ConversationId> = Stream.fromIterableEffectRepeat(\n Effect.sleep(config.scanInterval).pipe(Effect.andThen(scanOnce)),\n );\n\n return WakeScheduler.of({\n notify: (conversationId) => PubSub.publish(hints, conversationId).pipe(Effect.asVoid),\n wakes: Stream.merge(Stream.fromPubSub(hints), fallbackScans),\n });\n});\n\n/**\n * In-process Node `WakeScheduler`: `notify` publishes to a bounded sliding PubSub for prompt\n * same-process wakeups, and every `wakes` subscription additionally runs a periodic\n * `SubmissionLedger.scanNonterminal` fallback so a dropped, coalesced, or never-sent notification\n * can never strand accepted work (persistence §14). Delivery may duplicate; consumers already\n * treat wakes as pure liveness hints.\n */\nexport const nodeWakeSchedulerLayer: Layer.Layer<\n WakeScheduler,\n never,\n SubmissionLedger | NodeWakeSchedulerConfig\n> = Layer.effect(WakeScheduler)(makeWakeScheduler);\n","import type { SubmissionId } from \"@effect-agent/core\";\nimport {\n AgentBindingResolver,\n ConversationStore,\n DEFAULT_OWNERSHIP_LEASE_DURATION,\n DeploymentId,\n DurableAgentRuntime,\n DurableRuntimeConfig,\n DurableRuntimeFailpoint,\n ProducerId,\n ReleaseOwnershipRequest,\n SubmissionLedger,\n ToolReconciler,\n WakeScheduler,\n type DurableRuntimeFailpointHandler,\n type OwnershipToken,\n type ResolvedBinding,\n} from \"@effect-agent/session\";\nimport {\n conversationStoreLayer,\n SqliteStorageConfig,\n SqliteStorageConfigValue,\n storageFailpointLayer,\n submissionLedgerLayer,\n type SqliteStorageFailpointHandler,\n type SqliteStorageInitializationError,\n} from \"@effect-agent/storage-sqlite\";\nimport { NodeCrypto } from \"@effect/platform-node\";\nimport { SqliteClient } from \"@effect/sql-sqlite-node\";\nimport { Context, Duration, Effect, Layer, Ref, Schema } from \"effect\";\n\nimport { NodeWakeSchedulerConfig, nodeWakeSchedulerLayer } from \"./wake-scheduler.ts\";\n\nconst PositiveMillis = Schema.Int.check(Schema.isGreaterThan(0));\nconst NonNegativeMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));\nconst WorkerConcurrency = Schema.Int.check(\n Schema.isGreaterThanOrEqualTo(1),\n Schema.isLessThanOrEqualTo(64),\n);\n\n/** The supplied Node durable runtime configuration failed schema validation (DEPLOY-003). */\nexport class NodePlatformConfigError extends Schema.TaggedError<NodePlatformConfigError>()(\n \"NodePlatformConfigError\",\n {\n message: Schema.String,\n cause: Schema.optionalKey(Schema.Defect()),\n },\n) {}\n\n/**\n * Validated Node durable runtime configuration (deployment §4: decoded once during Layer\n * construction, exposed as a typed service). Every cadence is in milliseconds and every bound is\n * finite; `workerConcurrency` caps how many worker loops `NodeDurableHost.runWorkers` drives.\n */\nexport class NodeDurableRuntimeConfigValue extends Schema.Class<NodeDurableRuntimeConfigValue>(\n \"@effect-agent/platform-node/NodeDurableRuntimeConfigValue\",\n)({\n /** SQLite database file backing BOTH the Conversation Log and the Submission Ledger. */\n filename: Schema.NonEmptyString,\n deploymentId: DeploymentId,\n producerId: ProducerId,\n /** Submission ownership lease duration (D5); liveness hint only, epochs stay authoritative. */\n ownershipLeaseDuration: PositiveMillis,\n /** Finite bound on concurrent worker loops per host (rule 10). */\n workerConcurrency: WorkerConcurrency,\n /** Ledger-scan fallback cadence of the Node wake scheduler (deployment §3). */\n wakeScanInterval: PositiveMillis,\n /** `awaitSettlement` ledger re-check cadence when no wake arrives. */\n settlementPollInterval: PositiveMillis,\n /** Worker ownership-lease renewal cadence. */\n leaseRenewalInterval: PositiveMillis,\n /** Active-Run abort-intent poll cadence. */\n abortPollInterval: PositiveMillis,\n /** Bounded SQLITE_BUSY retry window for write-lock acquisition. */\n busyTimeout: NonNegativeMillis,\n /** Canonical observation poll cadence of the SQLite store. */\n observationPollInterval: NonNegativeMillis,\n /** Opt-in full payload/digest-chain audit while opening the store. */\n verifyOnOpen: Schema.Boolean,\n}) {}\n\n/** Explicit configuration authority for the assembled Node durable runtime. */\nexport class NodeDurableRuntimeConfig extends Context.Service<\n NodeDurableRuntimeConfig,\n NodeDurableRuntimeConfigValue\n>()(\"@effect-agent/platform-node/NodeDurableRuntimeConfig\") {}\n\n/**\n * Raw (unvalidated) construction options for `NodeDurableRuntime.layer`. Optional fields default\n * to the documented production values; everything is schema-decoded into\n * `NodeDurableRuntimeConfigValue` before any resource opens (deployment §5 gate 1).\n */\nexport interface NodeDurableRuntimeOptions {\n readonly filename: string;\n readonly deploymentId: string;\n readonly producerId: string;\n /** Milliseconds; default `DEFAULT_OWNERSHIP_LEASE_DURATION` (30s, D5). */\n readonly ownershipLeaseDuration?: number | undefined;\n /** Default 1; bounded to 1..64. */\n readonly workerConcurrency?: number | undefined;\n /** Milliseconds; default 1000. */\n readonly wakeScanInterval?: number | undefined;\n /** Milliseconds; default 500. */\n readonly settlementPollInterval?: number | undefined;\n /** Milliseconds; default 10000. */\n readonly leaseRenewalInterval?: number | undefined;\n /** Milliseconds; default 500. */\n readonly abortPollInterval?: number | undefined;\n /** Milliseconds; default 5000. */\n readonly busyTimeout?: number | undefined;\n /** Milliseconds; default 25. */\n readonly observationPollInterval?: number | undefined;\n /** Default false. */\n readonly verifyOnOpen?: boolean | undefined;\n /** SQLite adapter fault injection (`ledger:*` / `append:*` locations); default none. */\n readonly storageFailpoint?: SqliteStorageFailpointHandler | undefined;\n /** Coordinator fault injection (`submit:*` / `terminalize:*` locations); default none. */\n readonly runtimeFailpoint?: DurableRuntimeFailpointHandler | undefined;\n /**\n * Reconciliation policy consulted for open ordinary Tool Calls before an Unknown Outcome is\n * recorded (durability §10, DUR-009). Defaults to the fail-closed `ToolReconciler.uncertain`:\n * with no registered policy, every open call stays Unknown and routes to the authorized\n * DUR-017 resolution path.\n */\n readonly toolReconciler?: Layer.Layer<ToolReconciler> | undefined;\n /**\n * Registered worker Bindings resolved at durable claim time (S2, spec/subagents.md §11):\n * build each with `DurableWorkerBinding.make(binding, digests)` so\n * `NodeDurableHost.runResolvedWorkers` / `DurableAgentRuntime.runResolvedWorker` can serve\n * parent and child lanes from one pool with exact-digest resolution (SUB-023). Defaults to\n * the empty registration: every resolved claim then fails closed (`BindingUnavailable` for a\n * root, the framework `ChildCompatibilityFailure` Settlement for a parent-linked child).\n */\n readonly bindings?: ReadonlyArray<ResolvedBinding> | undefined;\n}\n\n/** Every construction failure of the assembled Node durable runtime stack. */\nexport type NodeDurableRuntimeInitializationError =\n | NodePlatformConfigError\n | SqliteStorageInitializationError;\n\n/** The services `NodeDurableRuntime.layer` provides. */\nexport type NodeDurableRuntimeServices =\n | DurableAgentRuntime\n | SubmissionLedger\n | ConversationStore\n | WakeScheduler\n | DurableRuntimeConfig\n | NodeDurableRuntimeConfig\n | AgentBindingResolver;\n\nconst decodeConfigValue = Schema.decodeUnknownEffect(NodeDurableRuntimeConfigValue);\n\nconst configFromOptions = (\n options: NodeDurableRuntimeOptions,\n): Effect.Effect<NodeDurableRuntimeConfigValue, NodePlatformConfigError> =>\n decodeConfigValue({\n filename: options.filename,\n deploymentId: options.deploymentId,\n producerId: options.producerId,\n ownershipLeaseDuration:\n options.ownershipLeaseDuration ?? Duration.toMillis(DEFAULT_OWNERSHIP_LEASE_DURATION),\n workerConcurrency: options.workerConcurrency ?? 1,\n wakeScanInterval: options.wakeScanInterval ?? 1_000,\n settlementPollInterval: options.settlementPollInterval ?? 500,\n leaseRenewalInterval: options.leaseRenewalInterval ?? 10_000,\n abortPollInterval: options.abortPollInterval ?? 500,\n busyTimeout: options.busyTimeout ?? 5_000,\n observationPollInterval: options.observationPollInterval ?? 25,\n verifyOnOpen: options.verifyOnOpen ?? false,\n }).pipe(\n Effect.mapError((error) =>\n NodePlatformConfigError.make({\n message: `Invalid Node durable runtime configuration: ${error.message}`,\n cause: error,\n }),\n ),\n );\n\n/** SQLite storage configuration derived from the single validated Node configuration. */\nconst sqliteStorageConfigLayer: Layer.Layer<SqliteStorageConfig, never, NodeDurableRuntimeConfig> =\n Layer.effect(SqliteStorageConfig)(\n Effect.gen(function* () {\n const config = yield* NodeDurableRuntimeConfig;\n return SqliteStorageConfigValue.make({\n observationPollInterval: config.observationPollInterval,\n busyTimeout: config.busyTimeout,\n ownershipLeaseDuration: config.ownershipLeaseDuration,\n verifyOnOpen: config.verifyOnOpen,\n });\n }),\n );\n\n/** Session coordinator configuration derived from the single validated Node configuration. */\nconst durableRuntimeConfigLayer: Layer.Layer<\n DurableRuntimeConfig,\n never,\n NodeDurableRuntimeConfig\n> = Layer.effect(DurableRuntimeConfig)(\n Effect.gen(function* () {\n const config = yield* NodeDurableRuntimeConfig;\n return DurableRuntimeConfig.make({\n deploymentId: config.deploymentId,\n producerId: config.producerId,\n settlementPollInterval: Duration.millis(config.settlementPollInterval),\n leaseRenewalInterval: Duration.millis(config.leaseRenewalInterval),\n abortPollInterval: Duration.millis(config.abortPollInterval),\n });\n }),\n);\n\n/** Wake fallback-scan cadence derived from the single validated Node configuration. */\nconst wakeSchedulerConfigLayer: Layer.Layer<\n NodeWakeSchedulerConfig,\n never,\n NodeDurableRuntimeConfig\n> = Layer.effect(NodeWakeSchedulerConfig)(\n Effect.gen(function* () {\n const config = yield* NodeDurableRuntimeConfig;\n return { scanInterval: Duration.millis(config.wakeScanInterval) };\n }),\n);\n\nconst releaseTrackedOwnership = (\n ledger: SubmissionLedger[\"Service\"],\n registry: Ref.Ref<ReadonlyMap<SubmissionId, OwnershipToken>>,\n): Effect.Effect<void> =>\n Effect.gen(function* () {\n const tracked = yield* Ref.getAndSet(registry, new Map<SubmissionId, OwnershipToken>());\n for (const [submissionId, ownershipToken] of tracked) {\n yield* ledger\n .releaseOwnership(ReleaseOwnershipRequest.make({ submissionId, ownershipToken }))\n .pipe(\n Effect.catchTags({\n // A newer epoch already owns (or settled) the lane: nothing left to drain.\n OwnershipLost: () => Effect.void,\n // Drain is best-effort by design: the lease still expires and the durability protocol,\n // not graceful shutdown, provides correctness (DEPLOY-006).\n LedgerError: (error) =>\n Effect.logWarning(\"Ownership drain failed; the lease will expire instead\", error),\n }),\n );\n }\n });\n\n/**\n * Shutdown-drain decorator for a `SubmissionLedger` (deployment §6 step 6): every ownership\n * period granted through this Layer is tracked — claims start tracking, renewals follow token\n * rotation, releases and settlement finalizations stop it — and every ownership still held when\n * the Layer's Scope closes is released so another host can claim the lane immediately instead of\n * waiting for lease expiry. The drain is a liveness courtesy only; producer-epoch fencing remains\n * the correctness authority (DUR-006), and a forced kill simply falls back to lease expiry.\n */\nexport const ownershipDrainLayer: Layer.Layer<SubmissionLedger, never, SubmissionLedger> =\n Layer.effect(SubmissionLedger)(\n Effect.gen(function* () {\n const ledger = yield* SubmissionLedger;\n const registry = yield* Ref.make<ReadonlyMap<SubmissionId, OwnershipToken>>(\n new Map<SubmissionId, OwnershipToken>(),\n );\n\n const track = (submissionId: SubmissionId, ownershipToken: OwnershipToken) =>\n Ref.update(registry, (tracked) => new Map(tracked).set(submissionId, ownershipToken));\n const untrack = (submissionId: SubmissionId) =>\n Ref.update(registry, (tracked) => {\n const next = new Map(tracked);\n next.delete(submissionId);\n return next;\n });\n\n yield* Effect.addFinalizer(() => releaseTrackedOwnership(ledger, registry));\n\n return SubmissionLedger.of({\n capabilities: ledger.capabilities,\n admit: ledger.admit,\n markReady: ledger.markReady,\n lookup: ledger.lookup,\n // The S2 subagent ops forward untouched: none of them grants an ownership period, so\n // the drain has nothing to track for them (`suspend` below already stops tracking the\n // waitingForChild ownership period the moment it ends).\n resolveAdmission: ledger.resolveAdmission,\n recordChildSettled: ledger.recordChildSettled,\n reserveChildBudget: ledger.reserveChildBudget,\n attachChildToReservation: ledger.attachChildToReservation,\n beginChildBudgetRelease: ledger.beginChildBudgetRelease,\n releaseChildBudget: ledger.releaseChildBudget,\n claim: (request) =>\n ledger\n .claim(request)\n .pipe(\n Effect.tap((claimed) =>\n claimed._tag === \"Some\"\n ? track(claimed.value.submissionId, claimed.value.ownershipToken)\n : Effect.void,\n ),\n ),\n renewOwnership: (request) =>\n ledger.renewOwnership(request).pipe(\n Effect.tap((renewal) => track(request.submissionId, renewal.ownershipToken)),\n Effect.tapError((error) =>\n error._tag === \"OwnershipLost\" ? untrack(request.submissionId) : Effect.void,\n ),\n ),\n releaseOwnership: (request) =>\n ledger.releaseOwnership(request).pipe(\n Effect.tap(() => untrack(request.submissionId)),\n Effect.tapError((error) =>\n error._tag === \"OwnershipLost\" ? untrack(request.submissionId) : Effect.void,\n ),\n ),\n markInputApplied: ledger.markInputApplied,\n reserveSettlement: ledger.reserveSettlement,\n finalizeSettlement: (request) =>\n ledger.finalizeSettlement(request).pipe(Effect.tap(() => untrack(request.submissionId))),\n requestAbort: ledger.requestAbort,\n claimJoining: ledger.claimJoining,\n markJoined: ledger.markJoined,\n revertJoining: ledger.revertJoining,\n // Suspension ends the ownership period by contract, so the drain stops tracking it.\n suspend: (request) =>\n ledger.suspend(request).pipe(Effect.tap(() => untrack(request.submissionId))),\n recordApprovalDecision: ledger.recordApprovalDecision,\n markUnknown: ledger.markUnknown,\n recordUnknownResolution: ledger.recordUnknownResolution,\n scanNonterminal: ledger.scanNonterminal,\n loadRecoverySnapshot: ledger.loadRecoverySnapshot,\n });\n }),\n );\n\n/**\n * The DN Layer assembly (deployment §12: a Layer-assembly library, not an app entrypoint).\n * `layer(options)` decodes the configuration, opens ONE SQLite database serving both the\n * Conversation Log and the Submission Ledger (so claims fence the same producer epochs), wires\n * the Node wake scheduler with its ledger-scan fallback, wraps the ledger with the shutdown\n * ownership drain, defaults the Tool reconciliation policy to the fail-closed\n * `ToolReconciler.uncertain` (override via `options.toolReconciler`), and provides a ready\n * `DurableAgentRuntime` on top. Storage compatibility is\n * verified during construction: an incompatible database file fails the Layer with\n * `SqliteStorageCompatibilityError` before anything is mutated (DEPLOY-008).\n */\nexport class NodeDurableRuntime {\n /** Validated configuration Layer; fails typed when the supplied options are out of bounds. */\n static configLayer(\n options: NodeDurableRuntimeOptions,\n ): Layer.Layer<NodeDurableRuntimeConfig, NodePlatformConfigError> {\n return Layer.effect(NodeDurableRuntimeConfig)(configFromOptions(options));\n }\n\n /** The full DN runtime stack over one SQLite file. */\n static layer(\n options: NodeDurableRuntimeOptions,\n ): Layer.Layer<NodeDurableRuntimeServices, NodeDurableRuntimeInitializationError> {\n const infrastructure = Layer.mergeAll(\n sqliteStorageConfigLayer,\n storageFailpointLayer({ filename: options.filename, failpoint: options.storageFailpoint }),\n SqliteClient.layer({ filename: options.filename }),\n NodeCrypto.layer,\n );\n const runtimeFailpointLayer =\n options.runtimeFailpoint === undefined\n ? DurableRuntimeFailpoint.layer\n : Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint });\n const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;\n const bindingResolverLayer = AgentBindingResolver.layer(options.bindings ?? []);\n const ports = Layer.mergeAll(\n conversationStoreLayer,\n nodeWakeSchedulerLayer.pipe(\n Layer.provideMerge(ownershipDrainLayer.pipe(Layer.provide(submissionLedgerLayer))),\n ),\n );\n return DurableAgentRuntime.layer.pipe(\n Layer.provideMerge(Layer.mergeAll(ports, durableRuntimeConfigLayer, bindingResolverLayer)),\n Layer.provide(\n Layer.mergeAll(wakeSchedulerConfigLayer, runtimeFailpointLayer, reconcilerLayer),\n ),\n Layer.provideMerge(infrastructure),\n Layer.provideMerge(NodeDurableRuntime.configLayer(options)),\n );\n }\n}\n","import type { ConversationId, SubmissionId } from \"@effect-agent/core\";\nimport {\n AgentBindingResolver,\n DurableAgentRuntime,\n type AbortCommand,\n type AbortIntent,\n type CanonicalRecordEnvelope,\n type ConversationNotMaterialized,\n type ConversationStoreError,\n type DurableAbortFailure,\n type DurableAwaitFailure,\n type DurableBindingFailure,\n type DurableExplainFailure,\n type DurableObserveOptions,\n type DurableObligationFailure,\n type DurableRetryFailure,\n type DurableSubmitAgent,\n type DurableSubmitFailure,\n type DurableSubmitOptions,\n type DurableVerifyFailure,\n type DurableWorkerFailure,\n type IntegrityReport,\n type ObligationReport,\n type ObligationThresholds,\n type OperationDenied,\n type Receipt,\n type RecoveryExplanation,\n type RecoveryReport,\n type RetryCommand,\n type Settlement,\n} from \"@effect-agent/session\";\nimport { Context, Effect, Layer, Ref, Schema, Stream } from \"effect\";\n\nimport {\n NodeDurableRuntime,\n NodeDurableRuntimeConfig,\n type NodeDurableRuntimeInitializationError,\n type NodeDurableRuntimeOptions,\n type NodeDurableRuntimeServices,\n} from \"./layers.ts\";\n\n/**\n * Admission is not open on this host: it is shutting down (deployment §6 step 1, DEPLOY-005).\n * Accepted work is unaffected — only NEW admissions are refused.\n */\nexport class AdmissionClosed extends Schema.TaggedError<AdmissionClosed>()(\"AdmissionClosed\", {\n message: Schema.String,\n}) {}\n\nconst makeHost = Effect.gen(function* () {\n const runtime = yield* DurableAgentRuntime;\n const config = yield* NodeDurableRuntimeConfig;\n const bindingResolver = yield* AgentBindingResolver;\n\n // Startup gate (deployment §5, plan §host): configuration decoding and storage compatibility\n // already gated this Layer's dependencies; the last gate before admission opens is recovering\n // EVERY nonterminal Submission. Work needing a live Agent Binding is reported `deferred` and\n // stays a visible obligation for `runWorkers`; lanes durably blocked on an Unknown Outcome are\n // reported `unknown` and wait for the authorized `resolveUnknown` path (DUR-017) — they consume\n // no worker permit while the settlement obligation stays owed.\n const startupRecovery = yield* runtime.runRecovery;\n\n const admission = yield* Ref.make(true);\n // Shutdown step 1 (DEPLOY-005): admission closes before the ownership drain in the runtime\n // Layer below releases claims and before the stores close in reverse acquisition order.\n yield* Effect.addFinalizer(() => Ref.set(admission, false));\n\n const requireAdmission: Effect.Effect<void, AdmissionClosed> = Ref.get(admission).pipe(\n Effect.flatMap((open) =>\n open\n ? Effect.void\n : Effect.fail(\n AdmissionClosed.make({ message: \"The host is shutting down; admission is closed.\" }),\n ),\n ),\n );\n\n const submit = <InputSchema extends Schema.Top>(\n agent: DurableSubmitAgent<InputSchema>,\n input: InputSchema[\"Type\"],\n options: DurableSubmitOptions,\n ): Effect.Effect<\n Receipt,\n AdmissionClosed | DurableSubmitFailure,\n InputSchema[\"EncodingServices\"]\n > => requireAdmission.pipe(Effect.andThen(runtime.submit(agent, input, options)));\n\n const runWorkers = <A, E, R>(worker: Effect.Effect<A, E, R>): Effect.Effect<void, E, R> =>\n Effect.forEach(\n Array.from({ length: config.workerConcurrency }, (_, index) => index),\n () => worker,\n { concurrency: \"unbounded\", discard: true },\n );\n\n // S2 multi-binding pool: every claimed head resolves its exact registered Binding through\n // the host's `AgentBindingResolver` (`NodeDurableRuntimeOptions.bindings`), so ONE bounded\n // pool serves parent and child lanes — the spec §12 smallest-pool suspension/wakeup proof\n // runs `workerConcurrency: 1` over exactly this loop.\n const runResolvedWorkers = runWorkers(\n runtime.runResolvedWorker.pipe(Effect.provideService(AgentBindingResolver, bindingResolver)),\n );\n\n return NodeDurableHost.of({\n startupRecovery,\n admissionOpen: Ref.get(admission),\n submit,\n awaitSettlement: runtime.awaitSettlement,\n observe: runtime.observe,\n abort: runtime.abort,\n explain: runtime.explain,\n explainConversation: runtime.explainConversation,\n verify: runtime.verify,\n retry: runtime.retry,\n wake: runtime.wake,\n scanObligations: runtime.scanObligations,\n runWorkers,\n runResolvedWorkers,\n });\n});\n\n/**\n * Operational host lifecycle for the DN runtime (deployment §2/§5/§6).\n *\n * Startup gates run during Layer construction, so the service existing implies readiness:\n * configuration was schema-decoded, the SQLite file passed the exact-version compatibility check,\n * and every nonterminal Submission went through one full recovery pass BEFORE admission opened.\n * `startupRecovery` is the auditable evidence of that reconciliation pass.\n *\n * Shutdown runs in reverse Layer order when the owning Scope closes: `submit` starts refusing\n * with `AdmissionClosed` first, then the runtime Layer's ownership drain releases every claim\n * still held so another host can take over the lanes immediately, then the SQLite resources\n * close. Forced termination at any point stays safe — the durability protocol, not graceful\n * shutdown, provides correctness (DEPLOY-006).\n */\nexport class NodeDurableHost extends Context.Service<\n NodeDurableHost,\n {\n /**\n * The recovery decisions executed (or deferred) by this host's startup reconciliation.\n * Reports with the `unknown` disposition identify lanes blocked on Unknown Outcomes that\n * only the authorized DUR-017 resolution path can release.\n */\n readonly startupRecovery: ReadonlyArray<RecoveryReport>;\n /** Admission-role readiness (deployment §7): true until shutdown begins. */\n readonly admissionOpen: Effect.Effect<boolean>;\n /** `DurableAgentRuntime.submit` behind the host admission gate. */\n readonly submit: <InputSchema extends Schema.Top>(\n agent: DurableSubmitAgent<InputSchema>,\n input: InputSchema[\"Type\"],\n options: DurableSubmitOptions,\n ) => Effect.Effect<\n Receipt,\n AdmissionClosed | DurableSubmitFailure,\n InputSchema[\"EncodingServices\"]\n >;\n readonly awaitSettlement: (receipt: Receipt) => Effect.Effect<Settlement, DurableAwaitFailure>;\n readonly observe: (\n receipt: Receipt,\n options?: DurableObserveOptions,\n ) => Stream.Stream<\n CanonicalRecordEnvelope,\n ConversationStoreError | ConversationNotMaterialized | OperationDenied\n >;\n readonly abort: (command: AbortCommand) => Effect.Effect<AbortIntent, DurableAbortFailure>;\n /** `DurableAgentRuntime.explain` — read-only recovery explanation of one Submission (P7). */\n readonly explain: (\n submissionId: SubmissionId,\n ) => Effect.Effect<RecoveryExplanation, DurableExplainFailure>;\n /** `DurableAgentRuntime.explainConversation` — explain every nonterminal lane member. */\n readonly explainConversation: (\n conversationId: ConversationId,\n ) => Effect.Effect<ReadonlyArray<RecoveryExplanation>, DurableExplainFailure>;\n /** `DurableAgentRuntime.verify` — read-only integrity checks, never a repair (P7). */\n readonly verify: (\n conversationId: ConversationId,\n ) => Effect.Effect<IntegrityReport, DurableVerifyFailure>;\n /** `DurableAgentRuntime.retry` — audited single-Submission re-drive with typed refusals. */\n readonly retry: (command: RetryCommand) => Effect.Effect<RecoveryReport, DurableRetryFailure>;\n /** `DurableAgentRuntime.wake` — the documented operator liveness nudge for one lane. */\n readonly wake: (conversationId: ConversationId) => Effect.Effect<void, OperationDenied>;\n /** `DurableAgentRuntime.scanObligations` — the scan-based DUR-017/OPS-001 report. */\n readonly scanObligations: (\n thresholds: ObligationThresholds,\n ) => Effect.Effect<ObligationReport, DurableObligationFailure>;\n /**\n * Run `workerConcurrency` copies of the given worker effect (typically\n * `DurableAgentRuntime.runWorker(agent)`) until the caller's Scope interrupts them. The\n * bound is the validated finite configuration value; the host never forks daemon fibers.\n */\n readonly runWorkers: <A, E, R>(worker: Effect.Effect<A, E, R>) => Effect.Effect<void, E, R>;\n /**\n * Run `workerConcurrency` copies of `DurableAgentRuntime.runResolvedWorker` over the host's\n * registered Bindings (S2): every claimed head resolves its exact stored Binding before any\n * code runs (SUB-023), so one bounded pool serves parent and attached-child lanes.\n */\n readonly runResolvedWorkers: Effect.Effect<void, DurableWorkerFailure | DurableBindingFailure>;\n }\n>()(\"@effect-agent/platform-node/NodeDurableHost\") {\n /** Host gates over an already-assembled `NodeDurableRuntime` stack. */\n static readonly layer: Layer.Layer<\n NodeDurableHost,\n DurableWorkerFailure,\n DurableAgentRuntime | NodeDurableRuntimeConfig | AgentBindingResolver\n > = Layer.effect(NodeDurableHost)(makeHost);\n\n /** The complete DN host: `NodeDurableRuntime.layer(options)` plus the host lifecycle gates. */\n static layerStack(\n options: NodeDurableRuntimeOptions,\n ): Layer.Layer<\n NodeDurableHost | NodeDurableRuntimeServices,\n DurableWorkerFailure | NodeDurableRuntimeInitializationError\n > {\n return NodeDurableHost.layer.pipe(Layer.provideMerge(NodeDurableRuntime.layer(options)));\n }\n}\n"],"mappings":"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"}