@fungi.computer/shiitake 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +1 -1
  3. package/dist/agent-control.js +40 -21
  4. package/dist/client-visitor.d.ts +2 -0
  5. package/dist/client-visitor.js +6 -1
  6. package/dist/client-wire.d.ts +4 -0
  7. package/dist/client-wire.js +18 -3
  8. package/dist/client.d.ts +24 -4
  9. package/dist/client.js +22 -0
  10. package/dist/cloudflare.d.ts +20 -0
  11. package/dist/cloudflare.js +4 -1
  12. package/dist/delegation.d.ts +5 -0
  13. package/dist/delegation.js +24 -8
  14. package/dist/effect.d.ts +1 -1
  15. package/dist/errors.d.ts +2 -2
  16. package/dist/errors.js +2 -0
  17. package/dist/extensions.d.ts +1 -1
  18. package/dist/fleet.d.ts +10 -2
  19. package/dist/fleet.js +77 -19
  20. package/dist/http-routes.d.ts +12 -9
  21. package/dist/http.d.ts +1 -0
  22. package/dist/http.js +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/internal/admission-door.d.ts +1 -1
  25. package/dist/internal/admission-door.js +8 -4
  26. package/dist/internal/contracts.d.ts +5 -0
  27. package/dist/internal/coordinator.js +107 -50
  28. package/dist/internal/effect-contracts.d.ts +5 -0
  29. package/dist/internal/extension-runtime.js +5 -3
  30. package/dist/internal/promise-facade.js +2 -0
  31. package/dist/internal/run-scope.js +27 -7
  32. package/dist/internal/runtime-coordinator.d.ts +14 -0
  33. package/dist/internal/runtime-coordinator.js +52 -10
  34. package/dist/internal/runtime-core.d.ts +3 -0
  35. package/dist/internal/runtime-core.js +1 -0
  36. package/dist/internal/runtime-reconciliation.js +4 -1
  37. package/dist/internal/session-context.d.ts +5 -5
  38. package/dist/internal/session-context.js +80 -83
  39. package/dist/internal/session-status.d.ts +2 -2
  40. package/dist/internal/session-status.js +5 -3
  41. package/dist/internal/woodstock/client-projection.js +5 -11
  42. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  43. package/dist/internal/woodstock/fleet-store.js +24 -4
  44. package/dist/internal/woodstock/index.d.ts +3 -3
  45. package/dist/internal/woodstock/index.js +16 -6
  46. package/dist/internal/woodstock/progress.d.ts +11 -1
  47. package/dist/internal/woodstock/progress.js +126 -25
  48. package/dist/internal/woodstock/session-delegator.d.ts +2 -1
  49. package/dist/internal/woodstock/session-store.d.ts +14 -2
  50. package/dist/internal/woodstock/session-store.js +329 -66
  51. package/dist/node.js +11 -2
  52. package/dist/server-visitor-stream.js +33 -24
  53. package/docs/api/agent-control.md +3 -2
  54. package/docs/api/client.md +77 -8
  55. package/docs/api/cloudflare.md +16 -0
  56. package/docs/api/delegation.md +84 -50
  57. package/docs/api/effect.md +85 -7
  58. package/docs/api/extensions.md +8 -8
  59. package/docs/api/fleet.md +87 -27
  60. package/docs/api/http.md +28 -0
  61. package/docs/api/index.md +39 -15
  62. package/docs/api/testing.md +9 -7
  63. package/package.json +5 -5
@@ -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
@@ -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;
@@ -1,15 +1,30 @@
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. 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
+ });
27
+ }
13
28
  const toolSetupMessages = {
14
29
  missing: "Instruction capabilities have no tool runtime",
15
30
  runtime: "Tool runtime failed",
@@ -119,12 +134,12 @@ function acquiredTools(options, sessionId, workId, meta, instructions, extension
119
134
  sessionGrants: () => readSessionGrants(options.store, sessionId)?.grants ?? {},
120
135
  },
121
136
  };
