@fungi.computer/shiitake 0.5.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 (88) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +94 -2
  3. package/dist/agent-control.js +147 -26
  4. package/dist/client-entry.d.ts +20 -0
  5. package/dist/client-entry.js +5 -0
  6. package/dist/client-visitor.d.ts +33 -0
  7. package/dist/client-visitor.js +231 -0
  8. package/dist/client-wire.d.ts +9 -0
  9. package/dist/client-wire.js +24 -3
  10. package/dist/client.d.ts +27 -6
  11. package/dist/client.js +23 -0
  12. package/dist/cloudflare.d.ts +20 -0
  13. package/dist/cloudflare.js +4 -1
  14. package/dist/delegation.d.ts +79 -53
  15. package/dist/delegation.js +26 -21
  16. package/dist/effect.d.ts +26 -1
  17. package/dist/effect.js +14 -1
  18. package/dist/errors.d.ts +2 -2
  19. package/dist/errors.js +2 -0
  20. package/dist/extensions.d.ts +22 -3
  21. package/dist/extensions.js +1 -1
  22. package/dist/fleet.d.ts +25 -2
  23. package/dist/fleet.js +108 -24
  24. package/dist/hosted.js +3 -0
  25. package/dist/http-routes.d.ts +23 -9
  26. package/dist/http.d.ts +1 -0
  27. package/dist/http.js +1 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/internal/accepted-run-executor.js +2 -1
  30. package/dist/internal/admission-door.d.ts +6 -1
  31. package/dist/internal/admission-door.js +33 -3
  32. package/dist/internal/contracts.d.ts +19 -1
  33. package/dist/internal/coordinator.js +115 -52
  34. package/dist/internal/effect-client.js +1 -1
  35. package/dist/internal/effect-contracts.d.ts +9 -2
  36. package/dist/internal/extension-command-registry.d.ts +2 -1
  37. package/dist/internal/extension-command-registry.js +3 -1
  38. package/dist/internal/extension-runtime.d.ts +3 -1
  39. package/dist/internal/extension-runtime.js +18 -8
  40. package/dist/internal/model-acquisition.js +2 -0
  41. package/dist/internal/observation-kernel.d.ts +2 -0
  42. package/dist/internal/observation-kernel.js +17 -6
  43. package/dist/internal/promise-facade.js +2 -0
  44. package/dist/internal/run-scope.d.ts +14 -0
  45. package/dist/internal/run-scope.js +58 -15
  46. package/dist/internal/runtime-coordinator.d.ts +14 -0
  47. package/dist/internal/runtime-coordinator.js +60 -11
  48. package/dist/internal/runtime-core.d.ts +3 -0
  49. package/dist/internal/runtime-core.js +1 -0
  50. package/dist/internal/runtime-reconciliation.js +123 -25
  51. package/dist/internal/session-context.d.ts +5 -5
  52. package/dist/internal/session-context.js +80 -83
  53. package/dist/internal/session-status.d.ts +2 -2
  54. package/dist/internal/session-status.js +5 -3
  55. package/dist/internal/woodstock/agent-reply.d.ts +5 -0
  56. package/dist/internal/woodstock/agent-reply.js +3 -0
  57. package/dist/internal/woodstock/client-projection.js +13 -11
  58. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  59. package/dist/internal/woodstock/fleet-store.js +24 -4
  60. package/dist/internal/woodstock/index.d.ts +7 -3
  61. package/dist/internal/woodstock/index.js +23 -6
  62. package/dist/internal/woodstock/progress.d.ts +11 -1
  63. package/dist/internal/woodstock/progress.js +126 -25
  64. package/dist/internal/woodstock/session-delegator.d.ts +13 -0
  65. package/dist/internal/woodstock/session-delegator.js +44 -0
  66. package/dist/internal/woodstock/session-entry.d.ts +12 -2
  67. package/dist/internal/woodstock/session-store.d.ts +16 -2
  68. package/dist/internal/woodstock/session-store.js +349 -66
  69. package/dist/internal/woodstock/work-senders.d.ts +32 -0
  70. package/dist/internal/woodstock/work-senders.js +70 -0
  71. package/dist/node.js +11 -2
  72. package/dist/remote-client.js +7 -2
  73. package/dist/server-visitor-stream.d.ts +13 -0
  74. package/dist/server-visitor-stream.js +124 -0
  75. package/dist/session-attention.d.ts +30 -0
  76. package/dist/session-attention.js +14 -0
  77. package/docs/api/agent-control.md +212 -15
  78. package/docs/api/client-entry.md +3 -0
  79. package/docs/api/client.md +209 -5
  80. package/docs/api/cloudflare.md +23 -1
  81. package/docs/api/delegation.md +284 -77
  82. package/docs/api/effect.md +100 -12
  83. package/docs/api/extensions.md +73 -12
  84. package/docs/api/fleet.md +138 -25
  85. package/docs/api/http.md +28 -0
  86. package/docs/api/index.md +43 -17
  87. package/docs/api/testing.md +9 -7
  88. package/package.json +10 -6
