@fungi.computer/shiitake 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +19 -18
  2. package/dist/agent-control.d.ts +24 -19
  3. package/dist/agent-control.js +69 -128
  4. package/dist/client-wire.d.ts +12 -3
  5. package/dist/client-wire.js +33 -8
  6. package/dist/client.d.ts +34 -3
  7. package/dist/client.js +1 -1
  8. package/dist/cloudflare.d.ts +3 -1
  9. package/dist/cloudflare.js +2 -2
  10. package/dist/delegation.d.ts +151 -17
  11. package/dist/delegation.js +174 -41
  12. package/dist/effect.d.ts +1 -1
  13. package/dist/fleet.d.ts +37 -10
  14. package/dist/fleet.js +217 -48
  15. package/dist/http-routes.d.ts +84 -12
  16. package/dist/http-routes.js +15 -1
  17. package/dist/index.d.ts +2 -2
  18. package/dist/internal/admission-door.d.ts +2 -0
  19. package/dist/internal/admission-door.js +9 -2
  20. package/dist/internal/client-boundary.d.ts +10 -0
  21. package/dist/internal/client-boundary.js +10 -0
  22. package/dist/internal/compaction/compaction.js +1 -1
  23. package/dist/internal/contracts.d.ts +2 -0
  24. package/dist/internal/coordinator.d.ts +3 -1
  25. package/dist/internal/coordinator.js +98 -13
  26. package/dist/internal/effect-client.d.ts +3 -1
  27. package/dist/internal/effect-client.js +3 -2
  28. package/dist/internal/effect-contracts.d.ts +6 -2
  29. package/dist/internal/model-acquisition.js +2 -0
  30. package/dist/internal/observation-kernel.d.ts +8 -4
  31. package/dist/internal/observation-kernel.js +44 -9
  32. package/dist/internal/promise-facade.d.ts +1 -1
  33. package/dist/internal/promise-facade.js +1 -1
  34. package/dist/internal/run-scope.js +37 -8
  35. package/dist/internal/runtime-coordinator.d.ts +5 -0
  36. package/dist/internal/runtime-coordinator.js +8 -4
  37. package/dist/internal/runtime-core.d.ts +2 -1
  38. package/dist/internal/runtime-core.js +6 -1
  39. package/dist/internal/session-context.d.ts +1 -1
  40. package/dist/internal/session-context.js +84 -13
  41. package/dist/internal/woodstock/agent-reply.d.ts +7 -2
  42. package/dist/internal/woodstock/agent-reply.js +22 -13
  43. package/dist/internal/woodstock/fleet-store.d.ts +4 -2
  44. package/dist/internal/woodstock/fleet-store.js +70 -6
  45. package/dist/internal/woodstock/index.d.ts +2 -0
  46. package/dist/internal/woodstock/index.js +11 -5
  47. package/dist/internal/woodstock/run-recovery.js +4 -1
  48. package/dist/internal/woodstock/session-entry.d.ts +13 -3
  49. package/dist/internal/woodstock/session-entry.js +1 -0
  50. package/dist/internal/woodstock/session-store.d.ts +4 -0
  51. package/dist/internal/woodstock/session-store.js +26 -19
  52. package/dist/remote-client.js +5 -1
  53. package/dist/testing.d.ts +5 -2
  54. package/dist/testing.js +4 -2
  55. package/docs/api/agent-control.md +18 -12
  56. package/docs/api/client.md +78 -0
  57. package/docs/api/delegation.md +72 -55
  58. package/docs/api/effect.md +5 -0
  59. package/docs/api/fleet.md +23 -23
  60. package/docs/api/http.md +2 -1
  61. package/docs/api/index.md +13 -0
  62. package/docs/api/testing.md +3 -3
  63. package/package.json +9 -8