137
+ const toolScope = yield* Scope.make();
138
+ yield* Effect.addFinalizer(() => boundedRunFinalizer("tools", workId, Scope.close(toolScope, Exit.succeed(undefined))));
122
139
  const runtime = yield* makeMycelium(toolSource.execution === undefined
123
140
  ? 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")));
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")));
128
143
  return [lease.executeTool, ...lease.tools];
129
144
  });
130
145
  }
@@ -137,11 +152,16 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
137
152
  });
138
153
  const releaseInstructions = instructions.release;
139
154
  if (releaseInstructions !== undefined)
140
- yield* Effect.addFinalizer(() => Effect.sync(releaseInstructions));
141
- 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({
142
158
  sessionId: prepared.command.sessionId,
143
159
  preference: prepared.modelPreference,
144
- }));
160
+ })
161
+ .pipe(Effect.map((model) => ({
162
+ ...model,
163
+ release: boundedRunFinalizer("model", prepared.command.entry.workId, model.release),
164
+ }))));
145
165
  const tools = yield* acquiredTools(options, prepared.command.sessionId, prepared.command.entry.workId, prepared.command.entry.meta, instructions, extensionModules, extensionFileTools);
146
166
  const accepted = Object.freeze({
147
167
  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,7 @@ 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";
@@ -11,7 +12,7 @@ import { openCompactionSettings } from "./compaction/settings.js";
11
12
  import { admitExtensionRosterV0, } from "./extension-roster.js";
12
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),
@@ -248,11 +278,20 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
248
278
  };
249
279
  }),
250
280
  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))),
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))),
256
295
  extensions,
257
296
  commit: bound.commit,
258
297
  assertOpen: Effect.suspend(() => lifecycleState === "open"
@@ -260,11 +299,14 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
260
299
  : Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"))),
261
300
  beginClose,
262
301
  door,
263
- 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
+ },
264
306
  stageSessionGrantsWithinTransaction: (sessionId, delivery) => {
265
307
  deliverSessionGrants(options.store, sessionId, delivery);
266
308
  },