@@ -1,17 +1,29 @@
1
1
  import { muleEffect, piCodingAgentRetryDefaults, } 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 Fiber from "effect/Fiber";
4
5
  import * as Cause from "effect/Cause";
5
6
  import * as Exit from "effect/Exit";
6
7
  import * as Option from "effect/Option";
8
+ import * as Scope from "effect/Scope";
7
9
  import { ShiitakeError } from "../errors.js";
8
10
  import { compactSession, runWithCompaction, } from "./compaction/session-compaction.js";
9
11
  import { openCompactionSettings } from "./compaction/settings.js";
10
12
  import { acquireOwnedModel } from "./model-acquisition.js";
11
13
  import { returnedFailureReason, unavailableRunReason, } from "./run-failure-reason.js";
12
14
  import { isCompactCommand, openMuleRecorder, readSessionGrants, recordLiveRetryEvent, recordSandboxFailure, takeSessionControls, } from "./woodstock/index.js";
13
- function runtimeFailure(message) {
14
- return new ShiitakeError("runtime", "rejected", message);
15
+ // A run writes its terminal before scope release. Give each foreign release
16
+ // one second to clean up without blocking the next Watchdog job indefinitely.
17
+ const runFinalizerGraceMs = 1_000;
18
+ function boundedRunFinalizer(name, workId, release) {
19
+ return Effect.gen(function* () {
20
+ // A detached release may finish later; interrupting a finalizer itself
21
+ // cannot bound an uninterruptible scope.
22
+ const fiber = yield* Effect.forkDetach(release);
23
+ const finished = yield* Effect.raceFirst(Fiber.join(fiber).pipe(Effect.as(true)), Effect.sleep(runFinalizerGraceMs).pipe(Effect.as(false)));
24
+ if (!finished)
25
+ yield* Effect.logError("shiitake_run_finalizer_hung").pipe(Effect.annotateLogs({ workId, finalizer: name }));
26
+ });
15
27
  }
16
28
  const toolSetupMessages = {
17
29
  missing: "Instruction capabilities have no tool runtime",
@@ -29,6 +41,28 @@ export class ToolSetupError extends ShiitakeError {
29
41
  return toolSetupMessages[this.phase];
30
42
  }
31
43
  }
44
+ /** Closed reasons a rejected run names, never a foreign error's own text. */
45
+ const runRejectedDiagnostics = {
46
+ model: "model-failed",
47
+ tool: "tool-failed",
48
+ control: "control-failed",
49
+ listener: "listener-failed",
50
+ };
51
+ /** A run Mule ended with a non-model error; it names a closed reason. */
52
+ export class RunRejectedError extends ShiitakeError {
53
+ reason;
54
+ constructor(reason, underlying) {
55
+ super("runtime", "rejected", "Mule rejected the durable command run", undefined, underlying);
56
+ this.reason = reason;
57
+ }
58
+ get diagnostic() {
59
+ return runRejectedDiagnostics[this.reason];
60
+ }
61
+ }
62
+ /** Name a non-model Mule error by its closed reason. */
63
+ function rejectedRun(error) {
64
+ return new RunRejectedError(error.phase, error);
65
+ }
32
66
  /**
33
67
  * Map Mule's retry transitions onto Woodstock's retained `live_retry` facts.
34
68
  * A started attempt ends the backoff countdown before streaming resumes.
@@ -100,12 +134,12 @@ function acquiredTools(options, sessionId, workId, meta, instructions, extension
100
134
  sessionGrants: () => readSessionGrants(options.store, sessionId)?.grants ?? {},
101
135
  },
102
136
  };
137
+ const toolScope = yield* Scope.make();
138
+ yield* Effect.addFinalizer(() => boundedRunFinalizer("tools", workId, Scope.close(toolScope, Exit.succeed(undefined))));
103
139
  const runtime = yield* makeMycelium(toolSource.execution === undefined
104
140
  ? runtimeOptions
105
- : { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.mapError(() => new ToolSetupError("runtime")));
106
- const lease = yield* runtime
107
- .acquire()
108
- .pipe(Effect.mapError(() => new ToolSetupError("lease")));
141
+ : { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.provideService(Scope.Scope, toolScope), Effect.mapError(() => new ToolSetupError("runtime")));
142
+ const lease = yield* runtime.acquire().pipe(Effect.provideService(Scope.Scope, toolScope), Effect.mapError(() => new ToolSetupError("lease")));
109
143
  return [lease.executeTool, ...lease.tools];
110
144
  });
111
145
  }
@@ -118,11 +152,16 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
118
152
  });
119
153
  const releaseInstructions = instructions.release;
120
154
  if (releaseInstructions !== undefined)
121
- yield* Effect.addFinalizer(() => Effect.sync(releaseInstructions));
122
- const acquired = yield* acquireOwnedModel(options.model.acquire({
155
+ yield* Effect.addFinalizer(() => boundedRunFinalizer("instructions", prepared.command.entry.workId, Effect.sync(releaseInstructions)));
156
+ const acquired = yield* acquireOwnedModel(options.model
157
+ .acquire({
123
158
  sessionId: prepared.command.sessionId,
124
159
  preference: prepared.modelPreference,
125
- }));
160
+ })
161
+ .pipe(Effect.map((model) => ({
162
+ ...model,
163
+ release: boundedRunFinalizer("model", prepared.command.entry.workId, model.release),
164
+ }))));
126
165
  const tools = yield* acquiredTools(options, prepared.command.sessionId, prepared.command.entry.workId, prepared.command.entry.meta, instructions, extensionModules, extensionFileTools);
127
166
  const accepted = Object.freeze({
128
167
  sessionId: prepared.command.sessionId,
@@ -229,22 +268,26 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
229
268
  acquired.failureReason?.(exit.value.message) ??
230
269
  returnedFailureReason(exit.value);
231
270
  failureReason = (failedModelAttempts > 1 &&
232
- exit.value.modelLimitReason === undefined
271
+ exit.value.modelLimitReason === undefined &&
272
+ acquired.rejection?.(exit.value.message)?.code !==
273
+ "usage-limit-reached"
233
274
  ? `The model failed ${String(failedModelAttempts)} times on this conversation: ${cause}`
234
275
  : cause).slice(0, 200);
235
276
  return recorder(failedEnd, failureReason);
236
277
  }
237
278
  const error = Cause.findErrorOption(exit.cause);
238
- return recorder(failedEnd, Option.isSome(error) && error.value.phase === "model"
239
- ? `model:rejected: ${error.value.message}`.slice(0, 200)
240
- : unavailableRunReason({
279
+ return recorder(failedEnd, Option.isNone(error)
280
+ ? unavailableRunReason({
241
281
  runId: failedEnd.run.value,
242
282
  provider: acquired.model.provider,
243
283
  model: acquired.model.id,
244
- }));
284
+ })
285
+ : error.value.phase === "model"
286
+ ? `model:rejected: ${error.value.message}`.slice(0, 200)
287
+ : `runtime:rejected: ${rejectedRun(error.value).diagnostic}`);
245
288
  }), Effect.mapError((error) => error.phase === "model"
246
289
  ? new ShiitakeError("model", "rejected", error.message, undefined, error)
247
- : runtimeFailure("Mule rejected the durable command run")));
290
+ : rejectedRun(error)));
248
291
  },
249
292
  });
250
293
  return { run, failureReason };
@@ -9,10 +9,23 @@ import type { ShiitakeEffectOptions } from "./effect-contracts.js";
9
9
  import { type AdmittedExtensionV0 } from "./extension-roster.js";
10
10
  import { type ExtensionRuntime } from "./extension-runtime.js";
11
11
  import { type RecordAcceptedCancellation, type StorageAdmission } from "./woodstock/index.js";
12
+ /** Owner-local archive blocker read within the caller's SQLite transaction. */
13
+ export type OwnerBusy = Readonly<{
14
+ kind: "session";
15
+ sessionId: string;
16
+ commandId: string;
17
+ }> | Readonly<{
18
+ kind: "delegation";
19
+ agentId: string;
20
+ sessionId: string;
21
+ commandId: string;
22
+ }>;
12
23
  /** Bound lifecycle and owner-transaction doors used by the public coordinator. */
13
24
  export type RuntimeCoordinator = Readonly<{
14
25
  prepareConfiguration: import("./effect-contracts.js").ShiitakeEffectOwner["prepareConfiguration"];
15
26
  activeSessions: Effect.Effect<readonly string[]>;
27
+ /** No awaits: use inside the same transaction as the host's archive flag. */
28
+ busySync: () => OwnerBusy | undefined;
16
29
  runningWork: (jobId: string) => Effect.Effect<AgentRunningWork | null, ShiitakeError>;
17
30
  assertOpen: Effect.Effect<void, ShiitakeError>;
18
31
  /** Rostered v1 extensions bound to core's doors and tables. */
@@ -22,6 +35,7 @@ export type RuntimeCoordinator = Readonly<{
22
35
  door: AdmissionDoor;
23
36
  /** Reserve a Session within the admission door's enclosing transaction. */
24
37
  createSessionWithinTransaction: (requestId: string) => string;
38
+ recordCreatedSessionDelegatorWithinTransaction: (sessionId: string, mail: import("@fungi.computer/bird-dog").BirdDogMail) => void;
25
39
  /** Write new-Session grants through the enclosing admission transaction. */
26
40
  stageSessionGrantsWithinTransaction: (sessionId: string, delivery: GrantsDelivery) => void;
27
41
  /** Storage-pressure check for new Sessions and new prompts. */
@@ -3,15 +3,16 @@ import { WatchdogError } from "@fungi.computer/watchdog";
3
3
  import { makeRuntime as makeWatchdogRuntime, } from "@fungi.computer/watchdog/effect";
4
4
  import * as Effect from "effect/Effect";
5
5
  import * as Latch from "effect/Latch";
6
+ import { readOutstandingDelegationSync } from "../fleet.js";
6
7
  import { publishAgentExecutionChange } from "./observation-kernel.js";
7
8
  import { ShiitakeError } from "../errors.js";
8
9
  import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.js";
9
10
  import { openAdmissionDoor } from "./admission-door.js";
10
11
  import { openCompactionSettings } from "./compaction/settings.js";
11
12
  import { admitExtensionRosterV0, } from "./extension-roster.js";
12
- import { coreConsumer, openExtensionRuntime, } from "./extension-runtime.js";
13
+ import { coreConsumer, reportConsumer, openExtensionRuntime, } from "./extension-runtime.js";
13
14
  import { reconcileRuntime } from "./runtime-reconciliation.js";
14
- import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
15
+ import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, recordSessionDelegator, projectSessionRun, openStorageAdmission, readCursor, readSessionInboxPage, readSessionSummary, } from "./woodstock/index.js";
15
16
  function runtimeFailure(message) {
16
17
  return new ShiitakeError("runtime", "rejected", message);
17
18
  }
@@ -119,13 +120,42 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
119
120
  ? error
120
121
  : runtimeFailure("Storage budget is unavailable"),
121
122
  });
