@fungi.computer/shiitake 0.6.0 → 0.8.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 (67) 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 +26 -15
  8. package/dist/client.d.ts +23 -11
  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 +14 -2
  19. package/dist/fleet.js +81 -19
  20. package/dist/http-routes.d.ts +12 -16
  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/observation-kernel.js +12 -3
  31. package/dist/internal/promise-facade.js +2 -0
  32. package/dist/internal/run-scope.d.ts +1 -0
  33. package/dist/internal/run-scope.js +29 -7
  34. package/dist/internal/runtime-coordinator.d.ts +14 -0
  35. package/dist/internal/runtime-coordinator.js +57 -10
  36. package/dist/internal/runtime-core.d.ts +3 -0
  37. package/dist/internal/runtime-core.js +1 -0
  38. package/dist/internal/runtime-reconciliation.js +4 -1
  39. package/dist/internal/session-context.d.ts +5 -5
  40. package/dist/internal/session-context.js +80 -83
  41. package/dist/internal/session-status.d.ts +4 -2
  42. package/dist/internal/session-status.js +22 -4
  43. package/dist/internal/woodstock/client-projection.js +5 -11
  44. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  45. package/dist/internal/woodstock/fleet-store.js +24 -4
  46. package/dist/internal/woodstock/index.d.ts +3 -3
  47. package/dist/internal/woodstock/index.js +18 -7
  48. package/dist/internal/woodstock/progress.d.ts +13 -1
  49. package/dist/internal/woodstock/progress.js +130 -24
  50. package/dist/internal/woodstock/session-delegator.d.ts +2 -1
  51. package/dist/internal/woodstock/session-store.d.ts +14 -2
  52. package/dist/internal/woodstock/session-store.js +329 -66
  53. package/dist/node.js +11 -2
  54. package/dist/server-visitor-stream.js +33 -24
  55. package/dist/testing.d.ts +2 -0
  56. package/dist/testing.js +2 -0
  57. package/docs/api/agent-control.md +3 -2
  58. package/docs/api/client.md +79 -19
  59. package/docs/api/cloudflare.md +16 -0
  60. package/docs/api/delegation.md +84 -50
  61. package/docs/api/effect.md +85 -7
  62. package/docs/api/extensions.md +8 -8
  63. package/docs/api/fleet.md +118 -29
  64. package/docs/api/http.md +28 -0
  65. package/docs/api/index.md +39 -15
  66. package/docs/api/testing.md +20 -7
  67. package/package.json +5 -5