267
- storage,
309
+ storage: { admitNew },
268
310
  recordAcceptedCancellation: ({ jobId, cancellationId, onlyIfPending }) => watchdog.transactional.recordCancellationIntent({
269
311
  jobId,
270
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)
@@ -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>;
@@ -14,103 +14,100 @@ function settledContextTokens(store, sessionId) {
14
14
  return messages.length === 0 ? null : estimateContextTokens(messages).tokens;
15
15
  }
16
16
  /**
17
- * The Session's context use as compaction measures it, against the window of
18
- * the model a run would select now. Undefined while the Session has no
19
- * context or its context cannot be read; a window the host cannot describe is
20
- * null, never guessed.
17
+ * Read the stored figure synchronously with the Session baseline. Only model
18
+ * description remains in the returned Effect, outside the SQLite transaction
19
+ * and the request's ancestry scope. Undefined without readable context; a model
20
+ * window the host cannot describe is null, never guessed.
21
21
  */
22
- export function readSessionContext(options, sessionId, cursor, cache = true) {
23
- return Effect.gen(function* () {
24
- const savedPreference = yield* Effect.result(Effect.try({
25
- try: () => readSessionModelPreference(options.store, sessionId),
26
- catch: (error) => error,
27
- }));
28
- if (savedPreference._tag === "Failure") {
29
- reportCatchFailure("session-context.preference", savedPreference.failure);
30
- return undefined;
31
- }
32
- const preference = savedPreference.success;
33
- const preferenceKey = JSON.stringify(preference);
34
- const ownerCache = contextByOwner.get(options) ?? new Map();
35
- contextByOwner.set(options, ownerCache);
36
- const previous = cache ? ownerCache.get(sessionId) : undefined;
37
- const describe = options.model.describe;
38
- if (previous?.cursor === cursor && previous.preference === preferenceKey) {
39
- if (previous.value === undefined || describe === undefined)
40
- return previous.value;
41
- const previousValue = previous.value;
42
- // A null Session override follows a mutable Agent default. Check the
43
- // selected window without decoding history again; a changed window is
44
- // a distinct effective preference for this cursor.
45
- return yield* describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => {
46
- if (previousValue.window === contextWindow)
47
- return previousValue;
48
- const value = Object.freeze({
49
- tokens: previousValue.tokens,
50
- window: contextWindow,
51
- });
52
- if (cache && ownerCache.get(sessionId)?.cursor === cursor)
53
- ownerCache.set(sessionId, {
54
- cursor,
55
- preference: preferenceKey,
56
- value,
57
- });
58
- return value;
59
- }), Effect.catch((error) => Effect.sync(() => {
60
- reportCatchFailure("session-context.describe", error);
22
+ export function prepareSessionContext(options, sessionId, cursor, cache = true) {
23
+ let preference;
24
+ try {
25
+ preference = readSessionModelPreference(options.store, sessionId);
26
+ }
27
+ catch (error) {
28
+ reportCatchFailure("session-context.preference", error);
29
+ return Effect.succeed(undefined);
30
+ }
31
+ const preferenceKey = JSON.stringify(preference);
32
+ const ownerCache = contextByOwner.get(options) ?? new Map();
33
+ contextByOwner.set(options, ownerCache);
34
+ const previous = cache ? ownerCache.get(sessionId) : undefined;
35
+ const describe = options.model.describe;
36
+ if (previous?.cursor === cursor && previous.preference === preferenceKey) {
37
+ if (previous.value === undefined || describe === undefined)
38
+ return Effect.succeed(previous.value);
39
+ const previousValue = previous.value;
40
+ // A null Session override follows a mutable Agent default. Check the
41
+ // selected window without decoding history again; a changed window is
42
+ // a distinct effective preference for this cursor.
43
+ return describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => {
44
+ if (previousValue.window === contextWindow)
61
45
  return previousValue;
62
- })));
63
- }
64
- // At most one cached whole-window read per Session, cursor, and
65
- // preference: a late describe may publish only while this cursor still
66
- // owns the cache entry. Reads before the tip bypass the cache and each read
67
- // the whole window. A cold-cache connection also reads the whole kept
68
- // window.
69
- const contextRead = yield* Effect.result(Effect.try({
70
- try: () => settledContextTokens(options.store, sessionId),
71
- catch: (error) => error,
72
- }));
73
- if (contextRead._tag === "Failure") {
74
- reportCatchFailure("session-context.figure", contextRead.failure);
75
- return undefined;
76
- }
77
- const tokens = contextRead.success;
78
- if (tokens === null) {
79
- if (cache)
46
+ const value = Object.freeze({
47
+ tokens: previousValue.tokens,
48
+ window: contextWindow,
49
+ });
50
+ if (cache && ownerCache.get(sessionId)?.cursor === cursor)
80
51
  ownerCache.set(sessionId, {
81
52
  cursor,
82
53
  preference: preferenceKey,
83
- value: undefined,
54
+ value,
84
55
  });
85
- return undefined;
86
- }
87
- if (describe === undefined) {
88
- const value = Object.freeze({ tokens, window: null });
89
- if (cache)
90
- ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
91
56
  return value;
92
- }
93
- // Publish the decoded figure before yielding to the host's model
94
- // description. Concurrent watchers then reuse these tokens rather than
95
- // decoding the same Session window while `describe` is in flight.
57
+ }), Effect.catch((error) => Effect.sync(() => {
58
+ reportCatchFailure("session-context.describe", error);
59
+ return previousValue;
60
+ })));
61
+ }
62
+ // At most one cached whole-window read per Session, cursor, and
63
+ // preference: a late describe may publish only while this cursor still
64
+ // owns the cache entry. Reads before the tip bypass the cache and each read
65
+ // the whole window. A cold-cache connection also reads the whole kept
66
+ // window.
67
+ let tokens;
68
+ try {
69
+ tokens = settledContextTokens(options.store, sessionId);
70
+ }
71
+ catch (error) {
72
+ reportCatchFailure("session-context.figure", error);
73
+ return Effect.succeed(undefined);
74
+ }
75
+ if (tokens === null) {
96
76
  if (cache)
97
77
  ownerCache.set(sessionId, {
98
78
  cursor,
99
79
  preference: preferenceKey,
100
- value: Object.freeze({ tokens, window: null }),
80
+ value: undefined,
101
81
  });
102
- // A null Session override follows a mutable Agent default. Resolve through
103
- // the host's turn-model selector on every read, without acquiring a turn.
104
- const window = yield* describe({
105
- sessionId,
106
- preference,
107
- }).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
108
- reportCatchFailure("session-context.describe", error);
109
- return null;
110
- })));
82
+ return Effect.succeed(undefined);
83
+ }
84
+ if (describe === undefined) {
85
+ const value = Object.freeze({ tokens, window: null });
86
+ if (cache)
87
+ ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
88
+ return Effect.succeed(value);
89
+ }
90
+ // Publish the decoded figure before yielding to the host's model
91
+ // description. Concurrent watchers then reuse these tokens rather than
92
+ // decoding the same Session window while `describe` is in flight.
93
+ if (cache)
94
+ ownerCache.set(sessionId, {
95
+ cursor,
96
+ preference: preferenceKey,
97
+ value: Object.freeze({ tokens, window: null }),
98
+ });
99
+ // A null Session override follows a mutable Agent default. Resolve through
100
+ // the host's turn-model selector on every read, without acquiring a turn.
101
+ return describe({
102
+ sessionId,
103
+ preference,
104
+ }).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
105
+ reportCatchFailure("session-context.describe", error);
106
+ return null;
107
+ })), Effect.map((window) => {
111
108
  const value = Object.freeze({ tokens, window });
112
109
  if (cache && ownerCache.get(sessionId)?.cursor === cursor)
113
110
  ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
114
111
  return value;
115
- });
112
+ }));
116
113
  }
