@fungi.computer/bird-dog 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,24 +1,12 @@
1
1
  # BirdDog (`@fungi.computer/bird-dog`)
2
2
 
3
- BirdDog is a durable mail room for AI agents. One agent sends work to another
4
- agent's session, the work runs as a [Watchdog](../watchdog/README.md) job, and
5
- when that job settles BirdDog delivers the reply to everyone on the envelope's
6
- `replyTo` list. Replies sit in a durable outbox and retry on a bounded schedule
7
- until the recipient accepts them.
8
-
9
- It does two jobs:
10
-
11
- - It translates addressed agent work into an opaque Watchdog job, and decodes
12
- that job's payload back after Watchdog claims it.
13
- - It fans out replies when the job settles, stores them in your outbox, and
14
- delivers them with retries through Carrier's Post outbox
15
- ([`@fungi.computer/carrier/post`](../carrier/README.md)).
16
-
17
- Every sender and recipient is an ordinary agent session. A parent and a child
18
- agent are both plain agents to BirdDog. Their relationship lives in your code,
19
- and all agent work uses Watchdog's `default` lane. You own storage, transport,
20
- the clock and wake scheduling. BirdDog owns the envelope format, the reply keys
21
- and the retry schedule.
3
+ BirdDog translates addressed Agent work into Watchdog jobs and builds replies
4
+ when that work settles. It owns envelope grammar, reply keys and settlement
5
+ content. The host stages each reply batch through its existing Carrier Post
6
+ owner; Watchdog owns delivery attempts and wakes.
7
+
8
+ Every sender and recipient is an ordinary Agent Session. Parent and child
9
+ relationships remain in the host's domain records.
22
10
 
23
11
  ## Install or set up
24
12
 
@@ -26,10 +14,10 @@ and the retry schedule.
26
14
  npm install @fungi.computer/bird-dog
27
15
  ```
28
16
 
29
- BirdDog is ESM-only. It depends on `@fungi.computer/watchdog`,
30
- `@fungi.computer/carrier` and `effect`, which npm installs for you. It does no
31
- I/O of its own, so it runs in Node, Cloudflare Workers or any runtime that can
32
- give it an outbox and a `post` function.
17
+ BirdDog is ESM-only. It depends on `@fungi.computer/watchdog` and `effect`,
18
+ which npm installs for you. It does no I/O of its own, so it runs in Node,
19
+ Cloudflare Workers or any runtime that can stage a batch of mail in its existing
20
+ delivery owner.
33
21
 
34
22
  Entrypoints:
35
23
 
@@ -41,13 +29,11 @@ Entrypoints:
41
29
 
42
30
  ## Quick start
43
31
 
44
- This sends work from a parent agent to a child agent, settles it, and delivers
45
- the reply. The outbox uses Carrier’s deterministic memory helper, and `post`
46
- prints the reply instead of sending it anywhere.
32
+ This constructs a reply batch. The example prints it; a durable host stages the
33
+ batch in its existing Carrier Post transaction.
47
34
 
48
35
  ```ts
49
36
  import { BirdDog, type BirdDogMail } from "@fungi.computer/bird-dog";
50
- import { createMemoryOutbox } from "@fungi.computer/carrier/testing";
51
37
 
