@fungi.computer/shiitake 0.1.0 → 0.3.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 (112) hide show
  1. package/README.md +155 -34
  2. package/dist/agent-control.d.ts +78 -11
  3. package/dist/agent-control.js +273 -70
  4. package/dist/client-entry.d.ts +15 -8
  5. package/dist/client-entry.js +7 -5
  6. package/dist/client-wire.d.ts +24 -8
  7. package/dist/client-wire.js +53 -11
  8. package/dist/client.d.ts +56 -11
  9. package/dist/client.js +1 -1
  10. package/dist/cloudflare.d.ts +42 -23
  11. package/dist/cloudflare.js +76 -54
  12. package/dist/cold-sessions.d.ts +28 -0
  13. package/dist/cold-sessions.js +1 -0
  14. package/dist/delegation.d.ts +214 -0
  15. package/dist/delegation.js +319 -0
  16. package/dist/effect.d.ts +1 -1
  17. package/dist/errors.d.ts +2 -2
  18. package/dist/errors.js +2 -2
  19. package/dist/extension-contract.d.ts +10 -2
  20. package/dist/extensions.d.ts +6 -2
  21. package/dist/fleet.d.ts +52 -34
  22. package/dist/fleet.js +101 -63
  23. package/dist/hosted.js +1 -0
  24. package/dist/http-routes.d.ts +106 -22
  25. package/dist/http-routes.js +18 -1
  26. package/dist/index.d.ts +3 -2
  27. package/dist/internal/accepted-run-executor.d.ts +5 -0
  28. package/dist/internal/accepted-run-executor.js +10 -4
  29. package/dist/internal/admission-door.d.ts +3 -0
  30. package/dist/internal/admission-door.js +12 -2
  31. package/dist/internal/client-boundary.js +7 -1
  32. package/dist/internal/compaction/compaction-slot.d.ts +36 -0
  33. package/dist/internal/compaction/compaction-slot.js +41 -0
  34. package/dist/internal/compaction/compaction.d.ts +7 -4
  35. package/dist/internal/compaction/compaction.js +59 -30
  36. package/dist/internal/compaction/session-compaction.d.ts +8 -16
  37. package/dist/internal/compaction/session-compaction.js +358 -119
  38. package/dist/internal/compaction/settings.js +5 -0
  39. package/dist/internal/compaction/utils.d.ts +19 -16
  40. package/dist/internal/compaction/utils.js +30 -84
  41. package/dist/internal/contracts.d.ts +40 -13
  42. package/dist/internal/coordinator.js +28 -10
  43. package/dist/internal/effect-client.d.ts +4 -2
  44. package/dist/internal/effect-client.js +29 -0
  45. package/dist/internal/effect-contracts.d.ts +17 -4
  46. package/dist/internal/extension-runtime.d.ts +5 -0
  47. package/dist/internal/extension-runtime.js +184 -89
  48. package/dist/internal/host-meta.d.ts +7 -0
  49. package/dist/internal/model-acquisition.js +7 -1
  50. package/dist/internal/model-preference.d.ts +2 -0
  51. package/dist/internal/model-preference.js +2 -1
  52. package/dist/internal/promise-facade.js +4 -16
  53. package/dist/internal/run-failure-reason.d.ts +10 -0
  54. package/dist/internal/run-failure-reason.js +19 -0
  55. package/dist/internal/run-scope.d.ts +4 -1
  56. package/dist/internal/run-scope.js +38 -5
  57. package/dist/internal/runtime-coordinator.d.ts +2 -1
  58. package/dist/internal/runtime-coordinator.js +65 -4
  59. package/dist/internal/runtime-core.js +1 -0
  60. package/dist/internal/runtime-reconciliation.js +42 -6
  61. package/dist/internal/session-context.d.ts +1 -2
  62. package/dist/internal/session-context.js +8 -30
  63. package/dist/internal/session-status.d.ts +2 -1
  64. package/dist/internal/session-status.js +4 -1
  65. package/dist/internal/woodstock/client-projection.d.ts +6 -5
  66. package/dist/internal/woodstock/client-projection.js +79 -53
  67. package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
  68. package/dist/internal/woodstock/cold-sessions.js +90 -0
  69. package/dist/internal/woodstock/extension-store.d.ts +2 -0
  70. package/dist/internal/woodstock/extension-store.js +4 -0
  71. package/dist/internal/woodstock/fleet-store.d.ts +3 -3
  72. package/dist/internal/woodstock/fleet-store.js +16 -11
  73. package/dist/internal/woodstock/index.d.ts +11 -3
  74. package/dist/internal/woodstock/index.js +33 -14
  75. package/dist/internal/woodstock/progress.d.ts +14 -1
  76. package/dist/internal/woodstock/progress.js +97 -19
  77. package/dist/internal/woodstock/run-recovery.js +6 -4
  78. package/dist/internal/woodstock/session-attention.d.ts +1 -1
  79. package/dist/internal/woodstock/session-attention.js +2 -2
  80. package/dist/internal/woodstock/session-entry.d.ts +109 -42
  81. package/dist/internal/woodstock/session-entry.js +63 -20
  82. package/dist/internal/woodstock/session-inbox.js +2 -0
  83. package/dist/internal/woodstock/session-store.d.ts +104 -24
  84. package/dist/internal/woodstock/session-store.js +937 -246
  85. package/dist/internal/woodstock/storage-admission.d.ts +8 -8
  86. package/dist/internal/woodstock/storage-admission.js +8 -34
  87. package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
  88. package/dist/internal/woodstock/storage-conformance.js +489 -0
  89. package/dist/model-preference.d.ts +2 -2
  90. package/dist/model-preference.js +2 -2
  91. package/dist/node.js +1 -1
  92. package/dist/remote-client.js +8 -1
  93. package/dist/session-attention.d.ts +8 -1
  94. package/dist/session-attention.js +6 -1
  95. package/dist/testing.js +1 -0
  96. package/docs/api/README.md +18 -16
  97. package/docs/api/agent-control.md +172 -2
  98. package/docs/api/client-entry.md +26 -10
  99. package/docs/api/client.md +153 -37
  100. package/docs/api/cloudflare.md +76 -23
  101. package/docs/api/delegation.md +485 -0
  102. package/docs/api/effect.md +20 -2
  103. package/docs/api/extensions.md +21 -9
  104. package/docs/api/fleet.md +149 -122
  105. package/docs/api/http.md +4 -1
  106. package/docs/api/index.md +116 -20
  107. package/docs/api/internal/woodstock/storage-conformance.md +81 -0
  108. package/docs/api/model-preference.md +17 -0
  109. package/docs/api/testing.md +16 -4
  110. package/package.json +26 -14
  111. package/dist/internal/browser-sessions.d.ts +0 -31
  112. package/dist/internal/browser-sessions.js +0 -173
