@fungi.computer/shiitake 0.6.0 → 0.7.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 +59 -21
  2. package/dist/agent-control.d.ts +1 -1
  3. package/dist/agent-control.js +40 -21
  4. package/dist/client-visitor.d.ts +2 -0
  5. package/dist/client-visitor.js +6 -1
  6. package/dist/client-wire.d.ts +4 -0
  7. package/dist/client-wire.js +18 -3
  8. package/dist/client.d.ts +24 -4
  9. package/dist/client.js +22 -0
  10. package/dist/cloudflare.d.ts +20 -0
  11. package/dist/cloudflare.js +4 -1
  12. package/dist/delegation.d.ts +5 -0
  13. package/dist/delegation.js +24 -8
  14. package/dist/effect.d.ts +1 -1
  15. package/dist/errors.d.ts +2 -2
  16. package/dist/errors.js +2 -0
  17. package/dist/extensions.d.ts +1 -1
  18. package/dist/fleet.d.ts +10 -2
  19. package/dist/fleet.js +77 -19
  20. package/dist/http-routes.d.ts +12 -9
  21. package/dist/http.d.ts +1 -0
  22. package/dist/http.js +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/internal/admission-door.d.ts +1 -1
  25. package/dist/internal/admission-door.js +8 -4
  26. package/dist/internal/contracts.d.ts +5 -0
  27. package/dist/internal/coordinator.js +107 -50
  28. package/dist/internal/effect-contracts.d.ts +5 -0
  29. package/dist/internal/extension-runtime.js +5 -3
  30. package/dist/internal/promise-facade.js +2 -0
  31. package/dist/internal/run-scope.js +27 -7
  32. package/dist/internal/runtime-coordinator.d.ts +14 -0
  33. package/dist/internal/runtime-coordinator.js +52 -10
  34. package/dist/internal/runtime-core.d.ts +3 -0
  35. package/dist/internal/runtime-core.js +1 -0
  36. package/dist/internal/runtime-reconciliation.js +4 -1
  37. package/dist/internal/session-context.d.ts +5 -5
  38. package/dist/internal/session-context.js +80 -83
  39. package/dist/internal/session-status.d.ts +2 -2
  40. package/dist/internal/session-status.js +5 -3
  41. package/dist/internal/woodstock/client-projection.js +5 -11
  42. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  43. package/dist/internal/woodstock/fleet-store.js +24 -4
  44. package/dist/internal/woodstock/index.d.ts +3 -3
  45. package/dist/internal/woodstock/index.js +16 -6
  46. package/dist/internal/woodstock/progress.d.ts +11 -1
  47. package/dist/internal/woodstock/progress.js +126 -25
  48. package/dist/internal/woodstock/session-delegator.d.ts +2 -1
  49. package/dist/internal/woodstock/session-store.d.ts +14 -2
  50. package/dist/internal/woodstock/session-store.js +329 -66
  51. package/dist/node.js +11 -2
  52. package/dist/server-visitor-stream.js +33 -24
  53. package/docs/api/agent-control.md +3 -2
  54. package/docs/api/client.md +77 -8
  55. package/docs/api/cloudflare.md +16 -0
  56. package/docs/api/delegation.md +84 -50
  57. package/docs/api/effect.md +85 -7
  58. package/docs/api/extensions.md +8 -8
  59. package/docs/api/fleet.md +87 -27
  60. package/docs/api/http.md +28 -0
  61. package/docs/api/index.md +39 -15
  62. package/docs/api/testing.md +9 -7
  63. package/package.json +5 -5
package/dist/fleet.d.ts CHANGED
@@ -82,8 +82,8 @@ export type AgentLifecycleFact = Readonly<{
82
82
  }>;
83
83
  /** Refusal of supplied authority; no host policy lookup is performed. */
84
84
  export declare class AgentGrantError extends Error {
85
- readonly reason: "agent" | "action" | "expired" | "delegation_loop" | "delegation_depth";
86
- constructor(reason: "agent" | "action" | "expired" | "delegation_loop" | "delegation_depth");
85
+ readonly reason: "agent" | "action" | "expired" | "delegation_loop" | "delegation_depth" | "delegator_missing";
86
+ constructor(reason: "agent" | "action" | "expired" | "delegation_loop" | "delegation_depth" | "delegator_missing");
87
87
  }
88
88
  /** Check every operation against its explicit authority. */
89
89
  export declare function assertAgentGrant(grant: AgentGrant, action: AgentAction, agentId?: string): void;
@@ -196,6 +196,7 @@ export declare const agentDelegationTaskSchema: z.ZodLazy<z.ZodObject<{
196
196
  followUp: "followUp";
197
197
  steer: "steer";
198
198
  }>>;