52
38
  const parent = {
53
39
  kind: "agent",
@@ -61,13 +47,8 @@ const child = {
61
47
  } as const;
62
48
 
63
49
  const room = await BirdDog.make({
64
- outbox: createMemoryOutbox(),
65
- post: async (mail: BirdDogMail) => {
66
- console.log(`to ${mail.to.agentId}:\n${mail.content}`);
67
- return true; // the recipient accepted it
68
- },
69
- scheduleWake: async (at) => {
70
- console.log(`wake me at ${at}`);
50
+ stage: async (mail: readonly BirdDogMail[]) => {
51
+ for (const reply of mail) console.log(reply.content);
71
52
  },
72
53
  now: () => Date.now(),
73
54
  });
@@ -94,16 +75,16 @@ const job = BirdDog.agentJob({
94
75
  const payload = BirdDog.decodeAgentJob(job);
95
76
  console.log(payload.work.commandFactId); // "command-1"
96
77
 
97
- // 3. When Watchdog settles the job, queue the replies, then deliver them.
78
+ // 3. When Watchdog settles the job, stage the replies.
98
79
  await room.fanOut({
99
80
  job: { ...job, state: "settled", outcome: "completed" },
100
81
  content: "A test fixture was missing. Fixed it.",
101
82
  });
102
- await room.drainOutbox();
103
83
  ```
104
84
 
105
- In a real host, the settled job comes from Watchdog's `tick()` result and
106
- `drainOutbox()` runs from the wake you scheduled.
85
+ In a real host, the settled job comes from Watchdog and `stage` commits the
86
+ whole batch before the settlement cursor advances. Replaying a stable reply key
87
+ retains the first staged letter, including its original timestamp.
107
88
 
108
89
  ## Core concepts
109
90
 
@@ -125,9 +106,8 @@ Watchdog's `transactional.enqueueAcceptedJob`. `BirdDog.decodeAgentJob(job)`
125
106
  parses the payload back on the executor side. Watchdog never reads it.
126
107
 
127
108
  **Fan-out.** `fanOut({ job, content, diagnostic? })` takes a settled job, the
128
- content owed to correspondents and the Session's terminal diagnostic. It writes
129
- one reply per `replyTo` address into the outbox, keyed
130
- `outbox:<mail key>:reply:<index>`, then schedules one wake for the earliest.
109
+ content owed to correspondents and the Session's terminal diagnostic. It passes
110
+ one reply per `replyTo` address to `stage`, keyed `<mail key>:reply:<index>`.
131
111
  Each reply's `from` is the mail's recipient, its `content` is your content
132
112
  verbatim (or, when it is empty,
133
113
  `The delegated work ended <outcome> without reply text.`, so a reply is always
@@ -155,31 +135,10 @@ The recipient's host owns authentication and presentation. It trusts the `reply`
155
135
  facts only after checking the reply against the mail it sent, then decides who
156
136
  authors the line and how its model sees it.
157
137
 
158
- **Draining.** `drainOutbox()` reads one page of up to 64 outbox rows and calls
159
- `post` for each due reply. `post` returns `true` when the recipient accepts the
160
- mail. A `false` or a throw schedules a retry. If more rows remain, BirdDog
161
- schedules an immediate wake and continues from the same place on the next drain.
162
-
163
- **Receipts.** A delivered reply's row is replaced by a compact completion
164
- receipt under the same key. Replaying the same settlement after a restart finds
165
- the receipt and does not post the reply again. Keep those receipts as long as
166
- source settlements can replay. BirdDog does not expire them.
167
- `hasPendingDelivery(outbox)` reads only whether any reply still awaits a
168
- delivery attempt, so a host can tell retained receipts and dead letters from
169
- owed mail.
170
-
171
- The Effect mail room's `nextWakeAt()` reads Carrier's earliest owed delivery,
172
- including future retries. Runtime hosts include that hint in their pending-work
173
- answer rather than clearing a retry when other runtime work is idle.
174
-
175
- **Host ports.** `BirdDog.make` takes four things:
176
-
177
- - `outbox`: string rows with `put`, `read`, ordered `list(limit, afterKey?)` and
178
- `delete`. Use the same key ordering for sorting and the exclusive `afterKey`
179
- filter so paging cannot skip or repeat rows.
180
- - `post(mail)`: delivers one reply and reports acceptance.
181
- - `scheduleWake(at)`: arms a durable timer for `drainOutbox`.
182
- - `now()`: the current time in milliseconds.
138
+ **Host ports.** `BirdDog.make` takes `stage(mail)` and `now()`. Staging is one
139
+ batch operation so the host can commit all replies atomically. The host retains
140
+ stable keys, prepared delivery facts and receipts through Carrier Post. Watchdog
141
+ reserves attempts, resumes retries after reopen and reports the next wake.
183
142
 
184
143
  ## API reference
185
144
 
@@ -187,19 +146,19 @@ The generated reference is in [docs/api](docs/api/README.md).
187
146
 
188
147
  - `@fungi.computer/bird-dog`: `BirdDog` with `agentJob`, `decodeAgentJob` and
189
148
  `make`, plus the `BirdDogMail`, `BirdDogAgentReference`, `BirdDogAgentWork`,
190
- `BirdDogAgentJobPayload`, `BirdDogReply`, `BirdDogOutbox`, `BirdDogOptions`,
149
+ `BirdDogAgentJobPayload`, `BirdDogReply`, `BirdDogOptions`,
191
150
  `BirdDogSettlement` and `BirdDogRuntime` types.
192
151
  - `@fungi.computer/bird-dog/effect`: `make`, `agentJobFromInput`,
193
- `decodeAgentJob`, `settlementReplies`, `hasPendingDelivery`,
194
- `boundedDiagnostic`, `BIRD_DOG_DIAGNOSTIC_MAX_LENGTH`, `BirdDogError`,
195
- `EffectBirdDogOptions`, `BirdDogEffect` and the schemas
196
- `BirdDogAgentReferenceSchema`, `BirdDogReplySchema`, `BirdDogMailSchema`,
197
- `BirdDogAgentWorkSchema`, `BirdDogAgentJobPayloadSchema` and
198
- `BirdDogAgentJobInputSchema`.
152
+ `decodeAgentJob`, `settlementReplies`, `boundedDiagnostic`,
153
+ `BIRD_DOG_DIAGNOSTIC_MAX_LENGTH`, `BirdDogError`, `EffectBirdDogOptions`,
154
+ `BirdDogEffect` and the schemas `BirdDogAgentReferenceSchema`,
155
+ `BirdDogReplySchema`, `BirdDogMailSchema`, `BirdDogAgentWorkSchema`,
156
+ `BirdDogAgentJobPayloadSchema` and `BirdDogAgentJobInputSchema`.
199
157
 
200
158
  ## Examples and checks
201
159
 
202
- The Effect entrypoint takes the same ports as Effect values:
160
+ The Effect entrypoint takes the same ports as Effect values. Using `job` from
161
+ the quick start, this prints the settlement reply batch:
203
162
 
204
163
  ```ts
205
164
  import * as Effect from "effect/Effect";
@@ -207,18 +166,19 @@ import { make } from "@fungi.computer/bird-dog/effect";
207
166
 
208
167
  const room = await Effect.runPromise(
209
168
  make({
210
- outbox: {
211
- put: () => Effect.void,
212
- read: () => Effect.succeed(undefined),
213
- list: () => Effect.succeed([]),
214
- delete: () => Effect.void,
215
- },
216
- post: () => Effect.succeed(true),
217
- scheduleWake: () => Effect.void,
169
+ stage: (mail) =>
170
+ Effect.sync(() => {
171
+ for (const reply of mail) console.log(reply.content);
172
+ }),
218
173
  now: () => Date.now(),
219
174
  }),
220
175
  );
221
- await Effect.runPromise(room.drainOutbox());
176
+ await Effect.runPromise(
177
+ room.fanOut({
178
+ job: { ...job, state: "settled", outcome: "completed" },
179
+ content: "A test fixture was missing. Fixed it.",
180
+ }),
181
+ );
222
182
  ```
223
183
 
224
184
  Use the schemas to parse mail that arrives from outside your process:
@@ -230,11 +190,9 @@ import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
230
190
  const mail = Schema.decodeUnknownSync(BirdDogMailSchema)(JSON.parse(body));
231
191
  ```
232
192
 
233
- The package tests cover job translation, payload decoding, replayed settlement
234
- after a restart, explicit cancellation replies, exact failure outcomes with
235
- bounded and redacted diagnostics, retry re-arming after reopen, bounded paging
236
- past full pages, retry exhaustion, receipt walks, wake failure after fan-out,
237
- and parity between the Promise and Effect schemas. From the package directory:
193
+ The package tests cover job translation, typed settlement replies, bounded and
194
+ redacted diagnostics, and batch staging failures. Carrier and Local consumer
195
+ tests cover durable retries, receipt loss and physical reopen.
238
196
 
239
197
  ```sh
240
198
  pnpm test
@@ -242,26 +200,11 @@ pnpm test
242
200
 
243
201
  ## Limits and errors
244
202
 
245
- - **Retry schedule.** A failed delivery retries after 2 seconds, doubling each
246
- time, capped at 5 minutes. After 20 attempts the reply is exhausted. Its row
247
- stays in the outbox, and BirdDog stops posting it and stops waking for it.
248
- - **At-least-once.** If the recipient accepts a reply but recording the receipt
249
- fails, the retry uses the same mail key. Recipients must admit a repeated key
250
- idempotently.
251
- - **Malformed rows.** A row that does not parse is deleted during a drain. A
252
- malformed row met during fan-out fails that fan-out.
253
- - **Fan-out writes first.** Every reply is persisted before the wake is
254
- scheduled. If `scheduleWake` fails, `fanOut` rejects but the replies are
255
- already in the outbox, so replaying the settlement is safe.
256
- - **Invalid input.** `agentJob` and `decodeAgentJob` throw a schema error on
257
- input that does not match. `fanOut` rejects with
258
- `BirdDogError("invalid agent job payload")` for a job whose payload is not
259
- BirdDog's.
260
- - **Host failures.** The Promise mail room wraps a failing port in a
261
- `BirdDogError` and keeps your original error as its `cause`. The messages are
262
- `outbox read failed`, `outbox write failed`, `outbox delete failed`,
263
- `mail transport failed` and `wake scheduling failed`. A `post` that throws
264
- during a drain is logged and retried, not rethrown.
203
+ `agentJob` and `decodeAgentJob` reject invalid input using the maintained
204
+ schema. A failing Promise staging port becomes
205
+ `BirdDogError("mail staging failed")` with the original error as its `cause`.
206
+ The caller must retain the settlement cursor until staging succeeds. Delivery
207
+ remains at least once: receivers must admit a repeated mail key idempotently.
265
208
 
266
209
  ## Contribute and license
267
210
 
package/dist/effect.d.ts CHANGED
@@ -158,6 +158,7 @@ export declare const BirdDogAgentWorkSchema: Schema.Struct<{
158
158
  readonly commandFactId: Schema.String;
159
159
  readonly recovery: Schema.optional<Schema.Struct<{
160
160
  readonly maxRecoveries: Schema.Int;
161
+ readonly windowMs: Schema.optionalKey<Schema.Int>;
161
162
  }>>;
162
163
  }>;
163
164
  /** Parser for the payload that Watchdog stores without interpretation. */
@@ -237,6 +238,7 @@ export declare const BirdDogAgentJobInputSchema: Schema.Struct<{
237
238
  readonly commandFactId: Schema.String;
238
239
  readonly recovery: Schema.optional<Schema.Struct<{
239
240
  readonly maxRecoveries: Schema.Int;
241
+ readonly windowMs: Schema.optionalKey<Schema.Int>;
240
242
  }>>;
241
243
  }>;
242
244
  readonly delivery: Schema.Literals<readonly ["steer", "followUp"]>;
@@ -309,38 +311,19 @@ export declare class BirdDogError extends Error {
309
311
  readonly _tag: "BirdDogError";
310
312
  constructor(message: string, options?: ErrorOptions);
311
313
  }
312
- /** Effect host ports for the durable mail room. */
314
+ /** Durable staging belongs to the host's existing Carrier Post transaction. */
313
315
  export type EffectBirdDogOptions = Readonly<{
314
- outbox: Readonly<{
315
- /** Store one encoded mail row under its durable key. */
316
- put: (key: string, value: string) => Effect.Effect<void, BirdDogError>;
317
- /** Read one encoded mail row, if it exists. */
318
- read: (key: string) => Effect.Effect<string | undefined, BirdDogError>;
319
- /** Read an ordered page of encoded mail rows. */
320
- list: (limit: number, afterKey?: string) => Effect.Effect<readonly Readonly<{
321
- key: string;
322
- value: string;
323
- }>[], BirdDogError>;
324
- /** Remove an invalid encoded row; completed deliveries retain receipts. */
325
- delete: (key: string) => Effect.Effect<void, BirdDogError>;
326
- }>;
327
- /** Deliver one reply mail and report whether it was accepted. */
328
- post: (mail: BirdDogMail) => Effect.Effect<boolean, BirdDogError>;
329
- /** Arrange a future wake for pending delivery work. */
330
- scheduleWake: (at: number) => Effect.Effect<void, BirdDogError>;
331
- /** Return the host's current wall-clock time in milliseconds. */
316
+ /** Commit the complete reply batch through the host delivery owner. */
317
+ stage: (mail: readonly BirdDogMail[]) => Effect.Effect<void, BirdDogError>;
318
+ /** Current time used when constructing a new reply. */
332
319
  now: () => number;
333
320
  }>;
334
- /** Effect-native durable reply delivery. */
321
+ /** Agent reply construction and durable staging; Watchdog owns delivery progress. */
335
322
  export type BirdDogEffect = Readonly<{
336
- /** Queue replies owed by a settled agent job. */
323
+ /** Stage all reply letters for one settled command as one batch. */
337
324
  fanOut: (settlement: BirdDogSettlement) => Effect.Effect<void, BirdDogError>;
338
- /** Durably queue one authored mail item in this Agent's existing outbox. */
325
+ /** Stage a host-authored letter through the same durable delivery owner. */
339
326
  postMail?: ((mail: BirdDogMail) => Effect.Effect<void, BirdDogError>) | undefined;
340
- /** Deliver due replies and schedule any required retry wake. */
341
- drainOutbox: () => Effect.Effect<void, BirdDogError>;
342
- /** Earliest owed reply delivery, including retries; absent after delivery. */
343
- nextWakeAt: () => Effect.Effect<number | undefined, BirdDogError>;
344
327
  }>;
345
328
  /** Validate and translate agent work into Watchdog's opaque job grammar. */
346
329
  export declare const agentJobFromInput: (input: BirdDogAgentJobInput) => WatchdogQueuedJob;
@@ -362,10 +345,5 @@ export declare const decodeAgentJob: (job: WatchdogStoredJob) => BirdDogAgentJob
362
345
  export declare function settlementReplies(settlement: BirdDogSettlement, createdAt: number): readonly BirdDogMail[];
363
346
  /** Build the recipient's one stable acceptance notice for one original mail. */
364
347
  export declare function acceptedNotice(mail: BirdDogMail, sessionId: string, commandId: string, createdAt: number): BirdDogMail;
365
- /**
366
- * Whether this mail room's outbox still owes any reply a delivery attempt.
367
- * Delivered receipts and exhausted dead letters stay but owe nothing.
368
- */
369
- export declare const hasPendingDelivery: (outbox: Pick<EffectBirdDogOptions["outbox"], "list">) => Effect.Effect<boolean, BirdDogError>;
370
- /** Construct BirdDog's Effect-native mail room over Post's shared outbox. */
348
+ /** Construct BirdDog's reply semantics over the existing durable staging owner. */
371
349
  export declare const make: (options: EffectBirdDogOptions) => Effect.Effect<BirdDogEffect>;
package/dist/effect.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Schema from "effect/Schema";
3
- import { makePostOutbox, postOutboxPending, } from "@fungi.computer/carrier/post";
4
3
  const Identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
5
4
  /** The maximum UTF-8 size of one mail body. */
6
5
  export const BIRD_DOG_MAIL_CONTENT_MAX_BYTES = 256 * 1024;
@@ -147,6 +146,7 @@ export const BirdDogAgentWorkSchema = Schema.Struct({
147
146
  commandFactId: Identifier,
148
147
  recovery: Schema.optional(Schema.Struct({
149
148
  maxRecoveries: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
149
+ windowMs: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
150
150
  })),
151
151
  });
152
152
  /** Parser for the payload that Watchdog stores without interpretation. */
@@ -266,49 +266,13 @@ export function acceptedNotice(mail, sessionId, commandId, createdAt) {
266
266
  reply,
267
267
  };
268
268
  }
269
- /**
270
- * Whether this mail room's outbox still owes any reply a delivery attempt.
271
- * Delivered receipts and exhausted dead letters stay but owe nothing.
272
- */
273
- export const hasPendingDelivery = (outbox) => postOutboxPending({ message: BirdDogMailSchema, outbox });
274
- /** Construct BirdDog's Effect-native mail room over Post's shared outbox. */
275
- export const make = (options) => {
276
- const post = makePostOutbox({
277
- message: BirdDogMailSchema,
278
- key: (mail) => `outbox:${mail.key}`,
279
- outbox: options.outbox,
280
- deliver: options.post,
281
- scheduleWake: options.scheduleWake,
282
- now: options.now,
283
- invalidRow: () => new BirdDogError("invalid outbox row"),
284
- encodingFailed: () => new BirdDogError("could not encode outbox row"),
285
- });
286
- const fanOut = (settlement) => Effect.gen(function* () {
287
- const createdAt = options.now();
288
- const replies = yield* Effect.try({
289
- try: () => settlementReplies(settlement, createdAt),
290
- catch: () => new BirdDogError("invalid agent job payload"),
291
- });
292
- let wake;
293
- for (const reply of replies) {
294
- const hint = yield* post.enqueue(reply, createdAt);
295
- if (hint !== undefined)
296
- wake = wake === undefined ? hint : Math.min(wake, hint);
297
- }
298
- if (wake !== undefined)
299
- yield* options.scheduleWake(wake);
300
- });
301
- const postMail = (mail) => Effect.gen(function* () {
302
- const createdAt = options.now();
303
- const hint = yield* post.enqueue(mail, createdAt);
304
- if (hint !== undefined)
305
- yield* options.scheduleWake(hint);
306
- return;
307
- });
308
- return Effect.succeed({
309
- fanOut,
310
- postMail,
311
- drainOutbox: post.drain,
312
- nextWakeAt: post.nextWakeAt,
313
- });
314
- };
269
+ /** Construct BirdDog's reply semantics over the existing durable staging owner. */
270
+ export const make = (options) => Effect.succeed({
271
+ /** Stage the complete settlement reply batch. */
272
+ fanOut: (settlement) => Effect.try({
273
+ try: () => settlementReplies(settlement, options.now()),
274
+ catch: (cause) => new BirdDogError("invalid agent job payload", { cause }),
275
+ }).pipe(Effect.flatMap(options.stage)),
276
+ /** Stage one host-authored letter. */
277
+ postMail: (mail) => options.stage([mail]),
278
+ });
package/dist/index.d.ts CHANGED
@@ -87,6 +87,7 @@ export type BirdDogAgentWork = Readonly<{
87
87
  commandFactId: string;
88
88
  recovery?: Readonly<{
89
89
  maxRecoveries: number;
90
+ windowMs?: number;
90
91
  }> | undefined;
91
92
  }>;
92
93
  /** Typed payload whose meaning belongs to BirdDog, never Watchdog. */
@@ -103,29 +104,11 @@ export type BirdDogAgentJobInput = Readonly<{
103
104
  delivery: "steer" | "followUp";
104
105
  mail?: BirdDogMail | undefined;
105
106
  }>;
106
- /** String-backed durable outbox owned by the BirdDog host. */
107
- export type BirdDogOutbox = Readonly<{
108
- /** Store one encoded mail row under its durable key. */
109
- put: (key: string, value: string) => Promise<void>;
110
- /** Read one encoded mail row, if it exists. */
111
- read: (key: string) => Promise<string | undefined>;
112
- /** Read an ordered page of encoded mail rows. */
113
- list: (limit: number, afterKey?: string) => Promise<readonly Readonly<{
114
- key: string;
115
- value: string;
116
- }>[]>;
117
- /** Remove an invalid encoded row; completed deliveries retain receipts. */
118
- delete: (key: string) => Promise<void>;
119
- }>;
120
- /** Host ports for BirdDog's durable mail delivery. */
107
+ /** Host ports for reply staging in its existing Carrier Post owner. */
121
108
  export type BirdDogOptions = Readonly<{
122
- /** Durable store for pending reply mail. */
123
- outbox: BirdDogOutbox;
124
- /** Deliver one reply mail and report whether it was accepted. */
125
- post: (mail: BirdDogMail) => Promise<boolean>;
126
- /** Arrange a future wake for pending delivery work. */
127
- scheduleWake: (at: number) => Promise<void>;
128
- /** Return the host's current wall-clock time in milliseconds. */
109
+ /** Commit the complete reply batch through the host delivery owner. */
110
+ stage: (mail: readonly BirdDogMail[]) => Promise<void>;
111
+ /** Current time used when constructing a new reply. */
129
112
  now: () => number;
130
113
  }>;
131
114
  /**
@@ -172,8 +155,6 @@ export type BirdDogWaitingItem = Readonly<{
172
155
  export type BirdDogRuntime = Readonly<{
173
156
  /** Queue replies owed by a settled agent job. */
174
157
  fanOut: (settlement: BirdDogSettlement) => Promise<void>;
175
- /** Deliver due replies and schedule any required retry wake. */
176
- drainOutbox: () => Promise<void>;
177
158
  }>;
178
159
  /** BirdDog's small synchronous translation interface for owner transactions. */
179
160
  export type BirdDogApi = Readonly<{
@@ -11,41 +11,14 @@ const runPromise = (effect) => Effect.runPromiseExit(effect).then((exit) => {
11
11
  });
12
12
  /** Lift Promise host ports once into BirdDog's Effect-owned mail room. */
13
13
  export const createPromiseBirdDog = async (options) => {
14
- const readOutbox = options.outbox.read;
15
- const readPort = {
16
- read: (key) => Effect.tryPromise({
17
- try: () => readOutbox(key),
18
- catch: (cause) => new BirdDogError("outbox read failed", { cause }),
19
- }),
20
- };
21
14
  const runtime = await Effect.runPromise(make({
22
- outbox: {
23
- ...readPort,
24
- put: (key, value) => Effect.tryPromise({
25
- try: () => options.outbox.put(key, value),
26
- catch: (cause) => new BirdDogError("outbox write failed", { cause }),
27
- }),
28
- list: (limit, afterKey) => Effect.tryPromise({
29
- try: () => options.outbox.list(limit, afterKey),
30
- catch: (cause) => new BirdDogError("outbox read failed", { cause }),
31
- }),
32
- delete: (key) => Effect.tryPromise({
33
- try: () => options.outbox.delete(key),
34
- catch: (cause) => new BirdDogError("outbox delete failed", { cause }),
35
- }),
36
- },
37
- post: (mail) => Effect.tryPromise({
38
- try: () => options.post(mail),
39
- catch: (cause) => new BirdDogError("mail transport failed", { cause }),
40
- }),
41
- scheduleWake: (at) => Effect.tryPromise({
42
- try: () => options.scheduleWake(at),
43
- catch: (cause) => new BirdDogError("wake scheduling failed", { cause }),
15
+ stage: (mail) => Effect.tryPromise({
16
+ try: () => options.stage(mail),
17
+ catch: (cause) => new BirdDogError("mail staging failed", { cause }),
44
18
  }),
45
19
  now: options.now,
46
20
  }));
47
21
  return Object.freeze({
48
22
  fanOut: (settlement) => runPromise(runtime.fanOut(settlement)),
49
- drainOutbox: () => runPromise(runtime.drainOutbox()),
50
23
  });
51
24
  };
@@ -59,28 +59,12 @@ Stable discriminator for BirdDog-owned effect failures.
59
59
 
60
60
  ```ts
61
61
  type EffectBirdDogOptions = Readonly<{
62
- outbox: Readonly<{
63
- put: (key, value) => Effect.Effect<void, BirdDogError>;
64
- read: (key) => Effect.Effect<string | undefined, BirdDogError>;
65
- list: (
66
- limit,
67
- afterKey?,
68
- ) => Effect.Effect<
69
- readonly Readonly<{
70
- key: string;
71
- value: string;
72
- }>[],
73
- BirdDogError
74
- >;
75
- delete: (key) => Effect.Effect<void, BirdDogError>;
76
- }>;
77
- post: (mail) => Effect.Effect<boolean, BirdDogError>;
78
- scheduleWake: (at) => Effect.Effect<void, BirdDogError>;
62
+ stage: (mail) => Effect.Effect<void, BirdDogError>;
79
63
  now: () => number;
80
64
  }>;
81
65
  ```
82
66
 
83
- Effect host ports for the durable mail room.
67
+ Durable staging belongs to the host's existing Carrier Post transaction.
84
68
 
85
69
  ---
86
70
 
@@ -90,12 +74,10 @@ Effect host ports for the durable mail room.
90
74
  type BirdDogEffect = Readonly<{
91
75
  fanOut: (settlement) => Effect.Effect<void, BirdDogError>;
92
76
  postMail?: (mail) => Effect.Effect<void, BirdDogError>;
93
- drainOutbox: () => Effect.Effect<void, BirdDogError>;
94
- nextWakeAt: () => Effect.Effect<number | undefined, BirdDogError>;
95
77
  }>;
96
78
  ```
97
79
 
98
- Effect-native durable reply delivery.
80
+ Agent reply construction and durable staging; Watchdog owns delivery progress.
99
81
 
100
82
  ## Variables
101
83
 
@@ -376,6 +358,7 @@ const BirdDogAgentWorkSchema: Struct<{
376
358
  recovery: optional<
377
359
  Struct<{
378
360
  maxRecoveries: Int;
361
+ windowMs: optionalKey<Int>;
379
362
  }>
380
363
  >;
381
364
  }>;
@@ -455,6 +438,7 @@ const BirdDogAgentJobInputSchema: Struct<{
455
438
  commandFactId: String;
456
439
  recovery: optional<Struct<{
457
440
  maxRecoveries: Int;
441
+ windowMs: optionalKey<Int>;
458
442
  }>>;
459
443
  }>;
460
444
  delivery: Literals<readonly ["steer", "followUp"]>;
@@ -560,8 +544,10 @@ Validate and translate agent work into Watchdog's opaque job grammar.
560
544
  | `lane` | `string` |
561
545
  | `priority` | `number` |
562
546
  | `payload` | `unknown` |
547
+ | `notBefore?` | `number` |
563
548
  | `recovery?` | `object` |
564
549
  | `recovery.maxRecoveries` | `number` |
550
+ | `recovery.windowMs?` | `number` |
565
551
 
566
552
  ---
567
553
 
@@ -575,9 +561,9 @@ Decode the opaque payload at BirdDog's side of the Watchdog seam.
575
561
 
576
562
  #### Parameters
577
563
 
578
- | Parameter | Type |
579
- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
580
- | `job` | \| \{ `state`: `"queued"`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `recovery?`: \{ `maxRecoveries`: `number`; \}; \} \| \{ `state`: `"running"`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `recovery?`: \{ `maxRecoveries`: `number`; \}; \} \| \{ `state`: `"settled"`; `outcome`: `"failed"` \| `"completed"` \| `"interrupted"` \| `"cancelled"` \| `"outcome_unknown"`; `failureReason?`: `string`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `recovery?`: \{ `maxRecoveries`: `number`; \}; \} |
564
+ | Parameter | Type |
565
+ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
566
+ | `job` | \| \{ `state`: `"queued"`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `notBefore?`: `number`; `recovery?`: \{ `maxRecoveries`: `number`; `windowMs?`: `number`; \}; \} \| \{ `state`: `"running"`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `notBefore?`: `number`; `recovery?`: \{ `maxRecoveries`: `number`; `windowMs?`: `number`; \}; \} \| \{ `state`: `"completing"`; `outcome`: `"failed"` \| `"completed"` \| `"interrupted"` \| `"cancelled"` \| `"outcome_unknown"`; `failureReason?`: `string`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `notBefore?`: `number`; `recovery?`: \{ `maxRecoveries`: `number`; `windowMs?`: `number`; \}; \} \| \{ `state`: `"settled"`; `outcome`: `"failed"` \| `"completed"` \| `"interrupted"` \| `"cancelled"` \| `"outcome_unknown"`; `failureReason?`: `string`; `jobId`: `string`; `queue`: `string`; `lane`: `string`; `priority`: `number`; `payload`: `unknown`; `notBefore?`: `number`; `recovery?`: \{ `maxRecoveries`: `number`; `windowMs?`: `number`; \}; \} |
581
567
 
582
568
  #### Returns
583
569
 
@@ -639,27 +625,6 @@ Build the recipient's one stable acceptance notice for one original mail.
639
625
 
640
626
  ---
641
627
 
642
- ### hasPendingDelivery()
643
-
644
- ```ts
645
- function hasPendingDelivery(outbox): Effect<boolean, BirdDogError>;
646
- ```
647
-
648
- Whether this mail room's outbox still owes any reply a delivery attempt.
649
- Delivered receipts and exhausted dead letters stay but owe nothing.
650
-
651
- #### Parameters
652
-
653
- | Parameter | Type |
654
- | --------- | --------------------------------------------------------------------------------- |
655
- | `outbox` | `Pick`\<[`EffectBirdDogOptions`](#effectbirddogoptions)\[`"outbox"`\], `"list"`\> |
656
-
657
- #### Returns
658
-
659
- `Effect`\<`boolean`, [`BirdDogError`](#birddogerror)\>
660
-
661
- ---
662
-
663
628
  ### make()
664
629
 
665
630
  ```ts
@@ -667,13 +632,11 @@ function make(options): Effect<
667
632
  Readonly<{
668
633
  fanOut: (settlement) => Effect<void, BirdDogError>;
669
634
  postMail?: (mail) => Effect<void, BirdDogError>;
670
- drainOutbox: () => Effect<void, BirdDogError>;
671
- nextWakeAt: () => Effect<number | undefined, BirdDogError>;
672
635
  }>
673
636
  >;
674
637
  ```
675
638
 
676
- Construct BirdDog's Effect-native mail room over Post's shared outbox.
639
+ Construct BirdDog's reply semantics over the existing durable staging owner.
677
640
 
678
641
  #### Parameters
679
642
 
@@ -685,6 +648,4 @@ Construct BirdDog's Effect-native mail room over Post's shared outbox.
685
648
 
686
649
  `Effect`\<`Readonly`\<\{ `fanOut`: (`settlement`) => `Effect`\<`void`,
687
650
  [`BirdDogError`](#birddogerror)\>; `postMail?`: (`mail`) => `Effect`\<`void`,
688
- [`BirdDogError`](#birddogerror)\>; `drainOutbox`: () => `Effect`\<`void`,
689
- [`BirdDogError`](#birddogerror)\>; `nextWakeAt`: () => `Effect`\<`number` \|
690
- `undefined`, [`BirdDogError`](#birddogerror)\>; \}\>\>
651
+ [`BirdDogError`](#birddogerror)\>; \}\>\>
package/docs/api/index.md CHANGED
@@ -155,6 +155,7 @@ type BirdDogAgentWork = Readonly<{
155
155
  commandFactId: string;
156
156
  recovery?: Readonly<{
157
157
  maxRecoveries: number;
158
+ windowMs?: number;
158
159
  }>;
159
160
  }>;
160
161
  ```
@@ -193,41 +194,16 @@ Input for projecting accepted agent work into Watchdog's generic grammar.
193
194
 
194
195
  ---
195
196
 
196
- ### BirdDogOutbox
197
-
198
- ```ts
199
- type BirdDogOutbox = Readonly<{
200
- put: (key, value) => Promise<void>;
201
- read: (key) => Promise<string | undefined>;
202
- list: (
203
- limit,
204
- afterKey?,
205
- ) => Promise<
206
- readonly Readonly<{
207
- key: string;
208
- value: string;
209
- }>[]
210
- >;
211
- delete: (key) => Promise<void>;
212
- }>;
213
- ```
214
-
215
- String-backed durable outbox owned by the BirdDog host.
216
-
217
- ---
218
-
219
197
  ### BirdDogOptions
220
198
 
221
199
  ```ts
222
200
  type BirdDogOptions = Readonly<{
223
- outbox: BirdDogOutbox;
224
- post: (mail) => Promise<boolean>;
225
- scheduleWake: (at) => Promise<void>;
201
+ stage: (mail) => Promise<void>;
226
202
  now: () => number;
227
203
  }>;
228
204
  ```
229
205
 
230
- Host ports for BirdDog's durable mail delivery.
206
+ Host ports for reply staging in its existing Carrier Post owner.
231
207
 
232
208
  ---
233
209
 
@@ -282,7 +258,6 @@ One open item named by a reply as waiting on its recipient.
282
258
  ```ts
283
259
  type BirdDogRuntime = Readonly<{
284
260
  fanOut: (settlement) => Promise<void>;
285
- drainOutbox: () => Promise<void>;
286
261
  }>;
287
262
  ```
288
263
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/bird-dog",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Durable addressed agent mail over generic Watchdog work",
6
6
  "type": "module",
@@ -23,8 +23,7 @@
23
23
  "LICENSE"
24
24
  ],
25
25
  "dependencies": {
26
- "@fungi.computer/carrier": "0.2.1",
27
- "@fungi.computer/watchdog": "0.1.0",
26
+ "@fungi.computer/watchdog": "0.1.1",
28
27
  "effect": "4.0.2"
29
28
  },
30
29
  "devDependencies": {