@@ -12,7 +12,7 @@ import { ShiitakeError } from "../errors.js";
12
12
  import { AdmissionError, ExtensionCommandError, ExtensionRead, ExtensionStateError, ExtensionStop, ExtensionTx, } from "../extensions.js";
13
13
  import { decodeClientSessionId } from "./client-boundary.js";
14
14
  import { AsyncExtensionTransactionError, invokeExtensionHook, invokeSyncHandler, } from "./extension-hook.js";
15
- import { setSessionAttention, clearSessionAttention, projectSessionAttention, readAttentionSessionPage, replaceAttentionGrants, hasStoredSession, hasPendingSessionSteer, hasSuccessfulToolResult, advanceCursor, declareSessionDocument, readSessionDocument, writeSessionDocument, deleteAllRecords, deleteRecord, deleteTimer, readAllRecords, readCursor, readDueTimers, readRecord, readRecordPage, readStateLedger, readStatus, readTimer, readTimerWake, writeRecord, writeStateLedger, writeStatus, writeTimer, } from "./woodstock/index.js";
15
+ import { setSessionAttention, clearSessionAttention, projectSessionAttention, readAttentionSessionPage, replaceAttentionGrants, hasStoredSession, readSessionLineage, hasPendingSessionSteer, hasSuccessfulToolResult, advanceCursor, declareSessionDocument, readSessionDocument, writeSessionDocument, deleteAllRecords, deleteRecord, deleteTimer, deleteExtensionTimers, readAllRecords, readCursor, readDueTimers, readRecord, readRecordPage, readStateLedger, readStatus, readTimer, readTimerWake, writeRecord, writeStateLedger, writeStatus, writeTimer, } from "./woodstock/index.js";
16
16
  import { JsonValueSchema } from "./json-value.js";
17
17
  import { publishOwnerObservation } from "./observation-kernel.js";
18
18
  /** The settled consumer name core itself uses for mail and control repair. */
@@ -26,7 +26,7 @@ const decodePromptSections = Schema.decodeUnknownResult(Schema.Array(Schema.Stru
26
26
  name: Schema.String.pipe(Schema.check(Schema.isPattern(/^[a-z][A-Za-z0-9_-]{0,63}$/u))),
27
27
  text: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.makeFilter((text) => encoder.encode(text).byteLength <= 8192))),
28
28
  })).pipe(Schema.check(Schema.isMaxLength(16))));
29
- /** A missing or failed pure migration cannot safely decode the new state. */
29
+ /** A declared migration failed or returned records the new state cannot decode. */
30
30
  class IncompatibleStateMigrationError extends Error {
31
31
  }
32
32
  function boundReason(reason) {
@@ -277,9 +277,12 @@ export function openExtensionRuntime(input) {
277
277
  projectSessionAttention(owner, sessionId, input.readQueueHead(sessionId));
278
278
  frame.observed.add(sessionId);
279
279
  },
280
- catch: (error) => error instanceof ShiitakeError && error.cause === "invalid"
281
- ? stateError("bound", error.message)
282
- : stateError("unavailable", "Attention update is unavailable"),
280
+ catch: (error) => error instanceof ExtensionStateError &&
281
+ error.reason === "session_absent"
282
+ ? error
283
+ : error instanceof ShiitakeError && error.cause === "invalid"
284
+ ? stateError("bound", error.message)
285
+ : stateError("unavailable", "Attention update is unavailable"),
283
286
  })));
