@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.
- package/README.md +59 -21
- package/dist/agent-control.d.ts +1 -1
- package/dist/agent-control.js +40 -21
- package/dist/client-visitor.d.ts +2 -0
- package/dist/client-visitor.js +6 -1
- package/dist/client-wire.d.ts +4 -0
- package/dist/client-wire.js +18 -3
- package/dist/client.d.ts +24 -4
- package/dist/client.js +22 -0
- package/dist/cloudflare.d.ts +20 -0
- package/dist/cloudflare.js +4 -1
- package/dist/delegation.d.ts +5 -0
- package/dist/delegation.js +24 -8
- package/dist/effect.d.ts +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -0
- package/dist/extensions.d.ts +1 -1
- package/dist/fleet.d.ts +10 -2
- package/dist/fleet.js +77 -19
- package/dist/http-routes.d.ts +12 -9
- package/dist/http.d.ts +1 -0
- package/dist/http.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/internal/admission-door.d.ts +1 -1
- package/dist/internal/admission-door.js +8 -4
- package/dist/internal/contracts.d.ts +5 -0
- package/dist/internal/coordinator.js +107 -50
- package/dist/internal/effect-contracts.d.ts +5 -0
- package/dist/internal/extension-runtime.js +5 -3
- package/dist/internal/promise-facade.js +2 -0
- package/dist/internal/run-scope.js +27 -7
- package/dist/internal/runtime-coordinator.d.ts +14 -0
- package/dist/internal/runtime-coordinator.js +52 -10
- package/dist/internal/runtime-core.d.ts +3 -0
- package/dist/internal/runtime-core.js +1 -0
- package/dist/internal/runtime-reconciliation.js +4 -1
- package/dist/internal/session-context.d.ts +5 -5
- package/dist/internal/session-context.js +80 -83
- package/dist/internal/session-status.d.ts +2 -2
- package/dist/internal/session-status.js +5 -3
- package/dist/internal/woodstock/client-projection.js +5 -11
- package/dist/internal/woodstock/fleet-store.d.ts +3 -2
- package/dist/internal/woodstock/fleet-store.js +24 -4
- package/dist/internal/woodstock/index.d.ts +3 -3
- package/dist/internal/woodstock/index.js +16 -6
- package/dist/internal/woodstock/progress.d.ts +11 -1
- package/dist/internal/woodstock/progress.js +126 -25
- package/dist/internal/woodstock/session-delegator.d.ts +2 -1
- package/dist/internal/woodstock/session-store.d.ts +14 -2
- package/dist/internal/woodstock/session-store.js +329 -66
- package/dist/node.js +11 -2
- package/dist/server-visitor-stream.js +33 -24
- package/docs/api/agent-control.md +3 -2
- package/docs/api/client.md +77 -8
- package/docs/api/cloudflare.md +16 -0
- package/docs/api/delegation.md +84 -50
- package/docs/api/effect.md +85 -7
- package/docs/api/extensions.md +8 -8
- package/docs/api/fleet.md +87 -27
- package/docs/api/http.md +28 -0
- package/docs/api/index.md +39 -15
- package/docs/api/testing.md +9 -7
- 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 === "
|
|
344
|
-
? "
|
|
345
|
-
: "
|
|
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
|
|
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
|
|
151
|
+
admitNew,
|
|
125
152
|
hostMeta: options.meta,
|
|
126
153
|
contract,
|
|
127
154
|
enqueue: (work, mail) => {
|
|
128
|
-
const input = {
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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,
|
|
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({
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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:
|
|
80
|
+
value: undefined,
|
|
101
81
|
});
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
/**
|
|
4
|
+
/** Activity facts plus an explicit, on-demand bounded progress read. */
|
|
5
5
|
export type ObservedActivity = Readonly<{
|
|
6
6
|
facts: SessionActivity;
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
31
|
-
|
|
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;
|