@@ -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,
@@ -101,6 +101,11 @@ export type ShiitakeEffectOptions = Readonly<{
101
101
  store: SqliteOwner;
102
102
  /** Host storage-pressure check; absent never refuses for storage pressure. */
103
103
  storagePressure?: StoragePressureCheck;
104
+ /** Host-owned synchronous new-work admission at the common door. */
105
+ admission?: Readonly<{
106
+ /** Check the host's current flag synchronously inside admission's transaction. */
107
+ assertOpen: () => void;
108
+ }>;
104
109
  /** Compaction budgets; absent uses Pi's defaults for every model. */
105
110
  compaction?: CompactionOptions;
106
111
  /** One host lane in Shiitake's existing Watchdog, opened before recovery. */
@@ -340,9 +340,11 @@ export function openExtensionRuntime(input) {
340
340
  reason: error instanceof ShiitakeError
341
341
  ? error.cause === "conflict"
342
342
  ? "conflict"
343
- : error.cause === "closed"
344
- ? "closed"
345
- : "invalid"
343
+ : error.cause === "archived"
344
+ ? "archived"
345
+ : error.cause === "closed"
346
+ ? "closed"
347
+ : "invalid"
346
348
  : "invalid",
347
349
  message: error instanceof ShiitakeError
348
350
  ? error.message
@@ -57,6 +57,14 @@ function deliverObservation(listener, reason) {
57
57
  listener.reset = true;
58
58
  }
59
59
  }
60
+ /** Queued views reconcile occupancy without replaying another Session's facts. */
61
+ function notifyQueuedViews(listeners, sourceSessionId) {
62
+ for (const listener of listeners)
63
+ if (listener.queued &&
64
+ !listener.settlementOnly &&
65
+ listener.sessionId !== sourceSessionId)
66
+ deliverObservation(listener, "activity");
67
+ }
60
68
  /** Notify observers of one Session's durable or process-local change. */
61
69
  export function publishOwnerObservation(store, sessionId, reason = "durable") {
62
70
  const publishers = ownerPublishers.get(store);
@@ -131,11 +139,12 @@ export function openObservationKernel(store) {
131
139
  };
132
140
  const publish = (publication) => {
133
141
  if (publication.kind === "agent-execution") {
134
- for (const listener of active)
135
- if (listener.queued && !listener.settlementOnly)
136
- deliverObservation(listener, "activity");
142
+ notifyQueuedViews(active, null);
137
143
  return;
138
144
  }
145
+ // Reuse bounded activity hints for the occupant's changing phase.
146
+ if (publication.sessionId !== null && publication.reason !== "settlement")
147
+ notifyQueuedViews(active, publication.sessionId);
139
148
  const listeners = publication.sessionId === null
140
149
  ? active
141
150
  : sessions.get(publication.sessionId);
@@ -139,6 +139,8 @@ function liftOptions(options, runtime) {
139
139
  Object.assign(base, { meta: options.meta });
140
140
  if (options.storagePressure !== undefined)
141
141
  Object.assign(base, { storagePressure: options.storagePressure });
142
+ if (options.admission !== undefined)
143
+ Object.assign(base, { admission: options.admission });
142
144
  if (options.compaction !== undefined)
143
145
  Object.assign(base, { compaction: options.compaction });
144
146
  const toolSource = options.tools;
@@ -7,6 +7,7 @@ import type { ShiitakeEffectOptions } from "./effect-contracts.js";
7
7
  import type { CompactionHandoffV0, PromptSectionV0 } from "../extensions.js";
8
8
  import { type ManualCompactionOutcome } from "./compaction/session-compaction.js";
9
9
  import { type PreparedSessionRun, type RunContract } from "./woodstock/index.js";
10
+ export declare const runFinalizerGraceMs = 1000;
10
11
  declare const toolSetupMessages: {
11
12
  readonly missing: "Instruction capabilities have no tool runtime";
12
13
  readonly runtime: "Tool runtime failed";
@@ -1,15 +1,32 @@
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";
15
+ // A run writes its terminal before scope release. Allow one second per foreign
16
+ // release before detaching it: the held-model regression exercises the full
17
+ // grace and proves Watchdog can settle and claim the next priority-1 steer.
18
+ // Keep the test's eventual queue-idle budget tied to this product bound.
19
+ export const runFinalizerGraceMs = 1_000;
20
+ function boundedRunFinalizer(name, workId, release) {
21
+ return Effect.gen(function* () {
22
+ // A detached release may finish later; interrupting a finalizer itself
23
+ // cannot bound an uninterruptible scope.
24
+ const fiber = yield* Effect.forkDetach(release);
25
+ const finished = yield* Effect.raceFirst(Fiber.join(fiber).pipe(Effect.as(true)), Effect.sleep(runFinalizerGraceMs).pipe(Effect.as(false)));
26
+ if (!finished)
27
+ yield* Effect.logError("shiitake_run_finalizer_hung").pipe(Effect.annotateLogs({ workId, finalizer: name }));
28
+ });
29
+ }
13
30
  const toolSetupMessages = {
14
31
  missing: "Instruction capabilities have no tool runtime",
15
32
  runtime: "Tool runtime failed",
@@ -119,12 +136,12 @@ function acquiredTools(options, sessionId, workId, meta, instructions, extension
119
136
  sessionGrants: () => readSessionGrants(options.store, sessionId)?.grants ?? {},
120
137
  },
121
138
  };
139
+ const toolScope = yield* Scope.make();
140
+ yield* Effect.addFinalizer(() => boundedRunFinalizer("tools", workId, Scope.close(toolScope, Exit.succeed(undefined))));
122
141
  const runtime = yield* makeMycelium(toolSource.execution === undefined
123
142
  ? runtimeOptions
124
- : { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.mapError(() => new ToolSetupError("runtime")));
125
- const lease = yield* runtime
126
- .acquire()
127
- .pipe(Effect.mapError(() => new ToolSetupError("lease")));
143
+ : { ...runtimeOptions, execution: toolSource.execution }).pipe(Effect.provideService(Scope.Scope, toolScope), Effect.mapError(() => new ToolSetupError("runtime")));
144
+ const lease = yield* runtime.acquire().pipe(Effect.provideService(Scope.Scope, toolScope), Effect.mapError(() => new ToolSetupError("lease")));
128
145
  return [lease.executeTool, ...lease.tools];
129
146
  });
130
147
  }
@@ -137,11 +154,16 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
137
154
  });