123
+ const admitNew = () => {
124
+ options.admission?.assertOpen();
125
+ storage.admitNew();
126
+ };
127
+ const busySync = () => {
128
+ let after;
129
+ do {
130
+ const page = readSessionInboxPage(options.store, after);
131
+ for (const session of page.sessions) {
132
+ if (session.state === "unavailable")
133
+ throw new ShiitakeError("store", "unavailable", `Session ${session.sessionId} is unreadable`);
134
+ const head = watchdog.transactional.readQueueHead(session.sessionId);
135
+ if (head.state !== "idle")
136
+ return {
137
+ kind: "session",
138
+ sessionId: session.sessionId,
139
+ commandId: head.jobId,
140
+ };
141
+ }
142
+ after = page.next;
143
+ } while (after !== undefined);
144
+ const outstanding = readOutstandingDelegationSync(options.store);
145
+ return outstanding === undefined
146
+ ? undefined
147
+ : { kind: "delegation", ...outstanding };
148
+ };
122
149
  const door = openAdmissionDoor({
123
150
  owner: options.store,
124
- admitNew: storage.admitNew,
151
+ admitNew,
125
152
  hostMeta: options.meta,
126
153
  contract,
127
154
  enqueue: (work, mail) => {
128
- const input = { work: { ...work, recovery: agentTurnRecovery } };
155
+ const input = {
156
+ work: { ...work, recovery: agentTurnRecovery },
157
+ delivery: work.delivery,
158
+ };
129
159
  watchdog.transactional.enqueueAcceptedJob(agentJobFromInput(mail === undefined ? input : { ...input, mail }));
130
160
  },
131
161
  readQueueHead: (queue) => watchdog.transactional.readQueueHead(queue),
@@ -141,6 +171,12 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
141
171
  options.store.transactionSync(() => {
142
172
  advanceCursor(options.store, coreConsumer, 0);
143
173
  });
174
+ // A store that predates reports starts them at the core cursor
175
+ // instead of reporting its history.
176
+ if (readCursor(options.store, reportConsumer) === undefined)
177
+ options.store.transactionSync(() => {
178
+ advanceCursor(options.store, reportConsumer, readCursor(options.store, coreConsumer) ?? 0);
179
+ });
144
180
  },
145
181
  catch: () => new ShiitakeError("store", "unavailable", "Extension store is unavailable"),
146
182
  });