@@ -333,10 +333,10 @@ export function makeCloudflareShiitake(unnamed, options) {
333
333
  return Object.freeze({
334
334
  ...handle,
335
335
  readSettlement: (sessionId, commandId) => runShiitakePromise(readSettlement(sessionId, commandId)),
336
- acceptMail: (mail, meta, reply) => {
336
+ acceptMail: (mail, meta, reply, stageAcceptedNotice, sessionGrantDelivery) => {
337
337
  if (options.mailRoom === undefined)
338
338
  return Promise.reject(runtimeFailure("unavailable", "Addressed mail is unavailable"));
339
- return runShiitakePromise(acceptMail(mail, meta, reply));
339
+ return runShiitakePromise(acceptMail(mail, meta, reply, stageAcceptedNotice, sessionGrantDelivery));
340
340
  },
341
341
  alarm: host.alarm,
342
342
  activeSessions: () => runShiitakePromise(activeSessions),
@@ -1,9 +1,9 @@
1
1
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
2
  import * as Schema from "effect/Schema";
3
3
  import { type BirdDogEffect } from "@fungi.computer/bird-dog/effect";
4
- import type { BirdDogMail } from "@fungi.computer/bird-dog";
4
+ import { type BirdDogMail } from "@fungi.computer/bird-dog";
5
5
  import { type PostRoute, type PostRuntime, type PostEnvelope, type PostDeliveryResult } from "@fungi.computer/carrier/post";
6
- import { type Accepted, type CancelResult } from "./client.js";
6
+ import { type Accepted, type DelegationReceipt, type CancelResult } from "./client.js";
7
7
  import type { CommandSettlement, ShiitakeHost, VerifiedAgentReply } from "./index.js";
8
8
  import type { AgentControlDirectory, AgentDelegationInput, AgentDelegationInvocation, AgentRecallInput } from "./agent-control.js";
9
9
  /** Shiitake host-pack identity; core consumes OysterAgents, never this engine. */
@@ -19,30 +19,39 @@ export declare class AgentDelegation {
19
19
  expiresAt: () => number;
20
20
  identity: () => DelegationIdentity | null | Promise<DelegationIdentity | null>;
21
21
  directory: () => AgentControlDirectory;
22
- deliver: (mail: BirdDogMail) => Promise<Accepted>;
22
+ deliver: (mail: BirdDogMail) => Promise<DelegationReceipt>;
23
23
  prepare: (mail: BirdDogMail) => Promise<void>;
24
+ /**
25
+ * Synchronously stage the accepted notice in this recipient's durable
26
+ * outbox, in the same transaction as acceptance.
27
+ */
28
+ stageAcceptedNotice: (mail: BirdDogMail, scopeId: string) => void;
24
29
  /** Admit mail; `reply` is present only for an authenticated reply. */
25
- accept: (mail: BirdDogMail, reply?: VerifiedAgentReply) => Promise<Accepted>;
30
+ accept: (mail: BirdDogMail, reply: VerifiedAgentReply | undefined, stageAcceptedNotice: (mail: BirdDogMail) => void) => Promise<Accepted>;
26
31
  }>);
27
32
  private fleetOwner;
28
33
  private fleet;
29
34
  private scope;
30
- private requestKey;
31
35
  /** Recipient-local disclosure through the public host, never another Agent's RPC. */
32
36
  settlement(sender: DelegationIdentity, sessionId: string, commandId: string, host: Pick<ShiitakeHost, "readSettlement">): Promise<CommandSettlement | undefined>;
33
37
  /** Delegate work to an in-scope Agent: steer its Session, or start fleet correspondence. */
34
- send(input: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation): Promise<Accepted>;
38
+ send(input: AgentDelegationInput, sourceSessionId: string, signal: AbortSignal, invocation?: AgentDelegationInvocation): Promise<DelegationReceipt>;
35
39
  /** Resolve this sender's exact request, then let the recipient guard pending work. */
36
40
  recall(input: AgentRecallInput, sourceSessionId: string, signal: AbortSignal): Promise<CancelResult>;
37
41
  /**
38
42
  * A reply-less envelope is only ever BirdDog's reply to work this Agent
39
43
  * sent. Admit it only after the fleet authenticates it against that frozen
40
- * mail; the line is then authored by the replying Agent, never by whoever
41
- * settled or cancelled the work, and its meta carries the verified reply.
44
+ * mail. A recipient-left reply remains Agent-authored; a recipient-offline
45
+ * reply is Hub-authored. Its meta carries only the authenticated reply.
42
46
  */
43
47
  private authenticatedReply;
48
+ private parseAddressedMail;
44
49
  /** Admit one addressed Agent mail for this Agent, or refuse it. */
45
50
  receive(raw: PostEnvelope): Promise<PostDeliveryResult>;
51
+ private recordAcceptedNotice;
52
+ private acceptAdmitted;
53
+ /** Admit local outbox exhaustion through the same Fleet/system reply gate. */
54
+ localUndeliverable(original: BirdDogMail): Promise<PostDeliveryResult>;
46
55
  }
47
56
  /** A current correspondent no longer has disclosure authority. */
48
57
  export declare class AgentDisclosureRefused extends Error {
@@ -63,7 +72,7 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
63
72
  readonly to: {
64
73
  readonly kind: "agent";
65
74
  readonly agentId: string;
66
- readonly sessionId: string;
75
+ readonly sessionId?: string;
67
76
  };
68
77
  readonly replyTo: readonly {
69
78
  readonly kind: "agent";
@@ -75,7 +84,36 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
75
84
  readonly commandId: string;
76
85
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
77
86
  readonly diagnostic?: string;
87
+ } | {
88
+ readonly outcome: "accepted";
89
+ readonly commandId: string;
90
+ } | {
91
+ readonly outcome: "undeliverable";
92
+ readonly reason: "recipient_left";
93
+ };
94
+ } | {
95
+ readonly key: string;
96
+ readonly reply: {
97
+ readonly outcome: "undeliverable";
98
+ readonly reason: "recipient_offline";
99
+ };
100
+ readonly content: string;
101
+ readonly createdAt: number;
102
+ readonly from: {
103
+ readonly kind: "system";
104
+ readonly systemId: "hub";
78
105
  };
106
+ readonly to: {
107
+ readonly kind: "agent";
108
+ readonly agentId: string;
109
+ readonly sessionId?: string;
110
+ };
111
+ readonly replyTo: readonly {
112
+ readonly kind: "agent";
113
+ readonly agentId: string;
114
+ readonly sessionId: string;
115
+ }[];
116
+ readonly meta?: Schema.Json;
79
117
  }, {
80
118
  readonly key: string;
81
119
  readonly content: string;
@@ -88,7 +126,7 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
88
126
  readonly to: {
89
127
  readonly kind: "agent";
90
128
  readonly agentId: string;
91
- readonly sessionId: string;
129
+ readonly sessionId?: string;
92
130
  };
93
131
  readonly replyTo: readonly {
94
132
  readonly kind: "agent";
@@ -100,7 +138,36 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
100
138
  readonly commandId: string;
101
139
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
102
140
  readonly diagnostic?: string;
141
+ } | {
142
+ readonly outcome: "accepted";
143
+ readonly commandId: string;
144
+ } | {
145
+ readonly outcome: "undeliverable";
146
+ readonly reason: "recipient_left";
147
+ };
148
+ } | {
149
+ readonly key: string;
150
+ readonly reply: {
151
+ readonly outcome: "undeliverable";
152
+ readonly reason: "recipient_offline";
153
+ };
154
+ readonly content: string;
155
+ readonly createdAt: number;
156
+ readonly from: {
157
+ readonly kind: "system";
158
+ readonly systemId: "hub";
159
+ };
160
+ readonly to: {
161
+ readonly kind: "agent";
162
+ readonly agentId: string;
163
+ readonly sessionId?: string;
103
164
  };
165
+ readonly replyTo: readonly {
166
+ readonly kind: "agent";
167
+ readonly agentId: string;
168
+ readonly sessionId: string;
169
+ }[];
170
+ readonly meta?: Schema.Json;
104
171
  }, import("@fungi.computer/seam/machine-link-protocol").AgentMailAddress, {
105
172
  readonly key: string;
106
173
  readonly content: string;
@@ -113,7 +180,7 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
113
180
  readonly to: {
114
181
  readonly kind: "agent";
115
182
  readonly agentId: string;
116
- readonly sessionId: string;
183
+ readonly sessionId?: string;
117
184
  };
118
185
  readonly replyTo: readonly {
119
186
  readonly kind: "agent";
@@ -125,7 +192,36 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
125
192
  readonly commandId: string;
126
193
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
127
194
  readonly diagnostic?: string;
195
+ } | {
196
+ readonly outcome: "accepted";
197
+ readonly commandId: string;
198
+ } | {
199
+ readonly outcome: "undeliverable";
200
+ readonly reason: "recipient_left";
201
+ };
202
+ } | {
203
+ readonly key: string;
204
+ readonly reply: {
205
+ readonly outcome: "undeliverable";
206
+ readonly reason: "recipient_offline";
207
+ };
208
+ readonly content: string;
209
+ readonly createdAt: number;
210
+ readonly from: {
211
+ readonly kind: "system";
212
+ readonly systemId: "hub";
213
+ };
214
+ readonly to: {
215
+ readonly kind: "agent";
216
+ readonly agentId: string;
217
+ readonly sessionId?: string;
128
218
  };
219
+ readonly replyTo: readonly {
220
+ readonly kind: "agent";
221
+ readonly agentId: string;
222
+ readonly sessionId: string;
223
+ }[];
224
+ readonly meta?: Schema.Json;
129
225
  }, {
130
226
  readonly key: string;
131
227
  readonly content: string;
@@ -138,7 +234,7 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
138
234
  readonly to: {
139
235
  readonly kind: "agent";
140
236
  readonly agentId: string;
141
- readonly sessionId: string;
237
+ readonly sessionId?: string;
142
238
  };
143
239
  readonly replyTo: readonly {
144
240
  readonly kind: "agent";
@@ -150,9 +246,39 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
150
246
  readonly commandId: string;
151
247
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
152
248
  readonly diagnostic?: string;
249
+ } | {
250
+ readonly outcome: "accepted";
251
+ readonly commandId: string;
252
+ } | {
253
+ readonly outcome: "undeliverable";
254
+ readonly reason: "recipient_left";
255
+ };
256
+ } | {
257
+ readonly key: string;
258
+ readonly reply: {
259
+ readonly outcome: "undeliverable";
260
+ readonly reason: "recipient_offline";
261
+ };
262
+ readonly content: string;
263
+ readonly createdAt: number;
264
+ readonly from: {
265
+ readonly kind: "system";
266
+ readonly systemId: "hub";
267
+ };
268
+ readonly to: {
269
+ readonly kind: "agent";
270
+ readonly agentId: string;
271
+ readonly sessionId?: string;
153
272
  };
273
+ readonly replyTo: readonly {
274
+ readonly kind: "agent";
275
+ readonly agentId: string;
276
+ readonly sessionId: string;
277
+ }[];
278
+ readonly meta?: Schema.Json;
154
279
  }> & Omit<Readonly<{
155
280
  addressKind: string;
281
+ deadlineMs?: number | undefined;
156
282
  address: Schema.Codec<unknown, unknown>;
157
283
  event: Schema.Codec<unknown, unknown>;
158
284
  encodeEnvelope: (envelope: PostEnvelope) => PostEnvelope;
@@ -167,7 +293,7 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
167
293
  /** The host's one Post lane, as Agent mail borrows it. */
168
294
  export type AgentMailPost = Pick<PostRuntime<{
169
295
  agentMail: ReturnType<typeof agentMailRoute>;
170
- }>, "send" | "read" | "nextWake">;
296
+ }>, "send" | "read" | "nextWake" | "deadLetters">;
171
297
  /** Recipient-owned replies and receipt-bearing submissions over the host's one Post lane. */
172
298
  export declare class AgentMail {
173
299
  private readonly deps;
@@ -177,14 +303,22 @@ export declare class AgentMail {
177
303
  identity: () => DelegationIdentity | null | Promise<DelegationIdentity | null>;
178
304
  /** Only a typed disclosure refusal becomes undefined; missing settled facts must fail. */
179
305
  replySettlement: AgentReplySettlement;
306
+ /** Route a terminal reply for this sender through its Fleet admission. */
307
+ onUndeliverable: (original: BirdDogMail, reply: BirdDogMail) => Promise<PostDeliveryResult>;
180
308
  drain: () => Promise<void>;
181
309
  wake: () => Promise<void>;
182
310
  }>);
183
311
  private envelope;
184
- /** Submit one mail and return the recipient's Accepted receipt. */
185
- send(mail: BirdDogMail): Promise<Accepted>;
312
+ /** Submit one mail and return recipient acceptance or relay-held custody. */
313
+ send(mail: BirdDogMail): Promise<DelegationReceipt>;
314
+ /** Reconcile exhausted original outbox mail into one local system reply. */
315
+ stageUndeliverableReplies(): Promise<void>;
316
+ /** Stage the acceptance reply through this Post owner without leaving the transaction. */
317
+ stageAcceptedNotice(mail: BirdDogMail, scopeId: string): void;
186
318
  /** BirdDog's mail room: disclosed settlement replies fan out over Post. */
187
- mailRoom(): BirdDogEffect;
319
+ mailRoom(): BirdDogEffect & {
320
+ stageAcceptedNotice: (mail: BirdDogMail, scopeId: string) => void;
321
+ };
188
322
  }
189
323
  /**
190
324
  * The Agent wire decoders a host lends Oyster's machine-link client, so the
@@ -197,7 +331,7 @@ export declare const machineAgentCodecs: {
197
331
  }> | null;
198
332
  /** One serialized settlement from a hosted or machine-homed Agent. */
199
333
  readonly settlement: (encoded: string) => CommandSettlement;
200
- /** Throws unless an acknowledgment carries a well-formed Accepted receipt. */
334
+ /** Throws unless an acknowledgment carries a valid acceptance or Held result. */
201
335
  readonly accepted: (receipt: Schema.Json) => void;
202
336
  };