199
+ goal: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<false>]>>;
199
200
  text: z.ZodString;
200
201
  }, z.core.$strict>>;
201
202
  /** A caller reuses this task unchanged while polling Pending. */
@@ -271,6 +272,8 @@ export declare function delegationSendMeta(sender: Readonly<{
271
272
  * while the verified reply retains the failed recipient's identity.
272
273
  */
273
274
  export declare function delegationReplyMeta(delegation: AuthenticatedDelegationReply, replierName: string): DelegationReplyMeta;
275
+ /** An original one-way report can only name an ancestor of its Agent sender. */
276
+ export declare function isOneWayAncestorNotice(mail: BirdDogMail): boolean;
274
277
  /**
275
278
  * Delegated work an owner still waits on; `unreadable` says why its state
276
279
  * could not be read, when it could not.
@@ -281,6 +284,11 @@ export type OutstandingDelegation = Readonly<{
281
284
  commandId: string;
282
285
  unreadable?: string;
283
286
  }>;
287
+ /** Read confirmed sender receipts that have no locally observed settlement.
288
+ * Call inside the Agent owner's transaction that also changes archived state.
289
+ * Remote reads happen separately; an unobserved settlement remains busy.
290
+ */
291
+ export declare function readOutstandingDelegationSync(storage: WatchdogSqliteOwner): OutstandingDelegation | undefined;
284
292
  /**
285
293
  * BirdDog submission and reply authentication. The durable row freezes
286
294
  * correspondence before transport; an ambiguous delivery reuses that exact mail.
package/dist/fleet.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { commandSettlementSchema } from "./client-wire.js";
2
- import { createFleetDirectoryStore, createFleetDelegationStore, } from "./internal/woodstock/index.js";
2
+ import { createFleetDirectoryStore, createFleetDelegationStore, readSessionDelegator, } from "./internal/woodstock/index.js";
3
3
  import { pending } from "@fungi.computer/agar";
4
4
  import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
5
5
  import * as Schema from "effect/Schema";
@@ -280,17 +280,23 @@ export function settlementReadableBy(facts, agentId) {
280
280
  (facts.acceptedSender === null && facts.delegator === agentId));
281
281
  }
282
282
  /** Replay identity and work, with the parent supplied exclusively by the grant. */
283
- export const agentDelegationTaskSchema = z.lazy(() => z.strictObject({
283
+ export const agentDelegationTaskSchema = z.lazy(() => z
284
+ .strictObject({
284
285
  requestId: identity,
285
286
  agentId: identity,
286
287
  sessionId: identity.optional(),
287
288
  /** A live steer into the recipient's running turn; queued follow-up when omitted. */
288
289
  delivery: z.enum(["followUp", "steer"]).optional(),
290
+ /** Only a new-Session follow-up carries goal intent. */
291
+ goal: z
292
+ .union([z.string().min(1).max(4_000), z.literal(false)])
293
+ .optional(),
289
294
  text: z
290
295
  .string()
291
296
  .min(1)
292
297
  .max(256 * 1024),
293
- }));
298
+ })
299
+ .refine(({ goal, sessionId, delivery }) => goal === undefined || (sessionId === undefined && delivery !== "steer"), { path: ["goal"], error: "goal requires a new-Session follow-up" }));
294
300
  /**
295
301
  * Provenance for one delegated send from the invoking Agent's Session. The
296
302
  * model program's digest is frozen with the replay input, so a retried tool
@@ -355,6 +361,19 @@ export function delegationReplyMeta(delegation, replierName) {
355
361
  const delegationMetadata = z.object({
356
362
  delegation: FungiMessageMetaSchema.shape.delegation,
357
363
  });
364
+ /** An original one-way report can only name an ancestor of its Agent sender. */
365
+ export function isOneWayAncestorNotice(mail) {
366
+ if (mail.reply !== undefined ||
367
+ mail.replyTo.length !== 0 ||
368
+ mail.from.kind !== "agent" ||
369
+ mail.to.sessionId === undefined)
370
+ return false;
371
+ const parsed = delegationMetadata.safeParse(mail.meta);
372
+ return (parsed.success &&
373
+ parsed.data.delegation !== undefined &&
374
+ parsed.data.delegation.ancestors.includes(mail.to.agentId) &&
375
+ parsed.data.delegation.ancestors.at(-1) === mail.from.agentId);
376
+ }
358
377
  const delegationRowSchema = z.lazy(() => z.strictObject({
359
378
  input: z.string(),
360
379
  mail: z.string(),
@@ -375,6 +394,25 @@ function bindRecipientSession(mail, sessionId) {
375
394
  to: { ...mail.to, sessionId },
376
395
  };
377
396
  }
397
+ /** Read confirmed sender receipts that have no locally observed settlement.
398
+ * Call inside the Agent owner's transaction that also changes archived state.
399
+ * Remote reads happen separately; an unobserved settlement remains busy.
400
+ */
401
+ export function readOutstandingDelegationSync(storage) {
402
+ const raw = createFleetDelegationStore(storage).unsettled()[0];
403
+ if (raw === undefined)
404
+ return undefined;
405
+ const row = delegationRowSchema.parse(raw);
406
+ const mail = decodeMail(row.mail);
407
+ const sessionId = mail.to.sessionId;
408
+ if (sessionId === undefined)
409
+ throw new DirectoryError(503, "partial_init");
410
+ return {
411
+ agentId: mail.to.agentId,
412
+ sessionId,
413
+ commandId: parseAccepted(z.string().parse(row.receipt)).commandId,
414
+ };
415
+ }
378
416
  /**
379
417
  * BirdDog submission and reply authentication. The durable row freezes
380
418
  * correspondence before transport; an ambiguous delivery reuses that exact mail.
@@ -402,13 +440,18 @@ export function createFleetCorrespondence(options) {
402
440
  : undefined;
403
441
  const ancestors = inherited?.ancestors ?? [];
404
442
  const maxDepth = Math.min(grant.maxDepth ?? 8, inherited?.maxDepth ?? 8);
405
- // Ancestry bounds work depth; it does not remove authorized peer targets.
443
+ // A report to an ancestor is one-way: the parent handles the work but
444
+ // must not send its settlement back as a new prompt to the worker.
406
445
  if (parent.agentId === task.agentId)
407
446
  throw new AgentGrantError("delegation_loop");
408
447
  if (ancestors.length >= maxDepth)
409
448
  throw new AgentGrantError("delegation_depth");
410
449
  const path = { ancestors: [...ancestors, parent.agentId], maxDepth };
411
- const delegation = task.delivery === "steer" ? { ...path, steer: true } : path;
450
+ const delegation = {
451
+ ...path,
452
+ ...(task.delivery === "steer" && { steer: true }),
453
+ ...(task.goal !== undefined && { goal: task.goal }),
454
+ };
412
455
  const outgoingMeta = {
413
456
  ...(objectMeta.success
414
457
  ? objectMeta.data
@@ -427,10 +470,20 @@ export function createFleetCorrespondence(options) {
427
470
  let row = read(task.requestId);
428
471
  if (row === undefined) {
429
472
  const from = { kind: "agent", ...parent };
473
+ const ancestor = ancestors.includes(task.agentId);
474
+ // A sessionless report to a parent goes to the creator's recorded
475
+ // Session. Never create a fresh Session merely to carry a notice.
476
+ const targetSessionId = task.sessionId ??
477
+ (ancestor
478
+ ? readSessionDelegator(options.storage, parent.sessionId)?.replyTo.find(({ agentId }) => agentId === task.agentId)
479
+ ?.sessionId
480
+ : undefined);
481
+ if (ancestor && targetSessionId === undefined)
482
+ throw new AgentGrantError("delegator_missing");
430
483
  let mail = {
431
484
  key: task.requestId,
432
485
  from,
433
- to: task.sessionId === undefined
486
+ to: targetSessionId === undefined
434
487
  ? {
435
488
  kind: "agent",
436
489
  agentId: task.agentId,
@@ -438,9 +491,9 @@ export function createFleetCorrespondence(options) {
438
491
  : {
439
492
  kind: "agent",
440
493
  agentId: task.agentId,
441
- sessionId: task.sessionId,
494
+ sessionId: targetSessionId,
442
495
  },
443
- replyTo: [from],
496
+ replyTo: ancestor ? [] : [from],
444
497
  content: task.text,
445
498
  createdAt: Date.now(),
446
499
  };
@@ -677,6 +730,8 @@ export function createFleetDelegation(options) {
677
730
  if (sessionId === undefined)
678
731
  throw new DirectoryError(503, "partial_init");
679
732
  const settlement = await options.readSettlement(mail.to.agentId, sessionId, accepted.commandId);
733
+ if (settlement !== undefined)
734
+ store.recordSettled(task.requestId);
680
735
  return settlement === undefined
681
736
  ? pending(1000)
682
737
  : {
@@ -691,7 +746,8 @@ export function createFleetDelegation(options) {
691
746
  * target, so the owner is still waiting for a reply; undefined when none.
692
747
  */
693
748
  async function outstanding() {
694
- for (const raw of store.sent()) {
749
+ let waiting;
750
+ for (const raw of store.unsettled()) {
695
751
  const row = delegationRowSchema.parse(raw);
696
752
  if (row.receipt === null)
697
753
  continue;
@@ -706,21 +762,23 @@ export function createFleetDelegation(options) {
706
762
  sessionId: to.sessionId,
707
763
  commandId,
708
764
  };
709
- let settlement;
710
- try {
711
- settlement = await options.readSettlement(to.agentId, to.sessionId, commandId);
712
- }
713
- catch (cause) {
765
+ const result = await options
766
+ .readSettlement(to.agentId, to.sessionId, commandId)
767
+ .then((settlement) => ({ kind: "read", settlement }), (cause) => ({ kind: "unreadable", cause }));
768
+ if (result.kind === "unreadable") {
714
769
  // Work whose state cannot be read is still owed; say why.
715
- return {
770
+ waiting ??= {
716
771
  ...target,
717
- unreadable: cause instanceof Error ? cause.message : "unreadable",
772
+ unreadable: result.cause instanceof Error ? result.cause.message : "unreadable",
718
773
  };
774
+ continue;
719
775
  }
720
- if (settlement === undefined)
721
- return target;
776
+ if (result.settlement === undefined)
777
+ waiting ??= target;
778
+ else
779
+ store.recordSettled(decodeMail(row.mail).key);
722
780
  }
723
- return undefined;
781
+ return waiting;
724
782
  }
725
783
  return {
726
784
  /** The delegated work this owner still waits on, if any. */
@@ -42,7 +42,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
42
42
  };
43
43
  };
44
44
  output: {
45
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
45
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
46
46
  readonly _tag: "ShiitakeError";
47
47
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
48
48
  readonly message: string;
@@ -109,7 +109,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
109
109
  };
110
110
  };
111
111
  output: {
112
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
112
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
113
113
  readonly _tag: "ShiitakeError";
114
114
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
115
115
  readonly message: string;
@@ -150,7 +150,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
150
150
  $get: {
151
151
  input: {};
152
152
  output: {
153
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
153
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
154
154
  readonly _tag: "ShiitakeError";
155
155
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
156
156
  readonly message: string;
@@ -238,7 +238,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
238
238
  };
239
239
  };
240
240
  output: {
241
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
241
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
242
242
  readonly _tag: "ShiitakeError";
243
243
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
244
244
  readonly message: string;
@@ -276,7 +276,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
276
276
  };
277
277
  };
278
278
  output: {
279
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
279
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
280
280
  readonly _tag: "ShiitakeError";
281
281
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
282
282
  readonly message: string;
@@ -352,7 +352,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
352
352
  };
353
353
  };
354
354
  output: {
355
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
355
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
356
356
  readonly _tag: "ShiitakeError";
357
357
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
358
358
  readonly message: string;
@@ -481,7 +481,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
481
481
  };
482
482
  };
483
483
  output: {
484
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
484
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
485
485
  readonly _tag: "ShiitakeError";
486
486
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
487
487
  readonly message: string;
@@ -557,7 +557,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
557
557
  };
558
558
  };
559
559
  output: {
560
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
560
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
561
561
  readonly _tag: "ShiitakeError";
562
562
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
563
563
  readonly message: string;
@@ -821,6 +821,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
821
821
  readonly kind: "session";
822
822
  readonly sessionId: string;
823
823
  readonly commandId: string;
824
+ readonly title: string;
824
825
  } | {
825
826
  readonly kind: "host";
826
827
  readonly lane: string;
@@ -849,6 +850,8 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
849
850
  readonly revision: number;
850
851
  readonly text: string;
851
852
  readonly thinking: string;
853
+ readonly textOffset: number;
854
+ readonly thinkingOffset: number;
852
855
  readonly textTruncated: boolean;
853
856
  readonly thinkingTruncated: boolean;
854
857
  } | null;
@@ -926,7 +929,7 @@ export declare function createShiitakeHttpRoutes(resolveSession: ShiitakeSession
926
929
  };
927
930
  };
928
931
  output: {
929
- readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure";
932
+ readonly cause: "closed" | "conflict" | "incompatible" | "invalid" | "rejected" | "unavailable" | "state_reset_required" | "storage_pressure" | "archived";
930
933
  readonly _tag: "ShiitakeError";
931
934
  readonly boundary: "contract" | "extensions" | "instructions" | "model" | "runtime" | "store" | "tools";
932
935
  readonly message: string;
package/dist/http.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  export { createShiitakeHttpRoutes, type ShiitakeHttpEnv, type ShiitakeSessionResolver, } from "./http-routes.js";
2
+ export { toShiitakeErrorWire } from "./errors.js";
package/dist/http.js CHANGED
@@ -1 +1,2 @@
1
1
  export { createShiitakeHttpRoutes, } from "./http-routes.js";
2
+ export { toShiitakeErrorWire } from "./errors.js";
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ import type { VerifiedAgentReply } from "./internal/woodstock/index.js";
5
5
  import type { BirdDogEffect } from "@fungi.computer/bird-dog/effect";
6
6
  import type { Accepted } from "./client.js";
7
7
  import type { ShiitakeOptions, ShiitakeOwnerAgent } from "./internal/contracts.js";
8
- export type { AbortCommand, AgentExtensionControls, BranchCommand, CompactCommand, CancelCommand, CancelResult, Accepted, DelegationReceipt, Held, ForkCommand, ForkedSession, OperatorCommand, TitleCommand, PromptCommand, PromptDelivery, SendOptions, ReadOptions, SessionCommand, SessionCommandHandler, SessionExtensionControls, SessionExtensionSlots, SessionFrame, SessionMessage, SessionProgress, SessionToolOutput, SessionContext, SessionSnapshot, SessionInboxState, SessionSummary, SessionSummaryPage, SessionSummaryRow, SessionActivity, AgentRunningWork, SessionTerminal, SessionCompactionMarker, SessionCompactionNotice, SessionCompactionTrigger, ShiitakeEventSourceConstructor, SessionWatch, ShiitakeAgent, ShiitakeSession, SubscribeOptions, TooLate, UnknownRequest, WatchOptions, } from "./client.js";
8
+ export type { AbortCommand, AgentExtensionControls, BranchCommand, CompactCommand, CancelCommand, CancelResult, Accepted, DelegationReceipt, Held, ForkCommand, ForkedSession, OperatorCommand, TitleCommand, PromptCommand, PromptDelivery, SendOptions, ReadOptions, SessionCommand, SessionCommandHandler, SessionExtensionControls, SessionExtensionSlots, SessionFrame, SessionMessage, SessionProgress, SessionProgressDelta, SessionToolOutput, SessionContext, SessionSnapshot, SessionInboxState, SessionSummary, SessionSummaryPage, SessionSummaryRow, SessionActivity, AgentRunningWork, SessionTerminal, SessionCompactionMarker, SessionCompactionNotice, SessionCompactionTrigger, ShiitakeEventSourceConstructor, SessionWatch, ShiitakeAgent, ShiitakeSession, SubscribeOptions, TooLate, UnknownRequest, WatchOptions, } from "./client.js";
9
9
  export type { ColdSessionPage, ColdSessionPageRequest, ColdSessionRow, } from "./cold-sessions.js";
10
10
  export { parseForkedSession, parseSessionFrame, parseSessionSnapshot, } from "./client.js";
11
11
  export type { InstructionSnapshot, InstructionSource, ModelAcquisition, ModelDescription, ModelSource, RuntimeHost, RuntimePendingWork, SqliteCursor, SqliteOwner, SqliteValue, SqliteRow, StoragePressureCheck, CompactionBudget, CompactionOptions, GrantsDelivery, GrantsDeliveryResult, ShiitakeOptions, ShiitakeOwnerAgent, ToolAccess, HostMetaOptions, StandardHostMetaResult, StandardHostMetaSchema, ToolSource, } from "./internal/contracts.js";
@@ -32,7 +32,7 @@ export type AdmitRequest = Readonly<{
32
32
  * host passes it, after checking the reply against the mail it sent.
33
33
  */
34
34
  reply?: VerifiedAgentReply;
35
- /** Synchronous owner-local side effect committed with a mail admission. */
35
+ /** Synchronous owner-local side effect on first admission, in its transaction. */
36
36
  onAccepted?: (sessionId: string, commandId: string) => void;
37
37
  /**
38
38
  * New work that continues earlier work in this Session (for example, the
@@ -5,7 +5,7 @@ import { checkHostMeta } from "./host-meta.js";
5
5
  import { ShiitakeError } from "../errors.js";
6
6
  import { decodeClientAcceptedCommand, decodeClientSessionId, decodeClientSessionCreationRequestId, requireDecoded, } from "./client-boundary.js";
7
7
  import { publishOwnerObservation } from "./observation-kernel.js";
8
- import { AdmissionOriginSchema, acceptSessionCommandOwned, readSessionCommandAdmissionByRequest, withdrawSessionRequestOwned, assertSessionRequestNotWithdrawn, hasStoredSession, readWorkSender, recordWorkSender, recordSessionDelegator, VerifiedAgentReplySchema, } from "./woodstock/index.js";
8
+ import { AdmissionOriginSchema, acceptSessionCommandOwned, readSessionCommandAdmissionByRequest, withdrawSessionRequestOwned, assertSessionRequestNotWithdrawn, hasStoredSession, hasRecordedRunTerminal, readWorkSender, recordWorkSender, VerifiedAgentReplySchema, } from "./woodstock/index.js";
9
9
  const decodeOrigin = Schema.decodeUnknownResult(AdmissionOriginSchema, {
10
10
  onExcessProperty: "error",
11
11
  });
@@ -87,7 +87,8 @@ export function openAdmissionDoor(input) {
87
87
  const targetWorkId = command.type === "prompt" &&
88
88
  command.delivery === "steer" &&
89
89
  head.state === "running" &&
90
- !head.cancellationRequested
90
+ !head.cancellationRequested &&
91
+ !hasRecordedRunTerminal(input.owner, sessionId, head.jobId)
91
92
  ? head.jobId
92
93
  : undefined;
93
94
  const accepted = acceptSessionCommandOwned(input.owner, sessionId, command, request.meta, origin, input.contract(), targetWorkId === undefined
@@ -100,8 +101,11 @@ export function openAdmissionDoor(input) {
100
101
  if (sender !== undefined)
101
102
  recordWorkSender(input.owner, sessionId, accepted.accepted.commandId, sender);
102
103
  }
103
- recordSessionDelegator(input.owner, sessionId, request.mail);
104
- request.onAccepted?.(sessionId, accepted.accepted.commandId);
104
+ // The mail notice is staged in the same transaction as the first command.
105
+ // Replaying that admission must not stage a new notice with a different
106
+ // createdAt under the original Post key.
107
+ if (accepted.delivery === "new")
108
+ request.onAccepted?.(sessionId, accepted.accepted.commandId);
105
109
  touched.set(sessionId, (touched.get(sessionId) ?? false) || accepted.delivery === "new");
106
110
  return Object.freeze({
107
111
  status: "accepted",
@@ -188,6 +188,11 @@ export type ShiitakeOptions = Readonly<{
188
188
  store: SqliteOwner;
189
189
  /** Host storage-pressure check; absent never refuses for storage pressure. */
190
190
  storagePressure?: StoragePressureCheck;
191
+ /** Host-owned, synchronous admission for new Sessions and prompts only. */
192
+ admission?: Readonly<{
193
+ /** Check the host's current flag synchronously inside admission's transaction. */
194
+ assertOpen: () => void;
195
+ }>;
191
196
  /** Compaction budgets; absent uses Pi's defaults for every model. */
192
197
  compaction?: CompactionOptions;
193
198
  /** Runtime host that schedules the opaque durable reconciler. */
@@ -7,11 +7,11 @@ import * as Option from "effect/Option";
7
7
  import * as Stream from "effect/Stream";
8
8
  import { ShiitakeError } from "../errors.js";
9
9
  import { decodeClientReadOptions, decodeClientSettlementRequest, requireDecoded, decodeClientSessionCommand, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
10
- import { mergeObservationSignals, openObservationKernel, publishOwnerObservation, } from "./observation-kernel.js";
10
+ import { mergeObservationSignals, openObservationKernel, publishAgentExecutionChange, publishOwnerObservation, } from "./observation-kernel.js";
11
11
  import { checkHostMeta } from "./host-meta.js";
12
- import { readSessionActivity } from "./session-status.js";
13
- import { readSessionContext } from "./session-context.js";
14
- import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readCommandSettlement, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
12
+ import { readSessionActivity, } from "./session-status.js";
13
+ import { prepareSessionContext } from "./session-context.js";
14
+ import { progressPosition, readProcessProgressDelta, abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readCommandSettlement, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
15
15
  /** Generic Agent-scope extension controls over the runtime's one call door. */
16
16
  function agentExtensions(runtime) {
17
17
  return Object.freeze({
@@ -20,12 +20,15 @@ function agentExtensions(runtime) {
20
20
  call: (extensionId, command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "agent" }, extensionId, command, input))),
21
21
  });
22
22
  }
23
- function currentSessionActivity(options, runtime, sessionId) {
23
+ function currentSessionWork(runtime, sessionId) {
24
24
  return runtime
25
25
  .readQueueHead(sessionId)
26
26
  .pipe(Effect.flatMap((head) => (head.state === "queued"
27
27
  ? runtime.runningWork(head.jobId)
28
- : Effect.succeed(null)).pipe(Effect.map((runningWork) => readSessionActivity(options.store, sessionId, head, runningWork)))));
28
+ : Effect.succeed(null)).pipe(Effect.map((runningWork) => ({ head, runningWork })))));
29
+ }
30
+ function currentSessionActivity(options, runtime, sessionId) {
31
+ return currentSessionWork(runtime, sessionId).pipe(Effect.map(({ head, runningWork }) => readSessionActivity(options.store, sessionId, head, runningWork)));
29
32
  }
30
33
  /** Carry the Session's context use on a baseline, when it has one. */
31
34
  function withContext(snapshot, context) {
@@ -35,7 +38,10 @@ function withContext(snapshot, context) {
35
38
  }
36
39
  /** Read the baseline and its context use for one Session. */
37
40
  function readSessionWithContext(options, runtime, sessionId, input) {
38
- return currentSessionActivity(options, runtime, sessionId).pipe(Effect.flatMap((activity) => readSession(options.store, sessionId, activity, input, () => runtime.extensions.project(sessionId))), Effect.flatMap((snapshot) => readSessionContext(options, sessionId, snapshot.cursor, input?.before === undefined).pipe(Effect.map((context) => withContext(snapshot, context)))));
41
+ return currentSessionWork(runtime, sessionId).pipe(Effect.flatMap(({ head, runningWork }) => readSession(options.store, sessionId, () => readSessionActivity(options.store, sessionId, head, runningWork), input, () => runtime.extensions.project(sessionId), (snapshot) => ({
42
+ snapshot,
43
+ context: prepareSessionContext(options, sessionId, snapshot.cursor, input?.before === undefined),
44
+ }))), Effect.flatMap(({ snapshot, context }) => context.pipe(Effect.map((value) => withContext(snapshot, value)))));
39
45
  }
40
46
  function sessionOwnerOperation(runtime, sessionId, operation) {
41
47
  return runtime.assertOpen.pipe(Effect.andThen(Effect.try({
@@ -92,7 +98,46 @@ function continueFromReset(frame) {
92
98
  }),
93
99
  ];
94
100
  }
95
- function subscriptionFrames(options, runtime, registration, sessionId, after, delivery, includeSandboxFailures = false) {
101
+ function subscriptionFrames(options, runtime, registration, sessionId, after, delivery, includeSandboxFailures = false, baselineProgress = null) {
102
+ let position = progressPosition(baselineProgress);
103
+ let toolOutput = [];
104
+ const reset = () => resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map((frame) => {
105
+ position = progressPosition(frame.baseline.progress);
106
+ toolOutput = frame.baseline.toolOutput;
107
+ return continueFromReset(frame);
108
+ }));
109
+ const liveFrames = (activity) => {
110
+ const queue = activity.facts.queue;
111
+ const delta = queue.state !== "running"
112
+ ? null
113
+ : readProcessProgressDelta(options.store, sessionId, queue.commandId, position);
114
+ if (delta === undefined)
115
+ return undefined;
116
+ const frames = [];
117
+ if (delta !== null) {
118
+ frames.push({
119
+ kind: "progress",
120
+ progress: delta,
121
+ activity: activity.facts,
122
+ });
123
+ position = {
124
+ messageId: delta.messageId,
125
+ revision: delta.revision,
126
+ text: delta.text.offset + delta.text.append.length,
127
+ thinking: delta.thinking.offset + delta.thinking.append.length,
128
+ };
129
+ }
130
+ if (toolOutput.length !== activity.toolOutput.length ||
131
+ toolOutput.some((slot, index) => slot !== activity.toolOutput[index])) {
132
+ toolOutput = activity.toolOutput;
133
+ frames.push({
134
+ kind: "tool_output",
135
+ toolOutput,
136
+ activity: activity.facts,
137
+ });
138
+ }
139
+ return frames;
140
+ };
96
141
  const initial = {
97
142
  after,
98
143
  delivery,
@@ -106,64 +151,73 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
106
151
  if (signal === "closed")
107
152
  return Effect.succeed([[], Option.none()]);
108
153
  if (signal === "reset")
109
- return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
154
+ return reset();
110
155
  return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
111
156
  registration.setQueued(activity.facts.queue.state === "queued");
112
- if (signal === "activity" && state.delivery === "live")
113
- return Effect.succeed([
114
- [{ kind: "activity", activity: activity.facts }],
115
- Option.some({ ...state, awaitSignal: true }),
116
- ]);
117
- if (signal === "progress" && state.delivery === "live") {
157
+ if ((signal === "progress" || signal === "activity") &&
158
+ state.delivery === "live") {
159
+ const frames = liveFrames(activity);
160
+ if (frames === undefined)
161
+ return reset();
118
162
  return Effect.succeed([
119
163
  [
120
- ...(activity.progress === null
121
- ? []
122
- : [
164
+ ...(signal === "activity"
165
+ ? [
123
166
  {
124
- kind: "progress",
125
- progress: activity.progress,
167
+ kind: "activity",
126
168
  activity: activity.facts,
127
169
  },
128
- ]),
129
- {
130
- kind: "tool_output",
131
- toolOutput: activity.toolOutput,
132
- activity: activity.facts,
133
- },
170
+ ]
171
+ : []),
172
+ ...frames,
134
173
  ],
135
174
  Option.some({ ...state, awaitSignal: true }),
136
175
  ]);
137
176
  }
138
- return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures).pipe(Effect.flatMap((page) => {
177
+ return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures, (page) => {
178
+ const latestCursor = page.kind === "page"
179
+ ? page.frames
180
+ .flatMap((frame) => frame.kind === "progress" ||
181
+ frame.kind === "activity" ||
182
+ frame.kind === "tool_output"
183
+ ? []
184
+ : [frame.cursor])
185
+ .at(-1)
186
+ : undefined;
187
+ // Prepare a figure only for the final moving update,
188
+ // while its cursor and stored context share the read.
189
+ const context = page.kind === "page" &&
190
+ !page.hasMore &&
191
+ page.frames.some((frame) => frame.kind === "update" &&
192
+ frame.cursor !== state.after)
193
+ ? prepareSessionContext(options, sessionId, latestCursor ?? null)
194
+ : Effect.succeed(undefined);
195
+ return { page, latestCursor, context };
196
+ }).pipe(Effect.flatMap(({ page, latestCursor, context, }) => {
139
197
  if (page.kind === "reset")
140
- return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
141
- const latestCursor = page.frames
142
- .flatMap((frame) => frame.kind === "progress" ||
143
- frame.kind === "activity" ||
144
- frame.kind === "tool_output"
198
+ return reset();
199
+ if (!page.hasMore)
200
+ toolOutput = activity.toolOutput;
201
+ const progressFrames = page.hasMore
145
202
  ? []
146
- : [frame.cursor])
147
- .at(-1);
203
+ : liveFrames(activity);
204
+ if (progressFrames === undefined)
205
+ return reset();
148
206
  const next = {
149
207
  after: latestCursor ?? state.after,
150
208
  delivery: page.hasMore ? state.delivery : "live",
151
209
  awaitSignal: !page.hasMore,
152
210
  };
153
- // Only a final update that moves the cursor gets a
154
- // figure. A wake that moved nothing never re-reads the
155
- // whole kept window.
156
- const movedHistory = page.frames.some((frame) => frame.kind === "update" &&
157
- frame.cursor !== state.after);
158
- return (movedHistory && !page.hasMore
159
- ? readSessionContext(options, sessionId, latestCursor ?? null)
160
- : Effect.succeed(undefined)).pipe(Effect.map((context) => [
161
- context === undefined
162
- ? page.frames
163
- : page.frames.map((frame) => frame.kind === "update" &&
164
- frame.cursor === latestCursor
165
- ? Object.freeze({ ...frame, context })
166
- : frame),
211
+ return context.pipe(Effect.map((context) => [
212
+ [
213
+ ...(context === undefined
214
+ ? page.frames
215
+ : page.frames.map((frame) => frame.kind === "update" &&
216
+ frame.cursor === latestCursor
217
+ ? Object.freeze({ ...frame, context })
218
+ : frame)),
219
+ ...progressFrames,
220
+ ],
167
221
  Option.some(next),
168
222
  ]));
169
223
  }));
@@ -191,7 +245,7 @@ function watchSession(options, runtime, observation, sessionId, input) {
191
245
  registration.setQueued(snapshot.activity.queue.state === "queued");
192
246
  return Object.freeze({
193
247
  snapshot,
194
- changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true).pipe(Stream.ensuring(Effect.sync(registration.close))),
248
+ changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true, snapshot.progress).pipe(Stream.ensuring(Effect.sync(registration.close))),
195
249
  close: Effect.sync(registration.close),
196
250
  });
197
251
  }))).pipe(Effect.onError(() => Effect.sync(registration.close)));
@@ -289,7 +343,9 @@ function executeSessionCommand(options, runtime, sessionId, meta, input) {
289
343
  .commit(sessionOwnerOperation(runtime, decodedSessionId, (decoded) => {
290
344
  setSessionTitle(options.store, decoded, command.title);
291
345
  }))
292
- .pipe(Effect.as(undefined));
346
+ .pipe(Effect.tap(() => Effect.sync(() => {
347
+ publishAgentExecutionChange(options.store);
348
+ })), Effect.as(undefined));
293
349
  }
294
350
  }));
295
351
  }
@@ -455,6 +511,7 @@ export function acceptAddressedMail(runtime, mail, meta, reply, stageMail, sessi
455
511
  const result = admit(meta === undefined ? request : { ...request, meta });
456
512
  if (result.status !== "accepted")
457
513
  throw new Error("Sessionless addressed mail was not admitted");
514
+ runtime.recordCreatedSessionDelegatorWithinTransaction(sessionId, addressed);
458
515
  return {
459
516
  status: "accepted",
460
517
  commandId: result.commandId,