@@ -165,6 +201,7 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
165
201
  const cursors = yield* Effect.try({
166
202
  try: () => [
167
203
  readCursor(options.store, coreConsumer) ?? 0,
204
+ readCursor(options.store, reportConsumer) ?? 0,
168
205
  ...extensions.settledConsumers().map(({ cursor }) => cursor),
169
206
  ],
170
207
  catch: () => runtimeFailure("Settled cursor is unavailable"),
@@ -241,11 +278,20 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
241
278
  };
242
279
  }),
243
280
  activeSessions: Effect.sync(() => [...activeSessions]),
244
- runningWork: (jobId) => watchdog.read(jobId).pipe(Effect.map((result) => result.status === "found"
245
- ? (runningSlots.get(result.job.lane === options.hostJobs?.lane
246
- ? "host"
247
- : "serial") ?? null)
248
- : null), Effect.mapError((cause) => new ShiitakeError("runtime", "rejected", "Running work is unavailable", undefined, cause))),
281
+ busySync,
282
+ runningWork: (jobId) => watchdog.read(jobId).pipe(Effect.flatMap((result) => {
283
+ const work = result.status === "found"
284
+ ? runningSlots.get(result.job.lane === options.hostJobs?.lane
285
+ ? "host"
286
+ : "serial")
287
+ : undefined;
288
+ if (work === undefined || work.kind === "host")
289
+ return Effect.succeed(work ?? null);
290
+ return readSessionSummary(options.store, work.sessionId).pipe(Effect.map((summary) => ({
291
+ ...work,
292
+ title: summary?.title ?? "",
293
+ })));
294
+ }), Effect.mapError((cause) => new ShiitakeError("runtime", "rejected", "Running work is unavailable", undefined, cause))),
249
295
  extensions,