@@ -1,10 +1,10 @@
1
1
  import type { WatchdogQueueHead } from "@fungi.computer/watchdog/effect";
2
2
  import type { AgentRunningWork, SessionActivity, SessionProgress, SessionToolOutput } from "../client.js";
3
3
  import type { SqliteOwner } from "./contracts.js";
4
- /** Public activity facts plus the process-local progress read with them. */
4
+ /** Activity facts plus an explicit, on-demand bounded progress read. */
5
5
  export type ObservedActivity = Readonly<{
6
6
  facts: SessionActivity;
7
- progress: SessionProgress | null;
7
+ readProgress: () => SessionProgress | null;
8
8
  toolOutput: readonly SessionToolOutput[];
9
9
  }>;
10
10
  /** Join Watchdog authority with Woodstock facts and process-local progress. */
@@ -12,7 +12,7 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
12
12
  lastModelChoiceSequence,
13
13
  compaction: null,
14
14
  },
15
- progress: null,
15
+ readProgress: () => null,
16
16
  toolOutput: [],
17
17
  };
18
18
  if (head.state === "queued")
@@ -27,7 +27,7 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
27
27
  lastModelChoiceSequence,
28
28
  compaction: null,
29
29
  },
30
- progress: null,
30
+ readProgress: () => null,
31
31
  toolOutput: [],
32
32
  };
33
33
  const latestRetry = readLatestLiveRetryForWork(owner, sessionId, head.jobId);
@@ -49,7 +49,9 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
49
49
  lastModelChoiceSequence,
50
50
  compaction: readProcessCompaction(owner, sessionId),
51
51
  },
52
- progress: readProcessProgressForWork(owner, sessionId, head.jobId),
52
+ // Only baselines need a flattened bounded tail. Live subscribers read
53
+ // appends directly and must not materialize it on every token.
54
+ readProgress: () => readProcessProgressForWork(owner, sessionId, head.jobId),
53
55
  toolOutput: readProcessToolOutputForWork(owner, sessionId, head.jobId),
54
56
  };
55
57
  }
@@ -5,7 +5,7 @@ import { readSessionDelegator } from "./session-delegator.js";
5
5
  import { ShiitakeError } from "../../errors.js";
6
6
  import { decodeWoodstockCursor, encodeWoodstockCursor } from "./cursor.js";