138
155
  const releaseInstructions = instructions.release;
139
156
  if (releaseInstructions !== undefined)
140
- yield* Effect.addFinalizer(() => Effect.sync(releaseInstructions));
141
- const acquired = yield* acquireOwnedModel(options.model.acquire({
157
+ yield* Effect.addFinalizer(() => boundedRunFinalizer("instructions", prepared.command.entry.workId, Effect.sync(releaseInstructions)));
158
+ const acquired = yield* acquireOwnedModel(options.model
159
+ .acquire({
142
160
  sessionId: prepared.command.sessionId,
143
161
  preference: prepared.modelPreference,
144
- }));
162
+ })
163
+ .pipe(Effect.map((model) => ({
164
+ ...model,
165
+ release: boundedRunFinalizer("model", prepared.command.entry.workId, model.release),
166
+ }))));
145
167
  const tools = yield* acquiredTools(options, prepared.command.sessionId, prepared.command.entry.workId, prepared.command.entry.meta, instructions, extensionModules, extensionFileTools);
146
168
  const accepted = Object.freeze({
147
169
  sessionId: prepared.command.sessionId,
@@ -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,6 +3,8 @@ 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 { readSessionPhase } from "./session-status.js";
7
+ import { readOutstandingDelegationSync } from "../fleet.js";
6
8
  import { publishAgentExecutionChange } from "./observation-kernel.js";
7
9
  import { ShiitakeError } from "../errors.js";
8
10
  import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.js";
@@ -11,7 +13,7 @@ import { openCompactionSettings } from "./compaction/settings.js";
11
13
  import { admitExtensionRosterV0, } from "./extension-roster.js";
12
14
  import { coreConsumer, reportConsumer, openExtensionRuntime, } from "./extension-runtime.js";
13
15
  import { reconcileRuntime } from "./runtime-reconciliation.js";
14
- import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
16
+ import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, recordSessionDelegator, projectSessionRun, openStorageAdmission, readCursor, readSessionInboxPage, } from "./woodstock/index.js";
15
17
  function runtimeFailure(message) {
16
18
  return new ShiitakeError("runtime", "rejected", message);
17
19
  }
@@ -119,13 +121,42 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
119
121
  ? error
120
122
  : runtimeFailure("Storage budget is unavailable"),
121
123
  });
