@fungi.computer/shiitake 0.4.0 → 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.
package/README.md CHANGED
@@ -20,16 +20,7 @@ To add your own behaviour, start the extension guide at
20
20
  ## Community-funded
21
21
 
22
22
  We don't take VC money. Fungi is built in the open and proudly backed by its
23
- community, including the **$SHIIT** community token.
24
-
25
- $SHIT is not $SHIIT.
26
-
27
- Official contract address (Solana):
28
- `412ECWXN3jGbd1kn8ck7eeupWttkmrFkNm9JqfAzpump`
29
-
30
- This is the only official address. Fungi will never DM you about the token.
31
- Nothing here is financial advice, and the token isn't a share in Fungi. More at
32
- [fungi.computer/funding](https://fungi.computer/funding).
23
+ community.
33
24
 
34
25
  ## Recover an uncertain send
35
26
 
@@ -136,13 +136,14 @@ export declare function waitForAgentSettlement(session: ShiitakeSession, command
136
136
  }>>;
137
137
  /**
138
138
  * A host's refusal of an Agent creating an Agent: the creator asked to give the
139
- * created Agent more than it holds, or the host cannot tell which Agent is
140
- * creating. It reaches the caller as an `agents.create` failure.
139
+ * created Agent more than it holds, the host cannot tell which Agent is
140
+ * creating, or the request id belongs to another creating Agent or Session. It
141
+ * reaches the caller as an `agents.create` failure.
141
142
  */
142
143
  export declare class AgentCreateRefused extends Error {
143
- readonly reason: "exceeds_creator" | "no_creator";
144
+ readonly reason: "exceeds_creator" | "no_creator" | "request_conflict";
144
145
  readonly name = "AgentCreateRefused";
145
- constructor(reason: "exceeds_creator" | "no_creator", message: string);
146
+ constructor(reason: "exceeds_creator" | "no_creator" | "request_conflict", message: string);
146
147
  }
147
148
  /** Compose one authorized Agent-control projection for a single run lease. */
148
149
  export declare function createAgentControlSource(directory: AgentControlDirectory): CapabilitySource;
@@ -258,8 +258,9 @@ export async function waitForAgentSettlement(session, commandId, timeoutMs, sign
258
258
  }
259
259
  /**
260
260
  * A host's refusal of an Agent creating an Agent: the creator asked to give the
261
- * created Agent more than it holds, or the host cannot tell which Agent is
262
- * creating. It reaches the caller as an `agents.create` failure.
261
+ * created Agent more than it holds, the host cannot tell which Agent is
262
+ * creating, or the request id belongs to another creating Agent or Session. It
263
+ * reaches the caller as an `agents.create` failure.
263
264
  */
264
265
  export class AgentCreateRefused extends Error {
265
266
  reason;
@@ -1,7 +1,7 @@
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
6
  import { type Accepted, type DelegationReceipt, type CancelResult } from "./client.js";
7
7
  import type { CommandSettlement, ShiitakeHost, VerifiedAgentReply } from "./index.js";
@@ -41,8 +41,8 @@ export declare class AgentDelegation {
41
41
  /**
42
42
  * A reply-less envelope is only ever BirdDog's reply to work this Agent
43
43
  * sent. Admit it only after the fleet authenticates it against that frozen
44
- * mail; the line is then authored by the replying Agent, never by whoever
45
- * 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.
46
46
  */
47
47
  private authenticatedReply;
48
48
  private parseAddressedMail;
@@ -50,6 +50,8 @@ export declare class AgentDelegation {
50
50
  receive(raw: PostEnvelope): Promise<PostDeliveryResult>;
51
51
  private recordAcceptedNotice;
52
52
  private acceptAdmitted;
53
+ /** Admit local outbox exhaustion through the same Fleet/system reply gate. */
54
+ localUndeliverable(original: BirdDogMail): Promise<PostDeliveryResult>;
53
55
  }
54
56
  /** A current correspondent no longer has disclosure authority. */
55
57
  export declare class AgentDisclosureRefused extends Error {
@@ -82,25 +84,24 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
82
84
  readonly commandId: string;
83
85
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
84
86
  readonly diagnostic?: string;
85
- } | {
86
- readonly outcome: "undeliverable";
87
- readonly reason: "recipient_offline" | "recipient_left";
88
87
  } | {
89
88
  readonly outcome: "accepted";
90
89
  readonly commandId: string;
90
+ } | {
91
+ readonly outcome: "undeliverable";
92
+ readonly reason: "recipient_left";
91
93
  };
92
94
  } | {
93
95
  readonly key: string;
94
96
  readonly reply: {
95
97
  readonly outcome: "undeliverable";
96
- readonly reason: "recipient_offline" | "recipient_left";
98
+ readonly reason: "recipient_offline";
97
99
  };
98
100
  readonly content: string;
99
101
  readonly createdAt: number;
100
102
  readonly from: {
101
- readonly kind: "agent";
102
- readonly agentId: string;
103
- readonly sessionId?: string;
103
+ readonly kind: "system";
104
+ readonly systemId: "hub";
104
105
  };
105
106
  readonly to: {
106
107
  readonly kind: "agent";
@@ -137,25 +138,24 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
137
138
  readonly commandId: string;
138
139
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
139
140
  readonly diagnostic?: string;
140
- } | {
141
- readonly outcome: "undeliverable";
142
- readonly reason: "recipient_offline" | "recipient_left";
143
141
  } | {
144
142
  readonly outcome: "accepted";
145
143
  readonly commandId: string;
144
+ } | {
145
+ readonly outcome: "undeliverable";
146
+ readonly reason: "recipient_left";
146
147
  };
147
148
  } | {
148
149
  readonly key: string;
149
150
  readonly reply: {
150
151
  readonly outcome: "undeliverable";
151
- readonly reason: "recipient_offline" | "recipient_left";
152
+ readonly reason: "recipient_offline";
152
153
  };
153
154
  readonly content: string;
154
155
  readonly createdAt: number;
155
156
  readonly from: {
156
- readonly kind: "agent";
157
- readonly agentId: string;
158
- readonly sessionId?: string;
157
+ readonly kind: "system";
158
+ readonly systemId: "hub";
159
159
  };
160
160
  readonly to: {
161
161
  readonly kind: "agent";
@@ -192,25 +192,24 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
192
192
  readonly commandId: string;
193
193
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
194
194
  readonly diagnostic?: string;
195
- } | {
196
- readonly outcome: "undeliverable";
197
- readonly reason: "recipient_offline" | "recipient_left";
198
195
  } | {
199
196
  readonly outcome: "accepted";
200
197
  readonly commandId: string;
198
+ } | {
199
+ readonly outcome: "undeliverable";
200
+ readonly reason: "recipient_left";
201
201
  };
202
202
  } | {
203
203
  readonly key: string;
204
204
  readonly reply: {
205
205
  readonly outcome: "undeliverable";
206
- readonly reason: "recipient_offline" | "recipient_left";
206
+ readonly reason: "recipient_offline";
207
207
  };
208
208
  readonly content: string;
209
209
  readonly createdAt: number;
210
210
  readonly from: {
211
- readonly kind: "agent";
212
- readonly agentId: string;
213
- readonly sessionId?: string;
211
+ readonly kind: "system";
212
+ readonly systemId: "hub";
214
213
  };
215
214
  readonly to: {
216
215
  readonly kind: "agent";
@@ -247,25 +246,24 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
247
246
  readonly commandId: string;
248
247
  readonly outcome: "completed" | "failed" | "cancelled" | "interrupted" | "outcome_unknown";
249
248
  readonly diagnostic?: string;
250
- } | {
251
- readonly outcome: "undeliverable";
252
- readonly reason: "recipient_offline" | "recipient_left";
253
249
  } | {
254
250
  readonly outcome: "accepted";
255
251
  readonly commandId: string;
252
+ } | {
253
+ readonly outcome: "undeliverable";
254
+ readonly reason: "recipient_left";
256
255
  };
257
256
  } | {
258
257
  readonly key: string;
259
258
  readonly reply: {
260
259
  readonly outcome: "undeliverable";
261
- readonly reason: "recipient_offline" | "recipient_left";
260
+ readonly reason: "recipient_offline";
262
261
  };
263
262
  readonly content: string;
264
263
  readonly createdAt: number;
265
264
  readonly from: {
266
- readonly kind: "agent";
267
- readonly agentId: string;
268
- readonly sessionId?: string;
265
+ readonly kind: "system";
266
+ readonly systemId: "hub";
269
267
  };
270
268
  readonly to: {
271
269
  readonly kind: "agent";
@@ -295,7 +293,7 @@ export declare function agentMailRoute(namespace: PostRoute<never, never>["names
295
293
  /** The host's one Post lane, as Agent mail borrows it. */
296
294
  export type AgentMailPost = Pick<PostRuntime<{
297
295
  agentMail: ReturnType<typeof agentMailRoute>;
298
- }>, "send" | "read" | "nextWake">;
296
+ }>, "send" | "read" | "nextWake" | "deadLetters">;
299
297
  /** Recipient-owned replies and receipt-bearing submissions over the host's one Post lane. */
300
298
  export declare class AgentMail {
301
299
  private readonly deps;
@@ -305,12 +303,16 @@ export declare class AgentMail {
305
303
  identity: () => DelegationIdentity | null | Promise<DelegationIdentity | null>;
306
304
  /** Only a typed disclosure refusal becomes undefined; missing settled facts must fail. */
307
305
  replySettlement: AgentReplySettlement;
306
+ /** Route a terminal reply for this sender through its Fleet admission. */
307
+ onUndeliverable: (original: BirdDogMail, reply: BirdDogMail) => Promise<PostDeliveryResult>;
308
308
  drain: () => Promise<void>;
309
309
  wake: () => Promise<void>;
310
310
  }>);
311
311
  private envelope;
312
312
  /** Submit one mail and return recipient acceptance or relay-held custody. */
313
313
  send(mail: BirdDogMail): Promise<DelegationReceipt>;
314
+ /** Reconcile exhausted original outbox mail into one local system reply. */
315
+ stageUndeliverableReplies(): Promise<void>;
314
316
  /** Stage the acceptance reply through this Post owner without leaving the transaction. */
315
317
  stageAcceptedNotice(mail: BirdDogMail, scopeId: string): void;
316
318
  /** BirdDog's mail room: disclosed settlement replies fan out over Post. */
@@ -9,6 +9,7 @@ import { agentMailAddressSchema } from "@fungi.computer/seam/machine-link-protoc
9
9
  import { BirdDogMailSchema, BirdDogError, settlementReplies, } from "@fungi.computer/bird-dog/effect";
10
10
  import * as Effect from "effect/Effect";
11
11
  import { messageAuthor } from "@fungi.computer/agar/agents";
12
+ import { undeliverableReply } from "@fungi.computer/bird-dog";
12
13
  import { PostEnvelopeSchema, definePostRoute, } from "@fungi.computer/carrier/post";
13
14
  import { parseDelegationReceipt, } from "./client.js";
14
15
  import { decodeClientSessionCreationRequest } from "./internal/client-boundary.js";
@@ -135,8 +136,8 @@ export class AgentDelegation {
135
136
  /**
136
137
  * A reply-less envelope is only ever BirdDog's reply to work this Agent
137
138
  * sent. Admit it only after the fleet authenticates it against that frozen
138
- * mail; the line is then authored by the replying Agent, never by whoever
139
- * 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.
140
141
  */
141
142
  async authenticatedReply(mail) {
142
143
  const delegation = this.fleet().authenticateReply(mail);
@@ -173,19 +174,21 @@ export class AgentDelegation {
173
174
  let authenticated;
174
175
  const valid = async () => {
175
176
  const scope = await this.scope(new AbortController().signal);
176
- const frozenUndeliverable = authenticated?.reply.outcome === "undeliverable" &&
177
- authenticated.reply.agentId === mail.from.agentId;
177
+ const sender = mail.from;
178
+ const authenticatedUndeliverable = authenticated?.reply.outcome === "undeliverable";
178
179
  return (address.scopeId === scope.scopeId &&
179
180
  address.agentId === scope.agentId &&
180
181
  mail.to.agentId === scope.agentId &&
181
182
  envelope.key === mail.key &&
182
- (frozenUndeliverable ||
183
- scope.agents.some(({ agentId }) => agentId === mail.from.agentId)) &&
183
+ (authenticatedUndeliverable ||
184
+ (sender.kind === "agent" &&
185
+ scope.agents.some(({ agentId }) => agentId === sender.agentId))) &&
184
186
  mail.replyTo.every(({ agentId }) => scope.agents.some((agent) => agent.agentId === agentId)) &&
185
187
  (mail.replyTo.length === 0 ||
186
188
  (mail.replyTo.length === 1 &&
187
- mail.replyTo[0]?.agentId === mail.from.agentId &&
188
- 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)));
189
192
  };
190
193
  if (mail.replyTo.length === 0)
191
194
  authenticated = await this.authenticatedReply(mail);
@@ -215,12 +218,37 @@ export class AgentDelegation {
215
218
  this.deps.stageAcceptedNotice(notice, scopeId);
216
219
  };
217
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
+ }
218
225
  const sessionId = admitted.mail.to.sessionId ?? receipt.sessionId;
219
226
  return {
220
227
  status: "accepted",
221
228
  receipt: sessionId === undefined ? receipt : { ...receipt, sessionId },
222
229
  };
223
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
+ }
224
252
  }
225
253
  /** A current correspondent no longer has disclosure authority. */
226
254
  export class AgentDisclosureRefused extends Error {
@@ -284,6 +312,39 @@ export class AgentMail {
284
312
  throw new Error(`Agent delivery ${row.reason}`);
285
313
  throw new Error("Agent did not return a delegated work receipt");
286
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
+ }
287
348
  /** Stage the acceptance reply through this Post owner without leaving the transaction. */
288
349
  stageAcceptedNotice(mail, scopeId) {
289
350
  this.deps.post().send({
package/dist/fleet.d.ts CHANGED
@@ -255,8 +255,9 @@ export declare function delegationSendMeta(sender: Readonly<{
255
255
  name: string;
256
256
  }>, invocationMeta?: Json): DelegationSendMeta;
257
257
  /**
258
- * The host metadata for one authenticated reply line: the replying Agent is
259
- * its author, and the verified reply rides beside the caller's own path.
258
+ * The host metadata for one authenticated reply line. An ordinary reply is
259
+ * authored by its replying Agent; a delivery failure is a Hub/system fact,
260
+ * while the verified reply retains the failed recipient's identity.
260
261
  */
261
262
  export declare function delegationReplyMeta(delegation: AuthenticatedDelegationReply, replierName: string): DelegationReplyMeta;
262
263
  /**
@@ -279,6 +280,8 @@ export declare function createFleetCorrespondence(options: Omit<FleetDelegationO
279
280
  start: (grant: AgentGrant, raw: AgentDelegationTask) => Promise<DelegationReceipt>;
280
281
  /** Match a reply to frozen mail and restore the caller's delegation path. */
281
282
  authenticateReply: (mail: BirdDogMail) => AuthenticatedDelegationReply | undefined;
283
+ recordLocalUndeliverable: (original: BirdDogMail, reply: BirdDogMail) => boolean;
284
+ recordUndeliverableReply: (mailKey: string) => boolean;
282
285
  /** Read an existing acceptance receipt without reserving or delivering mail. */
283
286
  receipt: (requestId: string) => Accepted | undefined;
284
287
  /** Consume a recipient's stable `<mailKey>:accepted` notice. */
@@ -292,6 +295,8 @@ export declare function createFleetDelegation(options: FleetDelegationOptions):
292
295
  start: (grant: AgentGrant, raw: AgentDelegationTask) => Promise<DelegationReceipt>;
293
296
  /** Match a reply to frozen mail and restore the caller's delegation path. */
294
297
  authenticateReply: (mail: BirdDogMail) => AuthenticatedDelegationReply | undefined;
298
+ recordLocalUndeliverable: (original: BirdDogMail, reply: BirdDogMail) => boolean;
299
+ recordUndeliverableReply: (mailKey: string) => boolean;
295
300
  /** Read an existing acceptance receipt without reserving or delivering mail. */
296
301
  receipt: (requestId: string) => Accepted | undefined;
297
302
  /** Consume a recipient's stable `<mailKey>:accepted` notice. */
package/dist/fleet.js CHANGED
@@ -318,16 +318,21 @@ export function delegationSendMeta(sender, invocationMeta) {
318
318
  };
319
319
  }
320
320
  /**
321
- * The host metadata for one authenticated reply line: the replying Agent is
322
- * its author, and the verified reply rides beside the caller's own path.
321
+ * The host metadata for one authenticated reply line. An ordinary reply is
322
+ * authored by its replying Agent; a delivery failure is a Hub/system fact,
323
+ * while the verified reply retains the failed recipient's identity.
323
324
  */
324
325
  export function delegationReplyMeta(delegation, replierName) {
325
- return {
326
- ...authorMeta({
326
+ const author = delegation.reply.outcome === "undeliverable" &&
327
+ delegation.reply.reason === "recipient_offline"
328
+ ? authorMeta({ id: "hub/system", name: "Hub/system" })
329
+ : authorMeta({
327
330
  id: delegation.reply.agentId,
328
331
  name: replierName,
329
332
  kind: "agent",
330
- }),
333
+ });
334
+ return {
335
+ ...author,
331
336
  delegation: {
332
337
  ancestors: [...delegation.ancestors],
333
338
  maxDepth: delegation.maxDepth,
@@ -341,7 +346,7 @@ const delegationMetadata = z.object({
341
346
  const delegationRowSchema = z.lazy(() => z.strictObject({
342
347
  input: z.string(),
343
348
  mail: z.string(),
344
- state: z.enum(["pending", "held", "accepted"]),
349
+ state: z.enum(["pending", "held", "accepted", "undeliverable"]),
345
350
  receipt: z.string().nullable(),
346
351
  }));
347
352
  const heldRecipientOffline = {
@@ -351,7 +356,7 @@ const heldRecipientOffline = {
351
356
  };
352
357
  const decodeMail = Schema.decodeUnknownSync(Schema.fromJsonString(BirdDogMailSchema));
353
358
  function bindRecipientSession(mail, sessionId) {
354
- if (mail.from.sessionId === undefined)
359
+ if (mail.from.kind !== "agent")
355
360
  throw new DirectoryError(503, "partial_init");
356
361
  return {
357
362
  ...mail,
@@ -434,7 +439,7 @@ export function createFleetCorrespondence(options) {
434
439
  throw new DirectoryError(503, "partial_init");
435
440
  if (row.input !== input)
436
441
  throw new DirectoryError(409, "request_conflict");
437
- if (row.state === "held")
442
+ if (row.state === "held" || row.state === "undeliverable")
438
443
  return heldRecipientOffline;
439
444
  if (row.state === "accepted") {
440
445
  if (row.receipt === null)
@@ -472,17 +477,36 @@ export function createFleetCorrespondence(options) {
472
477
  mail.reply.outcome === "accepted" ||
473
478
  !mail.key.endsWith(suffix))
474
479
  return undefined;
475
- const row = read(mail.key.slice(0, -suffix.length));
476
- if (row === undefined)
480
+ const mailKey = mail.key.slice(0, -suffix.length);
481
+ const row = read(mailKey);
482
+ if (row === undefined || row.state === "undeliverable")
477
483
  return undefined;
478
484
  const sent = decodeMail(row.mail);
479
- if (mail.reply.outcome !== "undeliverable" &&
480
- mail.from.sessionId === undefined)
485
+ if (sent.key !== mailKey)
481
486
  return undefined;
482
- if (mail.from.agentId !== sent.to.agentId ||
483
- mail.from.sessionId !== sent.to.sessionId ||
484
- mail.to.agentId !== sent.from.agentId ||
485
- mail.to.sessionId !== sent.from.sessionId)
487
+ const systemUndeliverable = mail.reply.outcome === "undeliverable" &&
488
+ mail.reply.reason === "recipient_offline" &&
489
+ mail.from.kind === "system";
490
+ if (systemUndeliverable) {
491
+ if (row.state !== "held" ||
492
+ sent.from.kind !== "agent" ||
493
+ mail.key !== `${sent.key}:reply:0` ||
494
+ mail.to.agentId !== sent.from.agentId ||
495
+ mail.to.sessionId !== sent.from.sessionId)
496
+ return undefined;
497
+ }
498
+ else {
499
+ if (sent.from.kind !== "agent" ||
500
+ mail.from.kind !== "agent" ||
501
+ mail.from.agentId !== sent.to.agentId ||
502
+ mail.from.sessionId !== sent.to.sessionId ||
503
+ mail.to.agentId !== sent.from.agentId ||
504
+ mail.to.sessionId !== sent.from.sessionId)
505
+ return undefined;
506
+ }
507
+ if (mail.reply.outcome === "undeliverable" &&
508
+ mail.reply.reason === "recipient_offline" &&
509
+ !systemUndeliverable)
486
510
  return undefined;
487
511
  if (mail.reply.outcome !== "undeliverable" &&
488
512
  row.state === "accepted" &&
@@ -496,16 +520,28 @@ export function createFleetCorrespondence(options) {
496
520
  return undefined;
497
521
  let reply;
498
522
  if (mail.reply.outcome === "undeliverable") {
499
- const undeliverable = { agentId: mail.from.agentId, ...mail.reply };
523
+ const agentId = systemUndeliverable
524
+ ? sent.to.agentId
525
+ : mail.from.kind === "agent"
526
+ ? mail.from.agentId
527
+ : undefined;
528
+ if (agentId === undefined)
529
+ return undefined;
530
+ const undeliverable = { agentId, ...mail.reply };
531
+ const sessionId = systemUndeliverable
532
+ ? sent.to.sessionId
533
+ : mail.from.kind === "agent"
534
+ ? mail.from.sessionId
535
+ : undefined;
500
536
  reply =
501
- mail.from.sessionId === undefined
537
+ sessionId === undefined
502
538
  ? undeliverable
503
- : { ...undeliverable, sessionId: mail.from.sessionId };
539
+ : { ...undeliverable, sessionId };
504
540
  }
505
541
  else {
506
- const sessionId = mail.from.sessionId;
507
- if (sessionId === undefined)
542
+ if (mail.from.kind !== "agent")
508
543
  return undefined;
544
+ const sessionId = mail.from.sessionId;
509
545
  reply = { agentId: mail.from.agentId, sessionId, ...mail.reply };
510
546
  }
511
547
  return {
@@ -514,6 +550,29 @@ export function createFleetCorrespondence(options) {
514
550
  reply,
515
551
  };
516
552
  }
553
+ /** Bind an exhausted local Carrier row to its frozen sender correspondence. */
554
+ function recordLocalUndeliverable(original, reply) {
555
+ const row = read(original.key);
556
+ if (row === undefined ||
557
+ (row.state !== "pending" && row.state !== "held") ||
558
+ original.from.kind !== "agent" ||
559
+ original.reply !== undefined ||
560
+ JSON.stringify(decodeMail(row.mail)) !== JSON.stringify(original) ||
561
+ reply.key !== `${original.key}:reply:0` ||
562
+ reply.from.kind !== "system" ||
563
+ reply.reply?.outcome !== "undeliverable" ||
564
+ reply.reply.reason !== "recipient_offline" ||
565
+ reply.to.agentId !== original.from.agentId ||
566
+ reply.to.sessionId !== original.from.sessionId ||
567
+ reply.replyTo.length !== 0)
568
+ return false;
569
+ store.recordHeld(original.key);
570
+ return read(original.key)?.state === "held";
571
+ }
572
+ /** Make a successfully admitted system disposition replay-ineligible. */
573
+ function recordUndeliverableReply(mailKey) {
574
+ return store.recordUndeliverable(mailKey);
575
+ }
517
576
  /** Read only the existing acceptance receipt; do not reserve or deliver mail. */
518
577
  function receipt(requestId) {
519
578
  const row = read(requestId);
@@ -524,9 +583,11 @@ export function createFleetCorrespondence(options) {
524
583
  /** Atomically bind the recipient Session carried by a late acceptance notice. */
525
584
  function recordAcceptedNotice(mail) {
526
585
  const suffix = ":accepted";
586
+ if (mail.from.kind !== "agent")
587
+ return undefined;
588
+ const sender = mail.from;
527
589
  if (mail.replyTo.length !== 0 ||
528
590
  mail.reply?.outcome !== "accepted" ||
529
- mail.from.sessionId === undefined ||
530
591
  !mail.key.endsWith(suffix))
531
592
  return undefined;
532
593
  const requestId = mail.key.slice(0, -suffix.length);
@@ -534,29 +595,31 @@ export function createFleetCorrespondence(options) {
534
595
  if (row === undefined)
535
596
  return undefined;
536
597
  const sent = decodeMail(row.mail);
598
+ if (sent.from.kind !== "agent")
599
+ return undefined;
600
+ const sentSender = sent.from;
537
601
  const path = delegationMetadata.safeParse(sent.meta);
538
- if (sent.from.sessionId === undefined ||
539
- !path.success ||
602
+ if (!path.success ||
540
603
  path.data.delegation === undefined ||
541
604
  mail.key !== `${sent.key}:accepted` ||
542
- mail.from.agentId !== sent.to.agentId ||
605
+ sender.agentId !== sent.to.agentId ||
543
606
  (sent.to.sessionId !== undefined &&
544
- sent.to.sessionId !== mail.from.sessionId) ||
545
- mail.to.agentId !== sent.from.agentId ||
546
- mail.to.sessionId !== sent.from.sessionId)
607
+ sent.to.sessionId !== sender.sessionId) ||
608
+ mail.to.agentId !== sentSender.agentId ||
609
+ mail.to.sessionId !== sentSender.sessionId)
547
610
  return undefined;
548
611
  if (row.state === "accepted" && row.receipt !== null) {
549
612
  const current = parseAccepted(row.receipt);
550
613
  if (current.commandId !== mail.reply.commandId ||
551
- current.sessionId !== mail.from.sessionId)
614
+ current.sessionId !== sender.sessionId)
552
615
  return undefined;
553
616
  }
554
617
  const accepted = {
555
618
  status: "accepted",
556
619
  commandId: mail.reply.commandId,
557
- sessionId: mail.from.sessionId,
620
+ sessionId: sender.sessionId,
558
621
  };
559
- store.recordReceipt(requestId, JSON.stringify(accepted), row.mail, JSON.stringify(bindRecipientSession(sent, mail.from.sessionId)));
622
+ store.recordReceipt(requestId, JSON.stringify(accepted), row.mail, JSON.stringify(bindRecipientSession(sent, sender.sessionId)));
560
623
  return accepted;
561
624
  }
562
625
  return {
@@ -564,6 +627,8 @@ export function createFleetCorrespondence(options) {
564
627
  start,
565
628
  /** Match a reply to frozen mail and restore the caller's delegation path. */
566
629
  authenticateReply,
630
+ recordLocalUndeliverable,
631
+ recordUndeliverableReply,
567
632
  /** Read an existing acceptance receipt without reserving or delivering mail. */
568
633
  receipt,
569
634
  /** Consume a recipient's stable `<mailKey>:accepted` notice. */
@@ -16,9 +16,14 @@ function decodeReply(request) {
16
16
  return undefined;
17
17
  const reply = requireDecoded(decodeVerifiedReply(request.reply), "Verified Agent reply is invalid");
18
18
  const mail = request.mail;
19
+ const agentAuthored = mail?.from.kind === "agent" &&
20
+ mail.from.agentId === reply.agentId &&
21
+ mail.from.sessionId === reply.sessionId;
22
+ const systemUndeliverable = mail?.from.kind === "system" &&
23
+ reply.outcome === "undeliverable" &&
24
+ reply.reason === "recipient_offline";
19
25
  if (mail?.replyTo.length !== 0 ||
20
- mail.from.agentId !== reply.agentId ||
21
- mail.from.sessionId !== reply.sessionId ||
26
+ (!agentAuthored && !systemUndeliverable) ||
22
27
  request.command.type !== "prompt" ||
23
28
  request.command.delivery !== "steer")
24
29
  throw new ShiitakeError("contract", "invalid", "Verified Agent reply does not match its mail");
@@ -34,4 +34,5 @@ export declare function createFleetDelegationStore(storage: WatchdogSqliteOwner)
34
34
  reserve(requestId: string, input: string, mail: string): void;
35
35
  recordReceipt(requestId: string, receipt: string, expectedMail: string, acceptedMail: string): void;
36
36
  recordHeld(requestId: string): void;
37
+ recordUndeliverable(requestId: string): boolean;
37
38
  };
@@ -86,11 +86,16 @@ export function createFleetDelegationStore(storage) {
86
86
  ) STRICT`);
87
87
  storage.sql.exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_held_delegations (
88
88
  request_id TEXT PRIMARY KEY
89
+ ) STRICT`);
90
+ storage.sql
91
+ .exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_undeliverable_delegations (
92
+ request_id TEXT PRIMARY KEY
89
93
  ) STRICT`);
90
94
  return {
91
95
  read: (requestId) => storage.sql
92
96
  .exec(`SELECT delegation.input, delegation.mail,
93
97
  CASE
98
+ WHEN terminal.request_id IS NOT NULL THEN 'undeliverable'
94
99
  WHEN held.request_id IS NOT NULL THEN 'held'
95
100
  WHEN delegation.receipt IS NOT NULL THEN 'accepted'
96
101
  ELSE 'pending'
@@ -99,6 +104,8 @@ export function createFleetDelegationStore(storage) {
99
104
  FROM shiitake_fleet_delegations AS delegation
100
105
  LEFT JOIN shiitake_fleet_held_delegations AS held
101
106
  ON held.request_id = delegation.request_id
107
+ LEFT JOIN shiitake_fleet_undeliverable_delegations AS terminal
108
+ ON terminal.request_id = delegation.request_id
102
109
  WHERE delegation.request_id = ?`, requestId)
103
110
  .toArray()[0],
104
111
  /** Every recipient-accepted delegation, newest first. */
@@ -135,7 +142,27 @@ export function createFleetDelegationStore(storage) {
135
142
  storage.transactionSync(() => {
136
143
  storage.sql.exec(`INSERT OR IGNORE INTO shiitake_fleet_held_delegations (request_id)
137
144
  SELECT request_id FROM shiitake_fleet_delegations
138
- WHERE request_id = ? AND receipt IS NULL`, requestId);
145
+ WHERE request_id = ? AND receipt IS NULL
146
+ AND NOT EXISTS (
147
+ SELECT 1 FROM shiitake_fleet_undeliverable_delegations
148
+ WHERE request_id = ?
149
+ )`, requestId, requestId);
150
+ });
151
+ },
152
+ recordUndeliverable(requestId) {
153
+ return storage.transactionSync(() => {
154
+ const held = storage.sql
155
+ .exec(`SELECT delegation.request_id
156
+ FROM shiitake_fleet_delegations AS delegation
157
+ JOIN shiitake_fleet_held_delegations AS held
158
+ ON held.request_id = delegation.request_id
159
+ WHERE delegation.request_id = ? AND delegation.receipt IS NULL`, requestId)
160
+ .toArray()[0];
161
+ if (held === undefined)
162
+ return false;
163
+ storage.sql.exec("INSERT OR IGNORE INTO shiitake_fleet_undeliverable_delegations(request_id) VALUES (?)", requestId);
164
+ storage.sql.exec("DELETE FROM shiitake_fleet_held_delegations WHERE request_id = ?", requestId);
165
+ return true;
139
166
  });
140
167
  },
141
168
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/shiitake",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Effect-native durable Session acceptance and bounded run orchestration",
6
6
  "type": "module",
@@ -82,10 +82,10 @@
82
82
  "node": ">=22.13"
83
83
  },
84
84
  "dependencies": {
85
- "@fungi.computer/bird-dog": "0.2.0",
85
+ "@fungi.computer/bird-dog": "0.2.1",
86
86
  "@fungi.computer/cairn": "0.1.0",
87
87
  "@fungi.computer/carrier": "0.2.0",
88
- "@fungi.computer/knapsack": "0.3.0",
88
+ "@fungi.computer/knapsack": "0.3.1",
89
89
  "@fungi.computer/mule": "0.2.0",
90
90
  "@fungi.computer/mycelium": "0.2.2",
91
91
  "@fungi.computer/seam": "0.2.0",
@@ -94,7 +94,7 @@
94
94
  "zod": "4.4.3",
95
95
  "@earendil-works/pi-ai": "1.3.0",
96
96
  "@fungi.computer/gills": "0.2.0",
97
- "@fungi.computer/agar": "0.3.1"
97
+ "@fungi.computer/agar": "0.4.0"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@cloudflare/workers-types": "5.20260828.1",