7
7
  import { operatorClientMessage, promptCommand } from "./session-entry.js";
8
- import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, hasSessionWorkTerminal, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, } from "./session-store.js";
8
+ import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, hasSessionWorkTerminal, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, withSessionReadTransaction, } from "./session-store.js";
9
9
  const maximumPagePayloadBytes = 2_097_152;
10
10
  const replayPageLimit = 200;
11
11
  const maximumRowsPerPage = lineageRowLimit;
@@ -344,7 +344,7 @@ function resolveHistoryAnchor(owner, lineage, before) {
344
344
  * history, including what compaction summarized away.
345
345
  */
346
346
  export function readClientSnapshot(owner, sessionId, options, activity, projectExtensions = () => ({})) {
347
- return owner.transactionSync(() => {
347
+ return withSessionReadTransaction(owner, sessionId, () => {
348
348
  const pending = readPendingPrompts(owner, sessionId);
349
349
  const lineage = readSessionLineage(owner, { sessionId });
350
350
  if (lineage === undefined)
@@ -359,7 +359,7 @@ export function readClientSnapshot(owner, sessionId, options, activity, projectE
359
359
  compactionNotices: Object.freeze([]),
360
360
  pending,
361
361
  activity: activity.facts,
362
- progress: activity.progress,
362
+ progress: activity.readProgress(),
363
363
  toolOutput: activity.toolOutput,
364
364
  extensions: projectExtensions(),
365
365
  attention: readSessionAttention(owner, sessionId),
@@ -394,7 +394,7 @@ export function readClientSnapshot(owner, sessionId, options, activity, projectE
394
394
  compactionNotices: compactionNoticesIn(consumedRows),
395
395
  pending,
396
396
  activity: activity.facts,
397
- progress: options.before === undefined ? activity.progress : null,
397
+ progress: options.before === undefined ? activity.readProgress() : null,
398
398
  toolOutput: options.before === undefined ? activity.toolOutput : [],
399
399
  extensions: projectExtensions(),
400
400
  attention: readSessionAttention(owner, sessionId),
@@ -442,12 +442,6 @@ function replayFrames(owner, lineage, start, delivery, activity, extensions, att
442
442
  ? { ...frame, sandboxFailures: page.sandboxFailures }
443
443
  : frame),
444
444
  ];
445
- if (delivery === "live" && !page.hasMore && activity.progress !== null)
446
- frames.push(Object.freeze({
447
- kind: "progress",
448
- progress: activity.progress,
449
- activity: activity.facts,
450
- }));
451
445
  return Object.freeze({
452
446
  kind: "page",
453
447
  frames: Object.freeze(!page.hasMore
@@ -468,7 +462,7 @@ export function readClientReplayPage(owner, sessionId, after, delivery, activity
468
462
  const position = after === undefined ? undefined : decodeWoodstockCursor(after);
469
463
  if (after !== undefined && position === undefined)
470
464
  return resetReplayPage;
471
- return owner.transactionSync(() => {
465
+ return withSessionReadTransaction(owner, sessionId, () => {
472
466
  const pending = readPendingPrompts(owner, sessionId);
473
467
  const lineage = readSessionLineage(owner, { sessionId });
474
468
  if (lineage === undefined) {
@@ -27,10 +27,11 @@ export declare function createFleetDelegationStore(storage: WatchdogSqliteOwner)
27
27
  read: (requestId: string) => {
28
28
  readonly [x: string]: string | number | ArrayBuffer | null;
29
29
  } | undefined;
30
- /** Every recipient-accepted delegation, newest first. */
31
- sent: () => readonly {
30
+ /** Accepted sends not yet observed settled, for owner-local archive admission. */
31
+ unsettled: () => readonly {
32
32
  readonly [x: string]: string | number | ArrayBuffer | null;
33
33
  }[];
34
+ recordSettled(requestId: string): void;
34
35
  reserve(requestId: string, input: string, mail: string): void;
35
36
  recordReceipt(requestId: string, receipt: string, expectedMail: string, acceptedMail: string): void;
36
37
  recordHeld(requestId: string): void;