124
+ const admitNew = () => {
125
+ options.admission?.assertOpen();
126
+ storage.admitNew();
127
+ };
128
+ const busySync = () => {
129
+ let after;
130
+ do {
131
+ const page = readSessionInboxPage(options.store, after);
132
+ for (const session of page.sessions) {
133
+ if (session.state === "unavailable")
134
+ throw new ShiitakeError("store", "unavailable", `Session ${session.sessionId} is unreadable`);
135
+ const head = watchdog.transactional.readQueueHead(session.sessionId);
136
+ if (head.state !== "idle")
137
+ return {
138
+ kind: "session",
139
+ sessionId: session.sessionId,
140
+ commandId: head.jobId,
141
+ };
142
+ }
143
+ after = page.next;
144
+ } while (after !== undefined);
145
+ const outstanding = readOutstandingDelegationSync(options.store);
146
+ return outstanding === undefined
147
+ ? undefined
148
+ : { kind: "delegation", ...outstanding };
149
+ };
122
150
  const door = openAdmissionDoor({
123
151
  owner: options.store,
124
- admitNew: storage.admitNew,
152
+ admitNew,
125
153
  hostMeta: options.meta,
126
154
  contract,
127
155
  enqueue: (work, mail) => {
128
- const input = { work: { ...work, recovery: agentTurnRecovery } };
156
+ const input = {
157
+ work: { ...work, recovery: agentTurnRecovery },
158
+ delivery: work.delivery,
159
+ };
129
160
  watchdog.transactional.enqueueAcceptedJob(agentJobFromInput(mail === undefined ? input : { ...input, mail }));
130
161
  },
131
162
  readQueueHead: (queue) => watchdog.transactional.readQueueHead(queue),
@@ -248,11 +279,24 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
248
279
  };
249
280
  }),
250
281
  activeSessions: Effect.sync(() => [...activeSessions]),
251
- runningWork: (jobId) => watchdog.read(jobId).pipe(Effect.map((result) => result.status === "found"
252
- ? (runningSlots.get(result.job.lane === options.hostJobs?.lane
253
- ? "host"
254
- : "serial") ?? null)
255
- : null), Effect.mapError((cause) => new ShiitakeError("runtime", "rejected", "Running work is unavailable", undefined, cause))),
282
+ busySync,
283
+ runningWork: (jobId) => watchdog.read(jobId).pipe(Effect.flatMap((result) => {
284
+ const work = result.status === "found"
285
+ ? runningSlots.get(result.job.lane === options.hostJobs?.lane
286
+ ? "host"
287
+ : "serial")
288
+ : undefined;
289
+ if (work === undefined)
290
+ return Effect.succeed(null);
291
+ if (work.kind === "host")
292
+ return Effect.succeed({ phase: "thinking" });
293
+ return Effect.try({
294
+ try: () => ({
295
+ phase: readSessionPhase(options.store, work.sessionId, watchdog.transactional.readQueueHead(work.sessionId)),
296
+ }),
297
+ catch: (cause) => new ShiitakeError("runtime", "rejected", "Running phase is unavailable", undefined, cause),
298
+ });
299
+ }), Effect.mapError((cause) => new ShiitakeError("runtime", "rejected", "Running work is unavailable", undefined, cause))),
256
300
  extensions,
257
301
  commit: bound.commit,
258
302
  assertOpen: Effect.suspend(() => lifecycleState === "open"
@@ -260,11 +304,14 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
260
304
  : Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"))),
261
305
  beginClose,
262
306
  door,
263
- createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId, storage.admitNew),
307
+ createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId, admitNew),
308
+ recordCreatedSessionDelegatorWithinTransaction: (sessionId, mail) => {
309
+ recordSessionDelegator(options.store, sessionId, mail);
310
+ },
264
311
  stageSessionGrantsWithinTransaction: (sessionId, delivery) => {
265
312
  deliverSessionGrants(options.store, sessionId, delivery);
266
313
  },
267
- storage,
314
+ storage: { admitNew },
268
315
  recordAcceptedCancellation: ({ jobId, cancellationId, onlyIfPending }) => watchdog.transactional.recordCancellationIntent({
269
316
  jobId,
270
317
  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)
@@ -155,7 +155,10 @@ function settleTargetedControls(store, watchdog, job) {
155
155
  outcome,
156
156
  diagnostic: job.failureReason,
157
157
  enqueue: (work) => {
158
- watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({ work: { ...work, recovery: agentTurnRecovery } }));
158
+ watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({
159
+ work: { ...work, recovery: agentTurnRecovery },
160
+ delivery: work.delivery,
161
+ }));
159
162
  },
160
163
  });
161
164
  });
@@ -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>;