284
287
  return {
285
288
  set: (sessionId, item) => {
@@ -290,7 +293,7 @@ export function openExtensionRuntime(input) {
290
293
  ? Effect.fail(stateError("bound", "Attention item is invalid"))
291
294
  : mutate(sessionId, () => {
292
295
  if (!hasStoredSession(owner, sessionId))
293
- throw new Error("Attention Session is unavailable");
296
+ throw stateError("session_absent", "Attention Session is absent");
294
297
  setSessionAttention(owner, sessionId, id, decoded.success);
295
298
  });
296
299
  },
@@ -564,6 +567,13 @@ export function openExtensionRuntime(input) {
564
567
  send: sendFor(member),
565
568
  timers: timersFor(member),
566
569
  attention: attentionFor(member),
570
+ sessionPosition: (sessionId) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "state")), Effect.andThen(storeStep(() => {
571
+ const id = decodeClientSessionId(sessionId);
572
+ if (!hasStoredSession(owner, id))
573
+ throw stateError("unavailable", "Session timeline is unavailable");
574
+ return (readSessionLineage(owner, { sessionId: id })
575
+ ?.newestSequence ?? 0);
576
+ }))),
567
577
  hasPendingSteer: (run) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasPendingSessionSteer(owner, decodeClientSessionId(run.sessionId), run.workId)))),
568
578
  hasSuccessfulToolResult: (run, toolCallId) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasSuccessfulToolResult(owner, decodeClientSessionId(run.sessionId), run.workId, toolCallId)))),
569
579
  observe: (sessionId) => requireFrame(member.admitted.id).pipe(Effect.map((frame) => {
@@ -587,54 +597,60 @@ export function openExtensionRuntime(input) {
587
597
  }),
588
598
  });
589
599
  }