250
296
  commit: bound.commit,
251
297
  assertOpen: Effect.suspend(() => lifecycleState === "open"
@@ -253,11 +299,14 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
253
299
  : Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"))),
254
300
  beginClose,
255
301
  door,
256
- createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId, storage.admitNew),
302
+ createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId, admitNew),
303
+ recordCreatedSessionDelegatorWithinTransaction: (sessionId, mail) => {
304
+ recordSessionDelegator(options.store, sessionId, mail);
305
+ },
257
306
  stageSessionGrantsWithinTransaction: (sessionId, delivery) => {
258
307
  deliverSessionGrants(options.store, sessionId, delivery);
259
308
  },
260
- storage,
309
+ storage: { admitNew },
261
310
  recordAcceptedCancellation: ({ jobId, cancellationId, onlyIfPending }) => watchdog.transactional.recordCancellationIntent({
262
311
  jobId,
263
312
  cancellationId,
@@ -8,12 +8,15 @@ import type { Accepted } from "../client.js";
8
8
  import type { GrantsDelivery } from "./contracts.js";
9
9
  import { ShiitakeError } from "../errors.js";
10
10
  import type { ShiitakeEffectOptions, ShiitakeEffectOwner } from "./effect-contracts.js";
11
+ import type { OwnerBusy } from "./runtime-coordinator.js";
11
12
  /** Private host composition with the addressed-mail ingress kept off Session. */
12
13
  export type RuntimeCore = Readonly<{
13
14
  agent: ShiitakeEffectOwner;
14
15
  readSettlement: (sessionId: string, commandId: string) => Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
15
16
  /** Owner-local execution projection; never served by the remote client. */
16
17
  activeSessions: Effect.Effect<readonly string[]>;
18
+ /** Synchronous archive blocker; the host supplies the enclosing transaction. */
19
+ busySync: () => OwnerBusy | undefined;
17
20
  acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply, stageAcceptedNotice?: (mail: BirdDogMail) => void, sessionGrantDelivery?: (sessionId: string) => GrantsDelivery) => Effect.Effect<Accepted, ShiitakeError>;
18
21
  }>;
19
22
  /** Open Runtime Core, optionally joining its owner-local BirdDog mail room. */
@@ -34,6 +34,7 @@ export function openRuntimeCore(options, mailRoom) {
34
34
  agent: owner,
35
35
  readSettlement: (sessionId, commandId) => runtime.assertOpen.pipe(Effect.andThen(readCommandSettlement(options.store, sessionId, commandId))),
36
36
  activeSessions: runtime.activeSessions,
37
+ busySync: runtime.busySync,
37
38
  acceptMail: (mail, meta, reply, stageAcceptedNotice, sessionGrantDelivery) => acceptAddressedMail(runtime, mail, meta, reply, stageAcceptedNotice ??
38
39
  ((notice) => {
39
40
  if (mailRoom?.postMail === undefined)
@@ -5,21 +5,19 @@ import * as Schema from "effect/Schema";
5
5
  import { agentTurnRecovery } from "./accepted-run-executor.js";
6
6
  import { ShiitakeError } from "../errors.js";
7
7
  import { ExtensionId } from "../extension-contract.js";
8
- import { coreConsumer } from "./extension-runtime.js";
8
+ import { coreConsumer, reportConsumer, } from "./extension-runtime.js";
9
9
  import { publishOwnerObservation } from "./observation-kernel.js";
10
- import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSettlementMeta, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
10
+ import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSessionDelegator, readSettlementMeta, readWaitingOn, readWaitingOnSender, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
11
11
  const pageSize = 64;
12
12
  function runtimeFailure(message) {
13
13
  return new ShiitakeError("runtime", "rejected", message);
14
14
  }
15
- function fanOutSettlement(store, mailRoom, job) {
16
- if (mailRoom === undefined)
17
- return Effect.void;
15
+ /** BirdDog's settlement facts for one settled job, without its correspondents. */
16
+ function settlementFacts(store, job,
17
+ /** The open items this settlement names as waiting on its recipient. */
18
+ waitingOnYou) {
18
19
  return Effect.gen(function* () {
19
- const payload = yield* Effect.try({
20
- try: () => decodeAgentJob(job),
21
- catch: () => runtimeFailure("Agent work payload is invalid"),
22
- });
20
+ const payload = yield* decodeJob(job);
23
21
  const content = job.outcome === "cancelled"
24
22
  ? ""
25
23
  : yield* readSettlementContent(store, payload.work);
@@ -27,14 +25,103 @@ function fanOutSettlement(store, mailRoom, job) {
27
25
  const diagnostic = job.outcome === "completed" || job.outcome === "cancelled"
28
26
  ? undefined
29
27
  : yield* readSettlementDiagnostic(store, payload.work);
30
- const settled = diagnostic === undefined
31
- ? { job, content }
32
- : { job, content, diagnostic };
33
- yield* mailRoom
34
- .fanOut(meta === undefined ? settled : { ...settled, meta })
35
- .pipe(Effect.mapError(() => runtimeFailure("Agent reply fan-out is unavailable")));
28
+ let settled = { job, content };
29
+ if (diagnostic !== undefined)
30
+ settled = { ...settled, diagnostic };
31
+ // A cancelled run names nothing; what it left open is the canceller's to see.
32
+ const waiting = job.outcome === "cancelled" ? [] : yield* waitingOnYou;
33
+ if (waiting.length > 0)
34
+ settled = { ...settled, waitingOnYou: waiting };
35
+ return meta === undefined ? settled : { ...settled, meta };
36
36
  });
37
37
  }
38
+ function decodeJob(job) {
39
+ return Effect.try({
40
+ try: () => decodeAgentJob(job),
41
+ catch: () => runtimeFailure("Agent work payload is invalid"),
42
+ });
43
+ }
44
+ function fanOut(mailRoom, settled) {
45
+ return mailRoom
46
+ .fanOut(settled)
47
+ .pipe(Effect.mapError(() => runtimeFailure("Agent reply fan-out is unavailable")));
48
+ }
49
+ /** Reply to the delegating mail that admitted this work, if it asked for one. */
50
+ function replyToMail(store, mailRoom, job) {
51
+ if (mailRoom === undefined)
52
+ return Effect.void;
53
+ return Effect.gen(function* () {
54
+ const payload = yield* decodeJob(job);
55
+ if ((payload.mail?.replyTo.length ?? 0) === 0)
56
+ return;
57
+ // The reply goes to the mail's sender: name what waits on that sender.
58
+ const waiting = Effect.try({
59
+ try: () => readWaitingOnSender(store, payload.work.sessionId, payload.work.commandFactId),
60
+ catch: () => runtimeFailure("Session attention is unavailable"),
61
+ });
62
+ yield* fanOut(mailRoom, yield* settlementFacts(store, job, waiting));
63
+ });
64
+ }
65
+ /**
66
+ * Report work no delegating mail answered to the Session's delegator of
67
+ * record. It runs after the extensions have seen the settlement, so a goal
68
+ * or loop that admitted the next turn keeps the Session busy and the stretch
69
+ * of turns reports once, when the Session finally rests. Another Agent's
70
+ * original mail is that Agent's correspondence and never reported here.
71
+ */
72
+ function reportToDelegator(store, watchdog, mailRoom, job) {
73
+ if (mailRoom === undefined)
74
+ return Effect.void;
75
+ return Effect.gen(function* () {
76
+ const payload = yield* decodeJob(job);
77
+ const mail = payload.mail;
78
+ if (mail !== undefined && mail.replyTo.length > 0)
79
+ return;
80
+ const head = yield* watchdog
81
+ .readQueueHead(job.queue)
82
+ .pipe(Effect.mapError(() => runtimeFailure("Session queue head is unavailable")));
83
+ if (head.state !== "idle")
84
+ return;
85
+ const delegator = yield* Effect.try({
86
+ try: () => readSessionDelegator(store, payload.work.sessionId),
87
+ catch: () => runtimeFailure("Session delegator is unavailable"),
88
+ });
89
+ if (delegator === undefined)
90
+ return;
91
+ const from = mail?.reply === undefined ? mail?.from : undefined;
92
+ const anotherAgentsMail = from !== undefined &&
93
+ !(from.kind === "agent" &&
94
+ delegator.replyTo.some(({ agentId }) => agentId === from.agentId));
95
+ if (anotherAgentsMail)
96
+ return;
97
+ // The report goes to the delegator: name what waits on it.
98
+ const waiting = Effect.try({
99
+ try: () => readWaitingOn(store, payload.work.sessionId, delegator.replyTo),
100
+ catch: () => runtimeFailure("Session attention is unavailable"),
101
+ });
102
+ const settled = yield* settlementFacts(store, job, waiting);
103
+ yield* fanOut(mailRoom, { ...settled, delegator });
104
+ });
105
+ }
106
+ /**
107
+ * The `core-report` settled consumer: after the extensions' turn, report each
108
+ * Session's last settled job in the page, one record at a time.
109
+ */
110
+ function reportSettled(store, watchdog, mailRoom, records) {
111
+ return Effect.forEach(records, (record, index) => Effect.gen(function* () {
112
+ const later = records
113
+ .slice(index + 1)
114
+ .some(({ job }) => job.queue === record.job.queue);
115
+ if (!later && !(yield* settledInRefusedSession(store, record.job)))
116
+ yield* reportToDelegator(store, watchdog, mailRoom, record.job);
117
+ yield* Effect.try({
118
+ try: () => {
119
+ advanceCursor(store, reportConsumer, record.sequence);
120
+ },
121
+ catch: () => runtimeFailure("Settled cursor is unavailable"),
122
+ });
123
+ }), { discard: true });
124
+ }
38
125
  function repairWorkSettlement(store, job) {
39
126
  const outcome = job.outcome;
40
127
  if (outcome !== "interrupted" &&
@@ -68,7 +155,10 @@ function settleTargetedControls(store, watchdog, job) {
68
155
  outcome,
69
156
  diagnostic: job.failureReason,
70
157
  enqueue: (work) => {
71
- watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({ work: { ...work, recovery: agentTurnRecovery } }));
158
+ watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({
159
+ work: { ...work, recovery: agentTurnRecovery },
160
+ delivery: work.delivery,
161
+ }));
72
162
  },
73
163
  });
74
164
  });
@@ -148,7 +238,7 @@ function settleCore(store, watchdog, mailRoom, records) {
148
238
  if (!(yield* settledInRefusedSession(store, record.job))) {
149
239
  yield* repairWorkSettlement(store, record.job);
150
240
  yield* settleTargetedControls(store, watchdog, record.job);
151
- yield* fanOutSettlement(store, mailRoom, record.job);
241
+ yield* replyToMail(store, mailRoom, record.job);
152
242
  }
153
243
  yield* Effect.try({
154
244
  try: () => {
@@ -172,26 +262,34 @@ export function reconcileRuntime(input) {
172
262
  try: () => readCursor(input.store, coreConsumer) ?? 0,
173
263
  catch: () => runtimeFailure("Settled cursor is unavailable"),
174
264
  });
265
+ const reportCursor = yield* Effect.try({
266
+ try: () => readCursor(input.store, reportConsumer) ?? coreCursor,
267
+ catch: () => runtimeFailure("Settled cursor is unavailable"),
268
+ });
175
269
  const consumers = input.extensions.settledConsumers();
176
- const after = Math.min(coreCursor, ...consumers.map(({ cursor }) => cursor));
270
+ const after = Math.min(coreCursor, reportCursor, ...consumers.map(({ cursor }) => cursor));
177
271
  const page = yield* input.watchdog.readSettled(pageSize, after);
178
272
  const core = yield* Effect.exit(settleCore(input.store, input.watchdog, input.mailRoom, page.filter(({ sequence }) => sequence > coreCursor)));
273
+ const processed = yield* Effect.try({
274
+ try: () => readCursor(input.store, coreConsumer) ?? 0,
275
+ catch: () => runtimeFailure("Settled cursor is unavailable"),
276
+ });
179
277
  const runs = yield* Effect.try({
180
- try: () => {
181
- const processed = readCursor(input.store, coreConsumer) ?? 0;
182
- return page
183
- .filter(({ sequence }) => sequence <= processed)
184
- .map((record) => settledRun(input.store, record));
185
- },
278
+ try: () => page
279
+ .filter(({ sequence }) => sequence <= processed)
280
+ .map((record) => settledRun(input.store, record)),
186
281
  catch: () => runtimeFailure("Settled run facts are unavailable"),
187
282
  });
188
- yield* input.extensions.deliverSettled(runs, readCursor(input.store, coreConsumer) ?? 0);
283
+ yield* input.extensions.deliverSettled(runs, processed);
284
+ const reports = yield* Effect.exit(reportSettled(input.store, input.watchdog, input.mailRoom, page.filter(({ sequence }) => sequence > reportCursor && sequence <= processed)));
189
285
  const moreTimers = yield* input.extensions.fireDueTimers(Date.now());
190
286
  if (page.length === pageSize || moreTimers)
191
287
  yield* input.requestWake;
192
288
  yield* drainMail(input.mailRoom);
193
289
  if (Exit.isFailure(core))
194
290
  return yield* Effect.failCause(core.cause);
291
+ if (Exit.isFailure(reports))
292
+ return yield* Effect.failCause(reports.cause);
195
293
  }).pipe(Effect.mapError((failure) => failure instanceof ShiitakeError
196
294
  ? failure
197
295
  : runtimeFailure("Runtime reconciliation failed")));
@@ -2,9 +2,9 @@ import * as Effect from "effect/Effect";
2
2
  import type { SessionContext } from "../client.js";
3
3
  import type { ShiitakeEffectOptions } from "./effect-contracts.js";
4
4
  /**
5
- * The Session's context use as compaction measures it, against the window of
6
- * the model a run would select now. Undefined while the Session has no
7
- * context or its context cannot be read; a window the host cannot describe is
8
- * null, never guessed.
5
+ * Read the stored figure synchronously with the Session baseline. Only model
6
+ * description remains in the returned Effect, outside the SQLite transaction
7
+ * and the request's ancestry scope. Undefined without readable context; a model
8
+ * window the host cannot describe is null, never guessed.
9
9
  */
10
- export declare function readSessionContext(options: ShiitakeEffectOptions, sessionId: string, cursor: string | null, cache?: boolean): Effect.Effect<SessionContext | undefined>;
10
+ export declare function prepareSessionContext(options: ShiitakeEffectOptions, sessionId: string, cursor: string | null, cache?: boolean): Effect.Effect<SessionContext | undefined>;