203
337
  /**
@@ -3,15 +3,15 @@
3
3
  * addressed Agent mail over Carrier Post, and the Agent grammar a host hands
4
4
  * to Oyster's machine link as ports. Oyster core never imports this engine.
5
5
  */
6
- import { commandSettlementSchema, createAgentDirectory, createFleetCorrespondence, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "./fleet.js";
7
- import { deterministicReplayKey } from "@fungi.computer/cairn";
6
+ import { commandSettlementSchema, createAgentDirectory, createFleetCorrespondence, agentDelegationRequestKey, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "./fleet.js";
8
7
  import * as Schema from "effect/Schema";
9
8
  import { agentMailAddressSchema } from "@fungi.computer/seam/machine-link-protocol";
10
9
  import { BirdDogMailSchema, BirdDogError, settlementReplies, } from "@fungi.computer/bird-dog/effect";
11
10
  import * as Effect from "effect/Effect";
12
11
  import { messageAuthor } from "@fungi.computer/agar/agents";
12
+ import { undeliverableReply } from "@fungi.computer/bird-dog";
13
13
  import { PostEnvelopeSchema, definePostRoute, } from "@fungi.computer/carrier/post";
14
- import { parseAccepted } from "./client.js";
14
+ import { parseDelegationReceipt, } from "./client.js";
15
15
  import { decodeClientSessionCreationRequest } from "./internal/client-boundary.js";
16
16
  /** Host-owned identity and admission around BirdDog's ordinary envelope. */
17
17
  export class AgentDelegation {
@@ -37,16 +37,6 @@ export class AgentDelegation {
37
37
  throw new Error("Agent is outside this scope");
38
38
  return { ...identity, agents, source };
39
39
  }
40
- requestKey(scope, input, sourceSessionId) {
41
- return deterministicReplayKey("oyster-agent-delegation", [
42
- scope.scopeId,
43
- scope.agentId,
44
- sourceSessionId,
45
- input.agentId,
46
- input.sessionId,
47
- input.requestId,
48
- ]);
49
- }
50
40
  /** Recipient-local disclosure through the public host, never another Agent's RPC. */
51
41
  async settlement(sender, sessionId, commandId, host) {
52
42
  const scope = await this.scope(new AbortController().signal);
@@ -65,7 +55,16 @@ export class AgentDelegation {
65
55
  const scope = await this.scope(signal);
66
56
  if (!scope.agents.some(({ agentId }) => agentId === input.agentId))
67
57
  throw new Error("Agent is outside this scope");
68
- const requestId = await this.requestKey(scope, input, sourceSessionId);
58
+ const requestId = await agentDelegationRequestKey({
59
+ owner: {
60
+ kind: "hosted",
61
+ scopeId: scope.scopeId,
62
+ agentId: scope.agentId,
63
+ },
64
+ sourceSessionId,
65
+ targetAgentId: input.agentId,
66
+ requestId: input.requestId,
67
+ });
69
68
  signal.throwIfAborted();
70
69
  if (input.delivery === "steer") {
71
70
  const opened = await this.deps.directory().open(input.agentId, signal);
@@ -104,12 +103,25 @@ export class AgentDelegation {
104
103
  const scope = await this.scope(signal);
105
104
  if (!scope.agents.some(({ agentId }) => agentId === input.agentId))
106
105
  throw new Error("Agent is outside this scope");
107
- const requestId = await this.requestKey(scope, input, sourceSessionId);
106
+ const requestId = await agentDelegationRequestKey({
107
+ owner: {
108
+ kind: "hosted",
109
+ scopeId: scope.scopeId,
110
+ agentId: scope.agentId,
111
+ },
112
+ sourceSessionId,
113
+ targetAgentId: input.agentId,
114
+ requestId: input.requestId,
115
+ });
108
116
  signal.throwIfAborted();
109
117
  const opened = await this.deps.directory().open(input.agentId, signal);
110
118
  try {
111
- const session = opened.agent.session(input.sessionId);
112
- const admitted = this.fleet().receipt(requestId) ?? (await session.withdraw(requestId));
119
+ const receipt = this.fleet().receipt(requestId);
120
+ if (receipt?.sessionId !== undefined &&
121
+ receipt.sessionId !== input.sessionId)
122
+ return { status: "unknown" };
123
+ const session = opened.agent.session(receipt?.sessionId ?? input.sessionId);
124
+ const admitted = receipt ?? (await session.withdraw(requestId));
113
125
  return admitted.status === "accepted"
114
126
  ? await session.cancel({
115
127
  commandId: admitted.commandId,
@@ -124,8 +136,8 @@ export class AgentDelegation {
124
136
  /**
125
137
  * A reply-less envelope is only ever BirdDog's reply to work this Agent
126
138
  * sent. Admit it only after the fleet authenticates it against that frozen
127
- * mail; the line is then authored by the replying Agent, never by whoever
128
- * settled or cancelled the work, and its meta carries the verified reply.
139
+ * mail. A recipient-left reply remains Agent-authored; a recipient-offline
140
+ * reply is Hub-authored. Its meta carries only the authenticated reply.
129
141
  */
130
142
  async authenticatedReply(mail) {
131
143
  const delegation = this.fleet().authenticateReply(mail);
@@ -133,48 +145,110 @@ export class AgentDelegation {
133
145
  return undefined;
134
146
  const scope = await this.scope(new AbortController().signal);
135
147
  const replier = scope.agents.find(({ agentId }) => agentId === delegation.reply.agentId);
136
- if (replier === undefined)
148
+ const undeliverable = delegation.reply.outcome === "undeliverable";
149
+ if (replier === undefined && !undeliverable)
137
150
  return undefined;
138
- const meta = delegationReplyMeta(delegation, replier.name);
151
+ const meta = delegationReplyMeta(delegation, replier?.name ?? delegation.reply.agentId);
139
152
  return { mail: { ...mail, meta }, reply: delegation.reply };
140
153
  }
141
- /** Admit one addressed Agent mail for this Agent, or refuse it. */
142
- async receive(raw) {
154
+ parseAddressedMail(raw) {
143
155
  const envelope = Schema.decodeUnknownOption(PostEnvelopeSchema)(raw);
144
156
  if (envelope._tag === "None" || envelope.value.kind !== "agentMail")
145
- return "refused";
157
+ return undefined;
146
158
  const address = Schema.decodeUnknownOption(agentMailAddressSchema)(envelope.value.address);
147
- const parsed = Schema.decodeUnknownOption(BirdDogMailSchema)(envelope.value.event);
148
- if (address._tag === "None" || parsed._tag === "None")
159
+ const mail = Schema.decodeUnknownOption(BirdDogMailSchema)(envelope.value.event);
160
+ if (address._tag === "None" || mail._tag === "None")
161
+ return undefined;
162
+ return {
163
+ envelope: envelope.value,
164
+ address: address.value,
165
+ mail: mail.value,
166
+ };
167
+ }
168
+ /** Admit one addressed Agent mail for this Agent, or refuse it. */
169
+ async receive(raw) {
170
+ const addressed = this.parseAddressedMail(raw);
171
+ if (addressed === undefined)
149
172
  return "refused";
150
- const mail = parsed.value;
173
+ const { envelope, address, mail } = addressed;
174
+ let authenticated;
151
175
  const valid = async () => {
152
176
  const scope = await this.scope(new AbortController().signal);
153
- return (address.value.scopeId === scope.scopeId &&
154
- address.value.agentId === scope.agentId &&
177
+ const sender = mail.from;
178
+ const authenticatedUndeliverable = authenticated?.reply.outcome === "undeliverable";
179
+ return (address.scopeId === scope.scopeId &&
180
+ address.agentId === scope.agentId &&
155
181
  mail.to.agentId === scope.agentId &&
156
- envelope.value.key === mail.key &&
157
- [mail.from, ...mail.replyTo].every((ref) => scope.agents.some(({ agentId }) => agentId === ref.agentId)) &&
182
+ envelope.key === mail.key &&
183
+ (authenticatedUndeliverable ||
184
+ (sender.kind === "agent" &&
185
+ scope.agents.some(({ agentId }) => agentId === sender.agentId))) &&
186
+ mail.replyTo.every(({ agentId }) => scope.agents.some((agent) => agent.agentId === agentId)) &&
158
187
  (mail.replyTo.length === 0 ||
159
188
  (mail.replyTo.length === 1 &&
160
- mail.replyTo[0]?.agentId === mail.from.agentId &&
161
- mail.replyTo[0].sessionId === mail.from.sessionId)));
189
+ sender.kind === "agent" &&
190
+ mail.replyTo[0]?.agentId === sender.agentId &&
191
+ mail.replyTo[0].sessionId === sender.sessionId)));
162
192
  };
193
+ if (mail.replyTo.length === 0)
194
+ authenticated = await this.authenticatedReply(mail);
163
195
  if (!(await valid()))
164
196
  return "refused";
165
- const admitted = mail.replyTo.length > 0
166
- ? { mail, reply: undefined }
167
- : await this.authenticatedReply(mail);
197
+ if (mail.reply?.outcome === "accepted")
198
+ return this.recordAcceptedNotice(mail, valid);
199
+ const admitted = mail.replyTo.length > 0 ? { mail, reply: undefined } : authenticated;
168
200
  if (admitted === undefined)
169
201
  return "refused";
202
+ return this.acceptAdmitted(admitted, address.scopeId, valid);
203
+ }
204
+ async recordAcceptedNotice(mail, valid) {
205
+ if (mail.to.sessionId === undefined)
206
+ return "refused";
207
+ await this.deps.prepare(mail);
208
+ if (!(await valid()))
209
+ return "refused";
210
+ const receipt = this.fleet().recordAcceptedNotice(mail);
211
+ return receipt === undefined ? "refused" : { status: "accepted", receipt };
212
+ }
213
+ async acceptAdmitted(admitted, scopeId, valid) {
170
214
  await this.deps.prepare(admitted.mail);
171
215
  if (!(await valid()))
172
216
  return "refused";
217
+ const stageAcceptedNotice = (notice) => {
218
+ this.deps.stageAcceptedNotice(notice, scopeId);
219
+ };
220
+ const receipt = await this.deps.accept(admitted.mail, admitted.reply, stageAcceptedNotice);
221
+ if (admitted.reply?.outcome === "undeliverable" &&
222
+ admitted.mail.from.kind === "system") {
223
+ this.fleet().recordUndeliverableReply(admitted.mail.key.slice(0, -":reply:0".length));
224
+ }
225
+ const sessionId = admitted.mail.to.sessionId ?? receipt.sessionId;
173
226
  return {
174
227
  status: "accepted",
175
- receipt: await this.deps.accept(admitted.mail, admitted.reply),
228
+ receipt: sessionId === undefined ? receipt : { ...receipt, sessionId },
176
229
  };
177
230
  }
231
+ /** Admit local outbox exhaustion through the same Fleet/system reply gate. */
232
+ async localUndeliverable(original) {
233
+ const identity = await this.deps.identity();
234
+ if (identity === null)
235
+ return "refused";
236
+ let reply;
237
+ try {
238
+ reply = undeliverableReply(original, Date.now());
239
+ }
240
+ catch {
241
+ return "refused";
242
+ }
243
+ if (!this.fleet().recordLocalUndeliverable(original, reply))
244
+ return "refused";
245
+ return await this.receive({
246
+ key: reply.key,
247
+ kind: "agentMail",
248
+ address: { scopeId: identity.scopeId, agentId: identity.agentId },
249
+ event: reply,
250
+ });
251
+ }
178
252
  }
179
253
  /** A current correspondent no longer has disclosure authority. */
180
254
  export class AgentDisclosureRefused extends Error {
@@ -194,6 +268,13 @@ export function agentMailRoute(namespace, replySettlement) {
194
268
  deliver: async (envelope) => {
195
269
  const mail = Schema.decodeUnknownSync(BirdDogMailSchema)(envelope.event);
196
270
  if (mail.reply !== undefined &&
271
+ mail.reply.outcome !== "accepted" &&
272
+ mail.reply.outcome !== "undeliverable" &&
273
+ mail.to.sessionId === undefined)
274
+ return "refused";
275
+ if (mail.reply !== undefined &&
276
+ mail.reply.outcome !== "undeliverable" &&
277
+ mail.reply.outcome !== "accepted" &&
197
278
  (await replySettlement(mail)) === undefined)
198
279
  return "refused";
199
280
  return namespace.getByName(name).deliver(envelope);
@@ -219,18 +300,60 @@ export class AgentMail {
219
300
  event: mail,
220
301
  };
221
302
  }
222
- /** Submit one mail and return the recipient's Accepted receipt. */
303
+ /** Submit one mail and return recipient acceptance or relay-held custody. */
223
304
  async send(mail) {
224
305
  this.deps.post().send(await this.envelope(mail));
225
306
  // Borrow the existing Post lane once, not a new waiter, loop or supervisor.
226
307
  await this.deps.drain();
227
308
  const row = this.deps.post().read(mail.key);
228
309
  if (row?.state === "delivered" && row.receipt !== undefined)
229
- return parseAccepted(JSON.stringify(row.receipt));
310
+ return parseDelegationReceipt(JSON.stringify(row.receipt));
230
311
  if (row?.state === "dead")
231
312
  throw new Error(`Agent delivery ${row.reason}`);
232
313
  throw new Error("Agent did not return a delegated work receipt");
233
314
  }
315
+ /** Reconcile exhausted original outbox mail into one local system reply. */
316
+ async stageUndeliverableReplies() {
317
+ const identity = await this.deps.identity();
318
+ if (identity === null)
319
+ return;
320
+ const post = this.deps.post();
321
+ let cursor;
322
+ do {
323
+ const page = post.deadLetters(64, cursor);
324
+ for (const letter of page.letters) {
325
+ if (letter.reason !== "exhausted" ||
326
+ letter.envelope.kind !== "agentMail")
327
+ continue;
328
+ const envelope = Schema.decodeUnknownOption(PostEnvelopeSchema)(letter.envelope);
329
+ if (envelope._tag === "None" || envelope.value.kind !== "agentMail")
330
+ continue;
331
+ const original = Schema.decodeUnknownOption(BirdDogMailSchema)(envelope.value.event);
332
+ const address = Schema.decodeUnknownOption(agentMailAddressSchema)(envelope.value.address);
333
+ if (original._tag === "None" ||
334
+ address._tag === "None" ||
335
+ original.value.key !== envelope.value.key ||
336
+ original.value.reply !== undefined ||
337
+ original.value.from.kind !== "agent" ||
338
+ original.value.from.agentId !== identity.agentId ||
339
+ address.value.scopeId !== identity.scopeId ||
340
+ address.value.agentId !== original.value.to.agentId)
341
+ continue;
342
+ const reply = undeliverableReply(original.value, Date.now());
343
+ await this.deps.onUndeliverable(original.value, reply);
344
+ }
345
+ cursor = page.nextCursor;
346
+ } while (cursor !== undefined);
347
+ }
348
+ /** Stage the acceptance reply through this Post owner without leaving the transaction. */
349
+ stageAcceptedNotice(mail, scopeId) {
350
+ this.deps.post().send({
351
+ key: mail.key,
352
+ kind: "agentMail",
353
+ address: { scopeId, agentId: mail.to.agentId },
354
+ event: mail,
355
+ });
356
+ }
234
357
  /** BirdDog's mail room: disclosed settlement replies fan out over Post. */
235
358
  mailRoom() {
236
359
  return {
@@ -268,6 +391,16 @@ export class AgentMail {
268
391
  },
269
392
  catch: (cause) => new BirdDogError("Agent reply fan-out is unavailable", { cause }),
270
393
  }),
394
+ postMail: (mail) => Effect.tryPromise({
395
+ try: async () => {
396
+ this.deps.post().send(await this.envelope(mail));
397
+ await this.deps.wake();
398
+ },
399
+ catch: (cause) => new BirdDogError("Agent mail staging is unavailable", { cause }),
400
+ }),
401
+ stageAcceptedNotice: (mail, scopeId) => {
402
+ this.stageAcceptedNotice(mail, scopeId);
403
+ },
271
404
  drainOutbox: () => Effect.void,
272
405
  nextWakeAt: () => Effect.try({
273
406
  try: () => this.deps.post().nextWake() ?? undefined,
@@ -290,9 +423,9 @@ export const machineAgentCodecs = {
290
423
  settlement(encoded) {
291
424
  return Schema.decodeUnknownSync(Schema.fromJsonString(commandSettlementSchema))(encoded);
292
425
  },
293
- /** Throws unless an acknowledgment carries a well-formed Accepted receipt. */
426
+ /** Throws unless an acknowledgment carries a valid acceptance or Held result. */
294
427
  accepted(receipt) {
295
- parseAccepted(JSON.stringify(receipt));
428
+ parseDelegationReceipt(JSON.stringify(receipt));
296
429
  },
297
430
  };
298
431
  /**
package/dist/effect.d.ts CHANGED
@@ -22,7 +22,7 @@ export type ShiitakeEffectHost = Readonly<{
22
22
  readSettlement: (sessionId: string, commandId: string) => Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
23
23
  agent: ShiitakeEffectOwner;
24
24
  /** Durably admit one addressed mail item through the host owner. */
25
- acceptMail: (mail: BirdDogMail, meta?: HostJson, reply?: VerifiedAgentReply) => Effect.Effect<Accepted, ShiitakeError>;
25
+ acceptMail: (mail: BirdDogMail, meta?: HostJson, reply?: VerifiedAgentReply, stageAcceptedNotice?: (mail: BirdDogMail) => void) => Effect.Effect<Accepted, ShiitakeError>;
26
26
  }>;
27
27
  /** Open the Effect-native host composition with addressed mail. */
28
28
  export declare function openHost(options: ShiitakeEffectOptions, mailRoom: BirdDogEffect): Effect.Effect<ShiitakeEffectHost, ShiitakeError>;