590
- /** Pure per-version migrations, applied and re-decoded in one transaction. */
600
+ /** Pure per-version migrations; prepare owns their transaction with declarations. */
591
601
  function migrate(member) {
592
602
  const { id, definition, revision } = member.admitted;
593
603
  const declared = definition.state.version;
594
- owner.transactionSync(() => {
595
- const ledger = readStateLedger(owner, id);
596
- if (ledger !== undefined && ledger.version > declared)
597
- throw new ShiitakeError("extensions", "conflict", `Stored ${id} state version ${String(ledger.version)} is newer than ${String(declared)}`);
598
- if (ledger !== undefined && ledger.version < declared) {
599
- let rows = readAllRecords(owner, id).map((row) => ({
600
- collection: row.collection,
601
- key: row.key,
602
- value: decodeStoredJson(row.valueJson),
603
- }));
604
- for (let version = ledger.version; version < declared; version += 1) {
605
- const step = definition.state.migrations[version];
606
- if (step === undefined)
607
- throw new IncompatibleStateMigrationError(`Missing ${id} state migration from ${String(version)}`);
608
- try {
609
- rows = rows.flatMap((row) => step(row));
610
- }
611
- catch (cause) {
612
- throw new IncompatibleStateMigrationError(`Failed ${id} state migration from ${String(version)}`, { cause });
613
- }
604
+ const ledger = readStateLedger(owner, id);
605
+ if (ledger !== undefined && ledger.version > declared)
606
+ throw new ShiitakeError("extensions", "conflict", `Stored ${id} state version ${String(ledger.version)} is newer than ${String(declared)}`);
607
+ if (ledger !== undefined && ledger.version < declared) {
608
+ let rows = readAllRecords(owner, id).map((row) => ({
609
+ collection: row.collection,
610
+ key: row.key,
611
+ value: decodeStoredJson(row.valueJson),
612
+ }));
613
+ for (let version = ledger.version; version < declared; version += 1) {
614
+ const step = definition.state.migrations[version];
615
+ if (step === undefined)
616
+ throw new ShiitakeError("extensions", "state_reset_required", `State reset required for ${id}: stored version ${String(ledger.version)}, declared version ${String(declared)}. An operator must authorize the reset; stored records have not been changed.`);
617
+ try {
618
+ rows = rows.flatMap((row) => step(row));
614
619
  }
615
- deleteAllRecords(owner, id);
616
- for (const row of rows) {
617
- const declaredSpec = definition.state.collections[row.collection];
618
- if (declaredSpec === undefined)
619
- throw new IncompatibleStateMigrationError(`Migrated ${id} record names an undeclared collection`);
620
- const decoded = declaredSpec.indexFromEncoded(row.value);
621
- if (decoded._tag === "Failure")
622
- throw new IncompatibleStateMigrationError(`Migrated ${id} ${row.collection} record is invalid`);
623
- writeRecord(owner, id, row.collection, row.key, decoded.success, JSON.stringify(row.value));
620
+ catch (cause) {
621
+ throw new IncompatibleStateMigrationError(`Failed ${id} state migration from ${String(version)}`, { cause });
624
622
  }
625
623
  }
626
- writeStateLedger(owner, id, declared, revision);
627
- });
624
+ deleteAllRecords(owner, id);
625
+ for (const row of rows) {
626
+ const declaredSpec = definition.state.collections[row.collection];
627
+ if (declaredSpec === undefined)
628
+ throw new IncompatibleStateMigrationError(`Migrated ${id} record names an undeclared collection`);
629
+ const decoded = declaredSpec.indexFromEncoded(row.value);
630
+ if (decoded._tag === "Failure")
631
+ throw new IncompatibleStateMigrationError(`Migrated ${id} ${row.collection} record is invalid`);
632
+ writeRecord(owner, id, row.collection, row.key, decoded.success, JSON.stringify(row.value));
633
+ }
634
+ }
635
+ writeStateLedger(owner, id, declared, revision);
628
636
  }
629
- function activateMember(member, scope) {
637
+ function activateMember(member, scope, strict = false) {
630
638
  const context = contextFor(member);
631
- return Effect.suspend(() => member.admitted.definition.activate(context)).pipe(Effect.provideService(Scope.Scope, scope), Effect.exit, Effect.flatMap((exit) => {
639
+ return Effect.gen(function* () {
640
+ // Each activation owns its resources. Closing a failed candidate must
641
+ // not wait for Agent shutdown or release another member's resources.
642
+ const activationScope = yield* Scope.fork(scope);
643
+ member.scope = activationScope;
644
+ const exit = yield* Effect.suspend(() => member.admitted.definition.activate(context)).pipe(Effect.provideService(Scope.Scope, activationScope), Effect.exit);
632
645
  if (Exit.isSuccess(exit)) {
633
646
  member.hooks = exit.value;
634
- return Effect.void;
647
+ return;
635
648
  }
636
- return recordStop(member, stopReason("activation", exit.cause), exit.cause, "own-transaction");
637
- }));
649
+ yield* Scope.close(activationScope, exit);
650
+ if (strict)
651
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", `Extension ${member.admitted.id} failed to start: ${Cause.pretty(exit.cause)}`));
652
+ yield* recordStop(member, stopReason("activation", exit.cause), exit.cause, "own-transaction");
653
+ });
638
654
  }
639
655
  const running = (capability) => [...members.values()].filter((member) => member.status === "running" &&
640
656
  member.hooks !== undefined &&
@@ -668,13 +684,14 @@ export function openExtensionRuntime(input) {
668
684
  : Effect.void;
669
685
  /**
670
686
  * Run pending state migrations. A stored version newer than the declared one
671
- * fails construction; any other failure stops only this extension.
687
+ * fails construction. A missing path isolates this extension with its rows
688
+ * retained; a failed declared migration also guards runs as incompatible.
672
689
  */
673
690
  function prepare(member) {
674
691
  return Effect.try({
675
692
  try: () => {
676
- migrate(member);
677
693
  owner.transactionSync(() => {
694
+ migrate(member);
678
695
  for (const [name, spec] of Object.entries(member.admitted.documents))
679
696
  declareSessionDocument(owner, {
680
697
  extensionId: member.admitted.id,
@@ -686,9 +703,15 @@ export function openExtensionRuntime(input) {
686
703
  });
687
704
  },
688
705
  catch: (error) => error,
689
- }).pipe(Effect.as(true), Effect.catch((error) => {
690
- if (error instanceof ShiitakeError)
691
- return Effect.fail(error);
706
+ }).pipe(Effect.tap(() => Effect.sync(() => {
707
+ delete member.stateResetRequired;
708
+ })), Effect.as(true), Effect.catch((error) => {
709
+ if (error instanceof ShiitakeError) {
710
+ if (error.cause !== "state_reset_required")
711
+ return Effect.fail(error);
712
+ member.stateResetRequired = error;
713
+ return recordStop(member, boundReason(error.message), Cause.fail(error), "own-transaction").pipe(Effect.as(false));
714
+ }
692
715
  if (error instanceof IncompatibleStateMigrationError &&
693
716
  (member.admitted.grants.has("run_tools") ||
694
717
  member.admitted.grants.has("files") ||
@@ -702,49 +725,56 @@ export function openExtensionRuntime(input) {
702
725
  }
703
726
  return Effect.gen(function* () {
704
727
  const scope = yield* Scope.make();
705
- for (const admitted of input.admitted) {
706
- const stored = readStatus(owner, admitted.id);
707
- const keepStopped = stored?.status === "stopped" && stored.revision === admitted.revision;
708
- const member = {
709
- admitted,
710
- hooks: undefined,
711
- status: keepStopped ? "stopped" : "running",
712
- reason: keepStopped ? stored.reason : null,
713
- stoppedAt: keepStopped ? stored.stoppedAt : null,
714
- };
715
- members.set(admitted.id, member);
716
- // Every settled consumer has a cursor from its first roster on, so a
717
- // later resume replays only what it missed.
718
- yield* Effect.try({
719
- try: () => {
720
- if (admitted.grants.has("settled"))
728
+ function startMember(admitted, strict) {
729
+ return Effect.gen(function* () {
730
+ const stored = readStatus(owner, admitted.id);
731
+ const keepStopped = stored?.status === "stopped" && stored.revision === admitted.revision;
732
+ const member = {
733
+ admitted,
734
+ hooks: undefined,
735
+ status: keepStopped ? "stopped" : "running",
736
+ reason: keepStopped ? stored.reason : null,
737
+ stoppedAt: keepStopped ? stored.stoppedAt : null,
738
+ };
739
+ // Every settled consumer has a cursor from its first roster on, so a
740
+ // later resume replays only what it missed.
741
+ yield* Effect.try({
742
+ try: () => {
743
+ if (admitted.grants.has("settled"))
744
+ owner.transactionSync(() => {
745
+ if (readCursor(owner, admitted.id) === undefined)
746
+ advanceCursor(owner, admitted.id, readCursor(owner, coreConsumer) ?? 0);
747
+ });
748
+ },
749
+ catch: () => new ShiitakeError("store", "unavailable", "Settled cursor is unavailable"),
750
+ });
751
+ const prepared = yield* prepare(member);
752
+ if (!prepared || keepStopped) {
753
+ if (strict)
754
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", `Extension ${admitted.id} cannot start: ${member.reason ?? "stopped"}`));
755
+ return member;
756
+ }
757
+ yield* Effect.try({
758
+ try: () => {
721
759
  owner.transactionSync(() => {
722
- if (readCursor(owner, admitted.id) === undefined)
723
- advanceCursor(owner, admitted.id, readCursor(owner, coreConsumer) ?? 0);
724
- });
725
- },
726
- catch: () => new ShiitakeError("store", "unavailable", "Settled cursor is unavailable"),
727
- });
728
- const prepared = yield* prepare(member).pipe(Effect.onError(() => Scope.close(scope, Exit.void)));
729
- if (!prepared)
730
- continue;
731
- if (keepStopped)
732
- continue;
733
- yield* Effect.try({
734
- try: () => {
735
- owner.transactionSync(() => {
736
- writeStatus(owner, admitted.id, {
737
- status: "running",
738
- reason: null,
739
- stoppedAt: null,
740
- revision: admitted.revision,
760
+ writeStatus(owner, admitted.id, {
761
+ status: "running",
762
+ reason: null,
763
+ stoppedAt: null,
764
+ revision: admitted.revision,
765
+ });
766
+ refreshAttentionOwner(admitted.id);
741
767
  });
742
- refreshAttentionOwner(admitted.id);
743
- });
744
- },
745
- catch: () => new ShiitakeError("store", "unavailable", "Extension status is unavailable"),
768
+ },
769
+ catch: () => new ShiitakeError("store", "unavailable", "Extension status is unavailable"),
770
+ });
771
+ yield* activateMember(member, scope, strict);
772
+ return member;
746
773
  });
747
- yield* activateMember(member, scope);
774
+ }
775
+ for (const admitted of input.admitted) {
776
+ const member = yield* startMember(admitted, false).pipe(Effect.onError(() => Scope.close(scope, Exit.void)));
777
+ members.set(admitted.id, member);
748
778
  }
749
779
  yield* Effect.try({
750
780
  try: () => {
@@ -759,6 +789,70 @@ export function openExtensionRuntime(input) {
759
789
  catch: () => new ShiitakeError("store", "unavailable", "Attention grants are unavailable"),
760
790
  });
761
791
  const runtime = {
792
+ stage: (admitted) => Effect.gen(function* () {
793
+ const changed = admitted.filter((next) => {
794
+ const previous = members.get(next.id);
795
+ return (previous !== undefined &&
796
+ previous.admitted.fingerprint !== next.fingerprint);
797
+ });
798
+ if (changed.length > 0)
799
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", `Extensions ${changed.map(({ id }) => id).join(", ")}: this change needs a restart: run fungi stop, then reopen fungi`));
800
+ const additions = new Map();
801
+ const discard = Effect.suspend(() => Effect.forEach([...additions.values()].reverse(), (member) => Effect.exit(member.scope === undefined
802
+ ? Effect.void
803
+ : Scope.close(member.scope, Exit.void)), { concurrency: 1 }).pipe(Effect.tap(() => Effect.sync(() => {
804
+ for (const next of admitted)
805
+ if (!members.has(next.id))
806
+ incompatibleMigrations.delete(next.id);
807
+ })), Effect.flatMap(Exit.asVoidAll)));
808
+ yield* Effect.gen(function* () {
809
+ for (const next of admitted)
810
+ if (!members.has(next.id))
811
+ additions.set(next.id, yield* startMember(next, true));
812
+ }).pipe(Effect.onError(() => discard));
813
+ const target = new Set(admitted.map(({ id }) => id));
814
+ let settled = false;
815
+ return {
816
+ discard: Effect.suspend(() => {
817
+ if (settled)
818
+ return Effect.void;
819
+ settled = true;
820
+ return discard;
821
+ }),
822
+ apply: Effect.gen(function* () {
823
+ if (settled)
824
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", "Roster transition already settled"));
825
+ // Finish fallible store work atomically before releasing live
826
+ // resources. A rolled-back publication must leave both the old
827
+ // timers and the old member scopes available.
828
+ yield* Effect.try({
829
+ try: () => {
830
+ owner.transactionSync(() => {
831
+ for (const [id, member] of members)
832
+ if (!target.has(member.admitted.id))
833
+ deleteExtensionTimers(owner, id);
834
+ const changed = replaceAttentionGrants(owner, admitted
835
+ .filter((entry) => entry.grants.has("attention"))
836
+ .map(({ id }) => id));
837
+ for (const id of changed)
838
+ refreshAttentionOwner(id);
839
+ });
840
+ },
841
+ catch: () => new ShiitakeError("store", "unavailable", "Roster publication storage is unavailable"),
842
+ });
843
+ for (const [id, member] of members)
844
+ if (!target.has(member.admitted.id)) {
845
+ if (member.scope !== undefined)
846
+ yield* Scope.close(member.scope, Exit.void);
847
+ members.delete(id);
848
+ incompatibleMigrations.delete(member.admitted.id);
849
+ }
850
+ for (const [id, member] of additions)
851
+ members.set(id, member);
852
+ settled = true;
853
+ }),
854
+ };
855
+ }),
762
856
  runParticipants: () => [...members.values()]
763
857
  .filter(isRunParticipant)
764
858
  .map((member) => [member.admitted.id, member.admitted.fingerprint]),
@@ -931,7 +1025,8 @@ export function openExtensionRuntime(input) {
931
1025
  if (member?.admitted.grants.has("commands") !== true)
932
1026
  return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is unavailable"));
933
1027
  if (member.status !== "running" || member.hooks === undefined)
934
- return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", `Extension ${extensionId} is stopped: ${member.reason ?? "not activated"}`));
1028
+ return yield* Effect.fail(member.stateResetRequired ??
1029
+ new ShiitakeError("extensions", "unavailable", `Extension ${extensionId} is stopped: ${member.reason ?? "not activated"}`));
935
1030
  const commands = member.admitted.definition.contract.commands;
936
1031
  const spec = Object.hasOwn(commands, command)
937
1032
  ? commands[command]
@@ -31,6 +31,13 @@ export type StandardHostMetaResult = Readonly<{
31
31
  */
32
32
  export type HostMetaOptions = Readonly<{
33
33
  schema: Schema.Codec<unknown, Json> | StandardHostMetaSchema;
34
+ /**
35
+ * Host-selected SQLite JSON path to its stable principal identity.
36
+ * The host owns this metadata grammar. When bound metadata is present,
37
+ * pending lookup and exact cancellation stay within that identity.
38
+ * Unbound in-process owners retain operational cancellation authority.
39
+ */
40
+ identityPath?: string;
34
41
  }>;
35
42
  /**
36
43
  * Check one host-attached value: finite JSON within the cap, and accepted by
@@ -14,7 +14,13 @@ function asEffectAcquisition(acquisition) {
14
14
  try: () => acquisition.release(),
15
15
  catch: releaseFailure,
16
16
  });
17
- const shared = { model: acquisition.model, release };
17
+ let shared = {
18
+ model: acquisition.model,
19
+ release,
20
+ retryable: acquisition.retryable,
21
+ };
22
+ if (acquisition.failureReason !== undefined)
23
+ shared = { ...shared, failureReason: acquisition.failureReason };
18
24
  if (acquisition.stream === undefined)
19
25
  return acquisition.retryAfterMs === undefined
20
26
  ? Object.freeze(shared)
@@ -1,4 +1,6 @@
1
1
  import * as Schema from "effect/Schema";
2
+ /** The thinking levels a model choice may name, shared by every model path. */
3
+ export declare const reasoningLevels: readonly ["minimal", "low", "medium", "high", "xhigh", "max"];
2
4
  /** Secret-free request to the host's maintained model catalogue. */
3
5
  export declare const ModelPreferenceSchema: Schema.Struct<{
4
6
  readonly providerId: Schema.String;
@@ -1,6 +1,7 @@
1
1
  import * as Schema from "effect/Schema";
2
2
  const identifier = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(256)));
3
- const reasoningLevels = [
3
+ /** The thinking levels a model choice may name, shared by every model path. */
4
+ export const reasoningLevels = [
4
5
  "minimal",
5
6
  "low",
6
7
  "medium",
@@ -1,9 +1,8 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Predicate from "effect/Predicate";
3
3
  import * as Schema from "effect/Schema";
4
- import { fromTypedSandbox } from "@fungi.computer/mycelium/effect";
5
4
  import { ShiitakeError } from "../errors.js";
6
- import { runShiitakePromise, toPromiseOwner } from "./effect-client.js";
5
+ import { liftTools, runShiitakePromise, toPromiseOwner, } from "./effect-client.js";
7
6
  import { adaptModelSource } from "./model-acquisition.js";
8
7
  function boundary(boundaryName, message) {
9
8
  return new ShiitakeError(boundaryName, "rejected", message);
@@ -138,25 +137,14 @@ function liftOptions(options, runtime) {
138
137
  };
139
138
  if (options.meta !== undefined)
140
139
  Object.assign(base, { meta: options.meta });
141
- if (options.storageBudget !== undefined)
142
- Object.assign(base, { storageBudget: options.storageBudget });
140
+ if (options.storagePressure !== undefined)
141
+ Object.assign(base, { storagePressure: options.storagePressure });
143
142
  if (options.compaction !== undefined)
144
143
  Object.assign(base, { compaction: options.compaction });
145
144
  const toolSource = options.tools;
146
145
  if (toolSource === undefined)
147
146
  return base;
148
- const tools = {
149
- modules: toolSource.modules ?? [],
150
- sources: toolSource.sources ?? [],
151
- sandbox: fromTypedSandbox(toolSource.sandbox),
152
- access: toolSource.access ?? {},
153
- };
154
- return {
155
- ...base,
156
- tools: toolSource.execution === undefined
157
- ? tools
158
- : { ...tools, execution: toolSource.execution },
159
- };
147
+ return { ...base, tools: liftTools(toolSource) };
160
148
  }
161
149
  /** Open the single Promise adaptation over the canonical Effect agent. */
162
150
  export function openPromiseAgent(options, make) {
@@ -0,0 +1,10 @@
1
+ import type { MuleRunOutcome } from "@fungi.computer/mule/effect";
2
+ /** Observations, not a classification of arbitrary provider error text. */
3
+ export type UnavailableRunReason = Readonly<{
4
+ kind: "reason_unavailable";
5
+ runId: string;
6
+ provider: string | null;
7
+ model: string | null;
8
+ }>;
9
+ export declare function unavailableRunReason(observations: Omit<UnavailableRunReason, "kind">): string;
10
+ export declare function returnedFailureReason(run: MuleRunOutcome): string;
@@ -0,0 +1,19 @@
1
+ export function unavailableRunReason(observations) {
2
+ const reason = {
3
+ kind: "reason_unavailable",
4
+ ...observations,
5
+ provider: observations.provider === "" ? null : observations.provider,
6
+ model: observations.model === "" ? null : observations.model,
7
+ };
8
+ // Watchdog bounds reasons to 200 characters. Bound identity metadata only;
9
+ // truncation is not redaction and must never make provider prose trusted.
10
+ return `${reason.kind}: run=${reason.runId}; provider=${reason.provider ?? "unknown"}; model=${reason.model ?? "unknown"}`.slice(0, 200);
11
+ }
12
+ export function returnedFailureReason(run) {
13
+ // Pi errorMessage can contain raw SDK messages and response bodies.
14
+ return unavailableRunReason({
15
+ runId: run.run.value,
16
+ provider: run.message.provider,
17
+ model: run.message.model,
18
+ });
19
+ }
@@ -18,7 +18,10 @@ export declare class ToolSetupError extends ShiitakeError {
18
18
  get diagnostic(): string;
19
19
  }
20
20
  /** Run one accepted command with Pi compaction explicitly composed around Mule. */
21
- export declare function runAcceptedCommand(options: ShiitakeEffectOptions, prepared: PreparedSessionRun, executionContract: RunContract, extensionModules: readonly ModuleRegistration[], extensionFileTools: readonly FileTool[], compactionHandoff: CompactionHandoffV0 | undefined, promptSections?: () => Effect.Effect<readonly PromptSectionV0[], ShiitakeError>): Effect.Effect<import("@fungi.computer/mule/effect").MuleRunOutcome, ShiitakeError, never>;
21
+ export declare function runAcceptedCommand(options: ShiitakeEffectOptions, prepared: PreparedSessionRun, executionContract: RunContract, extensionModules: readonly ModuleRegistration[], extensionFileTools: readonly FileTool[], compactionHandoff: CompactionHandoffV0 | undefined, promptSections?: () => Effect.Effect<readonly PromptSectionV0[], ShiitakeError>): Effect.Effect<{
22
+ run: import("@fungi.computer/mule/effect").MuleRunOutcome;
23
+ failureReason: string | undefined;
24
+ }, ShiitakeError, never>;
22
25
  /**
23
26
  * Run one accepted compaction request: the run's model acquisition and the
24
27
  * Session's one compaction commit, with no model turn and no tools.
@@ -1,10 +1,14 @@
1
1
  import { muleEffect, piCodingAgentRetryDefaults, piHttpIdleTimeoutMs, } from "@fungi.computer/mule/effect";
2
2
  import { make as makeMycelium } from "@fungi.computer/mycelium/effect";
3
3
  import * as Effect from "effect/Effect";
4
+ import * as Cause from "effect/Cause";
5
+ import * as Exit from "effect/Exit";
6
+ import * as Option from "effect/Option";
4
7
  import { ShiitakeError } from "../errors.js";
5
8
  import { compactSession, runWithCompaction, } from "./compaction/session-compaction.js";
6
9
  import { openCompactionSettings } from "./compaction/settings.js";
7
10
  import { acquireOwnedModel } from "./model-acquisition.js";
11
+ import { returnedFailureReason, unavailableRunReason, } from "./run-failure-reason.js";
8
12
  import { isCompactCommand, openMuleRecorder, readSessionGrants, recordLiveRetryEvent, recordSandboxFailure, takeSessionControls, } from "./woodstock/index.js";
9
13
  function runtimeFailure(message) {
10
14
  return new ShiitakeError("runtime", "rejected", message);
@@ -134,7 +138,8 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
134
138
  })));
135
139
  let resume = prepared.resume;
136
140
  const settings = openCompactionSettings(options.compaction)(acquired.model);
137
- return yield* runWithCompaction({
141
+ let failureReason;
142
+ const run = yield* runWithCompaction({
138
143
  store: options.store,
139
144
  sessionId: prepared.command.sessionId,
140
145
  workId: prepared.command.entry.workId,
@@ -148,12 +153,23 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
148
153
  run: (workingMessages, betweenTurns) => {
149
154
  const continuation = resume;
150
155
  resume = undefined;
151
- const listener = (event) => recorder(event);
156
+ // A failed lifecycle notification precedes Mule's returned outcome
157
+ // (or typed error). Join them before publishing the single terminal;
158
+ // no observer can win a race against a later diagnostic settlement.
159
+ let failedEnd;
160
+ const listener = (event) => {
161
+ if (event.type === "run_end" && event.outcome === "failed") {
162
+ failedEnd = event;
163
+ return Effect.void;
164
+ }
165
+ return recorder(event);
166
+ };
152
167
  const input = {
153
168
  model: acquired.model,
154
169
  modelStreamIdleTimeoutMs: options.modelStreamIdleTimeoutMs ?? piHttpIdleTimeoutMs,
155
170
  retry: options.retry ?? piCodingAgentRetryDefaults,
156
171
  retryAfterMs: acquired.retryAfterMs,
172
+ retryable: acquired.retryable,
157
173
  systemPrompt: instructions.text,
158
174
  prepareSystemPrompt: () => (promptSections === undefined
159
175
  ? Effect.succeed([])
@@ -181,11 +197,28 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
181
197
  .run(acquired.stream === undefined
182
198
  ? input
183
199
  : { ...input, stream: acquired.stream }, listener)
184
- .pipe(Effect.mapError((error) => runtimeFailure(error.phase === "model"
185
- ? error.message
186
- : "Mule rejected the durable command run")));
200
+ .pipe(Effect.onExit((exit) => {
201
+ if (failedEnd === undefined)
202
+ return Effect.void;
203
+ if (Exit.isSuccess(exit)) {
204
+ failureReason = (acquired.failureReason?.(exit.value.message) ??
205
+ returnedFailureReason(exit.value)).slice(0, 200);
206
+ return recorder(failedEnd, failureReason);
207
+ }
208
+ const error = Cause.findErrorOption(exit.cause);
209
+ return recorder(failedEnd, Option.isSome(error) && error.value.phase === "model"
210
+ ? `model:rejected: ${error.value.message}`.slice(0, 200)
211
+ : unavailableRunReason({
212
+ runId: failedEnd.run.value,
213
+ provider: acquired.model.provider,
214
+ model: acquired.model.id,
215
+ }));
216
+ }), Effect.mapError((error) => error.phase === "model"
217
+ ? new ShiitakeError("model", "rejected", error.message, undefined, error)
218
+ : runtimeFailure("Mule rejected the durable command run")));
187
219
  },
188
220
  });
221
+ return { run, failureReason };
189
222
  }));
190
223
  }
191
224
  /**
@@ -5,11 +5,12 @@ import type { AgentRunningWork } from "../client.js";
5
5
  import { ShiitakeError } from "../errors.js";
6
6
  import { type AdmissionDoor } from "./admission-door.js";
7
7
  import type { ShiitakeEffectOptions } from "./effect-contracts.js";
8
- import type { AdmittedExtensionV0 } from "./extension-roster.js";
8
+ import { type AdmittedExtensionV0 } from "./extension-roster.js";
9
9
  import { type ExtensionRuntime } from "./extension-runtime.js";
10
10
  import { type RecordAcceptedCancellation, type StorageAdmission } from "./woodstock/index.js";
11
11
  /** Bound lifecycle and owner-transaction doors used by the public coordinator. */
12
12
  export type RuntimeCoordinator = Readonly<{
13
+ prepareConfiguration: import("./effect-contracts.js").ShiitakeEffectOwner["prepareConfiguration"];
13
14
  activeSessions: Effect.Effect<readonly string[]>;
14
15
  runningWork: (jobId: string) => Effect.Effect<AgentRunningWork | null, ShiitakeError>;
15
16
  assertOpen: Effect.Effect<void, ShiitakeError>;