@fungi.computer/shiitake 0.8.0 → 0.9.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 +28 -0
- package/dist/client-wire.js +1 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +1 -0
- package/dist/delegation.d.ts +4 -4
- package/dist/http-routes.d.ts +3 -3
- package/dist/http-routes.js +2 -1
- package/dist/internal/coordinator.d.ts +2 -1
- package/dist/internal/coordinator.js +10 -218
- package/dist/internal/observation-kernel.d.ts +21 -0
- package/dist/internal/observation-kernel.js +128 -1
- package/dist/internal/runtime-core.js +2 -1
- package/dist/internal/runtime-reconciliation.js +5 -2
- package/dist/internal/session-observation.d.ts +37 -0
- package/dist/internal/session-observation.js +440 -0
- package/dist/internal/woodstock/client-projection.d.ts +3 -1
- package/dist/internal/woodstock/client-projection.js +12 -2
- package/dist/internal/woodstock/index.d.ts +7 -1
- package/dist/internal/woodstock/index.js +45 -3
- package/dist/internal/woodstock/session-entry.d.ts +2 -2
- package/dist/remote-client.js +5 -1
- package/dist/server-visitor-stream.js +11 -1
- package/dist/session-liveness.d.ts +4 -0
- package/dist/session-liveness.js +4 -0
- package/docs/api/agent-control.md +2 -2
- package/docs/api/client.md +23 -0
- package/docs/api/delegation.md +2 -2
- package/package.json +4 -4
|
@@ -12,7 +12,8 @@ export function openRuntimeCore(options, mailRoom) {
|
|
|
12
12
|
const coreDigest = yield* coreProtocolDigest([]);
|
|
13
13
|
yield* prepareWoodstockSchema(options.store);
|
|
14
14
|
const runtime = yield* openRuntimeCoordinator(options, coreDigest, admitted, mailRoom);
|
|
15
|
-
const
|
|
15
|
+
const ownerContext = yield* Effect.context();
|
|
16
|
+
const coordinator = openCoordinator(options, runtime, ownerContext);
|
|
16
17
|
const agent = Object.freeze({
|
|
17
18
|
session: coordinator.session,
|
|
18
19
|
createSession: coordinator.createSession,
|
|
@@ -7,7 +7,7 @@ import { ShiitakeError } from "../errors.js";
|
|
|
7
7
|
import { ExtensionId } from "../extension-contract.js";
|
|
8
8
|
import { coreConsumer, reportConsumer, } from "./extension-runtime.js";
|
|
9
9
|
import { publishOwnerObservation } from "./observation-kernel.js";
|
|
10
|
-
import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSessionDelegator, readSettlementMeta, readWaitingOn, readWaitingOnSender, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
|
|
10
|
+
import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementStretchContent, readSettlementDiagnostic, readSessionDelegator, readSettlementMeta, readWaitingOn, readWaitingOnSender, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
|
|
11
11
|
const pageSize = 64;
|
|
12
12
|
function runtimeFailure(message) {
|
|
13
13
|
return new ShiitakeError("runtime", "rejected", message);
|
|
@@ -100,7 +100,10 @@ function reportToDelegator(store, watchdog, mailRoom, job) {
|
|
|
100
100
|
catch: () => runtimeFailure("Session attention is unavailable"),
|
|
101
101
|
});
|
|
102
102
|
const settled = yield* settlementFacts(store, job, waiting);
|
|
103
|
-
|
|
103
|
+
const content = job.outcome === "cancelled"
|
|
104
|
+
? ""
|
|
105
|
+
: yield* readSettlementStretchContent(store, payload.work);
|
|
106
|
+
yield* fanOut(mailRoom, { ...settled, content, delegator });
|
|
104
107
|
});
|
|
105
108
|
}
|
|
106
109
|
/**
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import type * as Context from "effect/Context";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import type { SessionContext, SessionFrame, SessionSnapshot, SubscribeOptions, WatchOptions } from "../client.js";
|
|
5
|
+
import { ShiitakeError } from "../errors.js";
|
|
6
|
+
import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
7
|
+
import { type ObservationKernel } from "./observation-kernel.js";
|
|
8
|
+
import type { RuntimeCoordinator } from "./runtime-coordinator.js";
|
|
9
|
+
import { readSession } from "./woodstock/index.js";
|
|
10
|
+
/** Shared live observation; Woodstock remains the bounded baseline and replay owner. */
|
|
11
|
+
export declare function openSessionObservation(options: ShiitakeEffectOptions, runtime: RuntimeCoordinator, observation: ObservationKernel, ownerContext: Context.Context<never>): Readonly<{
|
|
12
|
+
read: (sessionId: string, input: Parameters<typeof readSession>[3]) => Effect.Effect<Readonly<{
|
|
13
|
+
sessionId: string;
|
|
14
|
+
cursor: string | null;
|
|
15
|
+
hasMore: boolean;
|
|
16
|
+
oldestCursor?: string | null;
|
|
17
|
+
messages: readonly import("../client.js").SessionMessage[];
|
|
18
|
+
terminals: readonly import("../client.js").SessionTerminal[];
|
|
19
|
+
compactions?: readonly import("../client.js").SessionCompactionMarker[];
|
|
20
|
+
compactionNotices?: readonly import("../client.js").SessionCompactionNotice[];
|
|
21
|
+
sandboxFailures?: readonly import("@fungi.computer/mycelium").SandboxFailureEvent[];
|
|
22
|
+
pending: import("../client.js").SessionPendingPrompts;
|
|
23
|
+
activity: import("../client.js").SessionActivity;
|
|
24
|
+
progress: import("../client.js").SessionProgress | null;
|
|
25
|
+
toolOutput: readonly import("../client.js").SessionToolOutput[];
|
|
26
|
+
extensions: import("../client.js").SessionExtensionSlots;
|
|
27
|
+
attention: readonly import("../session-attention.js").SessionAttention[];
|
|
28
|
+
context?: SessionContext;
|
|
29
|
+
}>, ShiitakeError, never>;
|
|
30
|
+
subscribe: (sessionId: string, input: SubscribeOptions) => Stream.Stream<SessionFrame, ShiitakeError, never>;
|
|
31
|
+
watch: (sessionId: string, input: WatchOptions | undefined) => Effect.Effect<Readonly<{
|
|
32
|
+
snapshot: SessionSnapshot;
|
|
33
|
+
changes: Stream.Stream<SessionFrame, ShiitakeError>;
|
|
34
|
+
close: Effect.Effect<void, ShiitakeError>;
|
|
35
|
+
}>, ShiitakeError, never>;
|
|
36
|
+
close: Effect.Effect<void, never, never>;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as Stream from "effect/Stream";
|
|
4
|
+
import * as Semaphore from "effect/Semaphore";
|
|
5
|
+
import * as Scope from "effect/Scope";
|
|
6
|
+
import * as Exit from "effect/Exit";
|
|
7
|
+
import { ShiitakeError } from "../errors.js";
|
|
8
|
+
import { decodeClientReadOptions, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
|
|
9
|
+
import { mergeObservationSignals, openSessionFrameFeed, } from "./observation-kernel.js";
|
|
10
|
+
import { readSessionActivity, } from "./session-status.js";
|
|
11
|
+
import { prepareSessionContext } from "./session-context.js";
|
|
12
|
+
import { compactionEntrySequence, decodeWoodstockCursor, progressPosition, readProcessProgressDelta, readSession, readSessionReplayPage, } from "./woodstock/index.js";
|
|
13
|
+
import { sessionHeartbeatIntervalMs } from "../session-liveness.js";
|
|
14
|
+
function currentSessionWork(runtime, sessionId) {
|
|
15
|
+
return runtime
|
|
16
|
+
.readQueueHead(sessionId)
|
|
17
|
+
.pipe(Effect.flatMap((head) => (head.state === "queued"
|
|
18
|
+
? runtime.runningWork(head.jobId)
|
|
19
|
+
: Effect.succeed(null)).pipe(Effect.map((runningWork) => ({ head, runningWork })))));
|
|
20
|
+
}
|
|
21
|
+
function currentSessionActivity(options, runtime, sessionId) {
|
|
22
|
+
return currentSessionWork(runtime, sessionId).pipe(Effect.map(({ head, runningWork }) => readSessionActivity(options.store, sessionId, head, runningWork)));
|
|
23
|
+
}
|
|
24
|
+
/** Carry the Session's context use on a baseline, when it has one. */
|
|
25
|
+
function withContext(snapshot, context) {
|
|
26
|
+
return context === undefined
|
|
27
|
+
? snapshot
|
|
28
|
+
: Object.freeze({ ...snapshot, context });
|
|
29
|
+
}
|
|
30
|
+
/** Read the baseline and its context use for one Session. */
|
|
31
|
+
function readSessionWithContext(options, runtime, sessionId, input, checkpoint = {}) {
|
|
32
|
+
return currentSessionWork(runtime, sessionId).pipe(Effect.flatMap(({ head, runningWork }) => {
|
|
33
|
+
checkpoint.beforeRead?.();
|
|
34
|
+
return readSession(options.store, sessionId, () => readSessionActivity(options.store, sessionId, head, runningWork), input, () => runtime.extensions.project(sessionId), (snapshot) => ({
|
|
35
|
+
snapshot,
|
|
36
|
+
context: prepareSessionContext(options, sessionId, snapshot.cursor, input?.before === undefined),
|
|
37
|
+
}));
|
|
38
|
+
}), Effect.flatMap(({ snapshot, context }) => {
|
|
39
|
+
checkpoint.afterRead?.();
|
|
40
|
+
return context.pipe(Effect.map((value) => withContext(snapshot, value)));
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
function decodeSubscriptionInput(sessionId, input) {
|
|
44
|
+
return Effect.try({
|
|
45
|
+
try: () => Object.freeze({
|
|
46
|
+
sessionId: decodeClientSessionId(sessionId),
|
|
47
|
+
options: decodeClientSubscribeOptions(input),
|
|
48
|
+
}),
|
|
49
|
+
catch: (error) => error instanceof ShiitakeError
|
|
50
|
+
? error
|
|
51
|
+
: new ShiitakeError("contract", "invalid", "Subscribe options are invalid"),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function resetFrame(options, runtime, sessionId, includeSandboxFailures = false, checkpoint = {}) {
|
|
55
|
+
return runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, sessionId, { includeSandboxFailures }, checkpoint)), Effect.map((baseline) => Object.freeze({
|
|
56
|
+
kind: "reset",
|
|
57
|
+
reason: "history_unavailable",
|
|
58
|
+
cursor: baseline.cursor,
|
|
59
|
+
baseline,
|
|
60
|
+
})));
|
|
61
|
+
}
|
|
62
|
+
function continueFromReset(frame) {
|
|
63
|
+
return [
|
|
64
|
+
[frame],
|
|
65
|
+
Option.some({
|
|
66
|
+
after: frame.cursor ?? undefined,
|
|
67
|
+
delivery: frame.baseline.hasMore ? "replay" : "live",
|
|
68
|
+
awaitSignal: !frame.baseline.hasMore,
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
function subscriptionFrames(options, runtime, sessionId, after, source, includeSandboxFailures = false) {
|
|
73
|
+
const registration = source.kind === "live" ? source.registration : undefined;
|
|
74
|
+
let position = source.kind === "live" ? progressPosition(source.progress) : null;
|
|
75
|
+
let toolOutput = [];
|
|
76
|
+
const reset = () => resetFrame(options, runtime, sessionId, includeSandboxFailures, source.kind === "replay"
|
|
77
|
+
? { beforeRead: source.checkpoint }
|
|
78
|
+
: { afterRead: source.stamp }).pipe(Effect.map((frame) => {
|
|
79
|
+
position = progressPosition(frame.baseline.progress);
|
|
80
|
+
toolOutput = frame.baseline.toolOutput;
|
|
81
|
+
return source.kind === "replay"
|
|
82
|
+
? [[frame], Option.none()]
|
|
83
|
+
: continueFromReset(frame);
|
|
84
|
+
}));
|
|
85
|
+
const liveFrames = (activity) => {
|
|
86
|
+
const queue = activity.facts.queue;
|
|
87
|
+
const delta = queue.state !== "running"
|
|
88
|
+
? null
|
|
89
|
+
: readProcessProgressDelta(options.store, sessionId, queue.commandId, position);
|
|
90
|
+
if (delta === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
const frames = [];
|
|
93
|
+
if (delta !== null) {
|
|
94
|
+
frames.push({
|
|
95
|
+
kind: "progress",
|
|
96
|
+
progress: delta,
|
|
97
|
+
activity: activity.facts,
|
|
98
|
+
});
|
|
99
|
+
position = {
|
|
100
|
+
messageId: delta.messageId,
|
|
101
|
+
revision: delta.revision,
|
|
102
|
+
text: delta.text.offset + delta.text.append.length,
|
|
103
|
+
thinking: delta.thinking.offset + delta.thinking.append.length,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (toolOutput.length !== activity.toolOutput.length ||
|
|
107
|
+
toolOutput.some((slot, index) => slot !== activity.toolOutput[index])) {
|
|
108
|
+
toolOutput = activity.toolOutput;
|
|
109
|
+
frames.push({
|
|
110
|
+
kind: "tool_output",
|
|
111
|
+
toolOutput,
|
|
112
|
+
activity: activity.facts,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return frames;
|
|
116
|
+
};
|
|
117
|
+
const initial = {
|
|
118
|
+
after,
|
|
119
|
+
delivery: source.kind,
|
|
120
|
+
// Live registration precedes its baseline; pending hints cover any join
|
|
121
|
+
// writes. Replaying it unconditionally adds an unprompted idle read.
|
|
122
|
+
awaitSignal: source.kind === "live",
|
|
123
|
+
};
|
|
124
|
+
return Stream.paginate(initial, (state) => (registration === undefined
|
|
125
|
+
? Effect.succeed(undefined)
|
|
126
|
+
: state.awaitSignal
|
|
127
|
+
? registration.awaitSignal.pipe(Effect.flatMap((signal) => registration.pollSignal.pipe(Effect.map((pending) => pending === undefined
|
|
128
|
+
? signal
|
|
129
|
+
: mergeObservationSignals(signal, pending)))))
|
|
130
|
+
: registration.pollSignal).pipe(Effect.flatMap((signal) => {
|
|
131
|
+
if (signal === "closed")
|
|
132
|
+
return Effect.succeed([[], Option.none()]);
|
|
133
|
+
if (signal === "reset")
|
|
134
|
+
return reset();
|
|
135
|
+
return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
|
|
136
|
+
if (source.kind === "replay")
|
|
137
|
+
source.checkpoint();
|
|
138
|
+
registration?.setQueued(activity.facts.queue.state === "queued");
|
|
139
|
+
if ((signal === "progress" || signal === "activity") &&
|
|
140
|
+
state.delivery === "live") {
|
|
141
|
+
const frames = liveFrames(activity);
|
|
142
|
+
if (frames === undefined)
|
|
143
|
+
return reset();
|
|
144
|
+
// This path reads no durable page; stamp the live projection.
|
|
145
|
+
if (source.kind === "live")
|
|
146
|
+
source.stamp();
|
|
147
|
+
return Effect.succeed([
|
|
148
|
+
[
|
|
149
|
+
...(signal === "activity"
|
|
150
|
+
? [
|
|
151
|
+
{
|
|
152
|
+
kind: "activity",
|
|
153
|
+
activity: activity.facts,
|
|
154
|
+
},
|
|
155
|
+
]
|
|
156
|
+
: []),
|
|
157
|
+
...frames,
|
|
158
|
+
],
|
|
159
|
+
Option.some({ ...state, awaitSignal: true }),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
return readSessionReplayPage(options.store, sessionId, state.after, state.delivery, activity, () => runtime.extensions.project(sessionId), includeSandboxFailures, (page) => {
|
|
163
|
+
const latestCursor = page.kind === "page"
|
|
164
|
+
? page.frames
|
|
165
|
+
.flatMap((frame) => frame.kind === "progress" ||
|
|
166
|
+
frame.kind === "activity" ||
|
|
167
|
+
frame.kind === "tool_output" ||
|
|
168
|
+
frame.kind === "heartbeat"
|
|
169
|
+
? []
|
|
170
|
+
: [frame.cursor])
|
|
171
|
+
.at(-1)
|
|
172
|
+
: undefined;
|
|
173
|
+
// Capture the final moving page's stored context in the
|
|
174
|
+
// same read transaction as its cursor. Model description
|
|
175
|
+
// remains deferred until after that transaction.
|
|
176
|
+
const context = page.kind === "page" &&
|
|
177
|
+
!page.hasMore &&
|
|
178
|
+
page.frames.some((frame) => frame.kind === "update" &&
|
|
179
|
+
frame.cursor !== state.after)
|
|
180
|
+
? prepareSessionContext(options, sessionId, latestCursor ?? null)
|
|
181
|
+
: Effect.succeed(undefined);
|
|
182
|
+
return { page, latestCursor, context };
|
|
183
|
+
}).pipe(Effect.flatMap(({ page, latestCursor, context, }) => {
|
|
184
|
+
if (page.kind === "reset")
|
|
185
|
+
return reset();
|
|
186
|
+
if (source.kind === "live")
|
|
187
|
+
source.stamp();
|
|
188
|
+
if (!page.hasMore)
|
|
189
|
+
toolOutput = activity.toolOutput;
|
|
190
|
+
const progressFrames = page.hasMore
|
|
191
|
+
? []
|
|
192
|
+
: liveFrames(activity);
|
|
193
|
+
if (progressFrames === undefined)
|
|
194
|
+
return reset();
|
|
195
|
+
const next = {
|
|
196
|
+
after: latestCursor ?? state.after,
|
|
197
|
+
delivery: page.hasMore ? state.delivery : "live",
|
|
198
|
+
awaitSignal: !page.hasMore,
|
|
199
|
+
};
|
|
200
|
+
return context.pipe(Effect.map((context) => [
|
|
201
|
+
[
|
|
202
|
+
...(context === undefined
|
|
203
|
+
? page.frames
|
|
204
|
+
: page.frames.map((frame) => frame.kind === "update" &&
|
|
205
|
+
frame.cursor === latestCursor
|
|
206
|
+
? Object.freeze({ ...frame, context })
|
|
207
|
+
: frame)),
|
|
208
|
+
...progressFrames,
|
|
209
|
+
],
|
|
210
|
+
source.kind === "replay" && !page.hasMore
|
|
211
|
+
? Option.none()
|
|
212
|
+
: Option.some(next),
|
|
213
|
+
]));
|
|
214
|
+
}));
|
|
215
|
+
}));
|
|
216
|
+
})));
|
|
217
|
+
}
|
|
218
|
+
/** One projection producer and heartbeat clock per watched Session. */
|
|
219
|
+
function openLiveSessionFeeds(options, runtime, observation, ownerContext) {
|
|
220
|
+
const lanes = new Map();
|
|
221
|
+
const scope = Scope.makeUnsafe();
|
|
222
|
+
let watchers = 0;
|
|
223
|
+
const acquire = (sessionId, includeSandboxFailures) => Effect.suspend(() => {
|
|
224
|
+
let lane = lanes.get(sessionId);
|
|
225
|
+
if (lane === undefined) {
|
|
226
|
+
lane = { lock: Semaphore.makeUnsafe(1), feed: undefined };
|
|
227
|
+
lanes.set(sessionId, lane);
|
|
228
|
+
}
|
|
229
|
+
const current = lane;
|
|
230
|
+
return current.lock.withPermit(Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
231
|
+
if (lanes.get(sessionId) !== current)
|
|
232
|
+
return yield* acquire(sessionId, includeSandboxFailures);
|
|
233
|
+
let feed = current.feed;
|
|
234
|
+
if (feed === undefined) {
|
|
235
|
+
const registration = yield* observation.register(sessionId).pipe(Effect.onError(() => Effect.sync(() => {
|
|
236
|
+
if (lanes.get(sessionId) === current)
|
|
237
|
+
lanes.delete(sessionId);
|
|
238
|
+
})));
|
|
239
|
+
const lifecycle = { stopped: false };
|
|
240
|
+
const frames = openSessionFrameFeed(() => {
|
|
241
|
+
stop();
|
|
242
|
+
});
|
|
243
|
+
const stop = () => {
|
|
244
|
+
if (lifecycle.stopped)
|
|
245
|
+
return;
|
|
246
|
+
lifecycle.stopped = true;
|
|
247
|
+
registration.close();
|
|
248
|
+
lifecycle.producer?.interruptUnsafe();
|
|
249
|
+
lifecycle.heartbeat?.interruptUnsafe();
|
|
250
|
+
frames.end();
|
|
251
|
+
current.feed = undefined;
|
|
252
|
+
if (lanes.get(sessionId) === current)
|
|
253
|
+
lanes.delete(sessionId);
|
|
254
|
+
};
|
|
255
|
+
feed = { registration, frames, stop };
|
|
256
|
+
current.feed = feed;
|
|
257
|
+
const baseline = yield* restore(readSessionWithContext(options, runtime, sessionId, {
|
|
258
|
+
includeSandboxFailures: true,
|
|
259
|
+
})).pipe(Effect.onError(() => Effect.sync(stop)));
|
|
260
|
+
registration.setQueued(baseline.activity.queue.state === "queued");
|
|
261
|
+
let revision = registration.revision;
|
|
262
|
+
// Replace the joining request's context while forking, so the
|
|
263
|
+
// long-lived children inherit only the Agent's services/span.
|
|
264
|
+
lifecycle.producer = yield* Effect.forkIn(subscriptionFrames(options, runtime, sessionId, baseline.cursor ?? undefined, {
|
|
265
|
+
kind: "live",
|
|
266
|
+
registration,
|
|
267
|
+
progress: baseline.progress,
|
|
268
|
+
stamp: () => {
|
|
269
|
+
revision = registration.revision;
|
|
270
|
+
},
|
|
271
|
+
}, true).pipe(Stream.runForEach((frame) => Effect.sync(() => {
|
|
272
|
+
frames.publish(frame, revision);
|
|
273
|
+
})), Effect.catch((error) => Effect.sync(() => {
|
|
274
|
+
frames.end(error);
|
|
275
|
+
})), Effect.ensuring(Effect.sync(stop))), scope, { uninterruptible: false }).pipe(Effect.setContext(ownerContext));
|
|
276
|
+
if (!lifecycle.stopped)
|
|
277
|
+
lifecycle.heartbeat = yield* Effect.forkIn(Stream.fromEffectRepeat(Effect.sleep(sessionHeartbeatIntervalMs).pipe(Effect.as({ kind: "heartbeat" }))).pipe(Stream.runForEach((frame) => Effect.sync(() => {
|
|
278
|
+
frames.publish(frame);
|
|
279
|
+
}))), scope, { uninterruptible: false }).pipe(Effect.setContext(ownerContext));
|
|
280
|
+
}
|
|
281
|
+
const queue = feed.frames.register(includeSandboxFailures);
|
|
282
|
+
return { feed, queue };
|
|
283
|
+
})));
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
acquire: (sessionId, includeSandboxFailures) => Effect.uninterruptibleMask((restore) => Effect.suspend(() => {
|
|
287
|
+
if (watchers >= 64)
|
|
288
|
+
return Effect.fail(new ShiitakeError("runtime", "rejected", "Observation capacity is exhausted"));
|
|
289
|
+
watchers += 1;
|
|
290
|
+
let released = false;
|
|
291
|
+
const release = () => {
|
|
292
|
+
if (released)
|
|
293
|
+
return;
|
|
294
|
+
released = true;
|
|
295
|
+
watchers -= 1;
|
|
296
|
+
};
|
|
297
|
+
return restore(acquire(sessionId, includeSandboxFailures)).pipe(Effect.map(({ feed, queue }) => ({
|
|
298
|
+
feed,
|
|
299
|
+
queue: Object.freeze({
|
|
300
|
+
...queue,
|
|
301
|
+
close: () => {
|
|
302
|
+
release();
|
|
303
|
+
queue.close();
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
})), Effect.onError(() => Effect.sync(release)));
|
|
307
|
+
})),
|
|
308
|
+
close: Effect.sync(() => {
|
|
309
|
+
for (const lane of lanes.values())
|
|
310
|
+
lane.feed?.stop();
|
|
311
|
+
lanes.clear();
|
|
312
|
+
}).pipe(Effect.andThen(Scope.close(scope, Exit.void))),
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function queuedSessionFrames(queue) {
|
|
316
|
+
return Stream.paginate(null, () => queue.take.pipe(Effect.map((frame) => frame === "closed"
|
|
317
|
+
? [[], Option.none()]
|
|
318
|
+
: [[frame], Option.some(null)])));
|
|
319
|
+
}
|
|
320
|
+
/** Durable rows dedup by sequence; ephemeral appends dedup only by revision. */
|
|
321
|
+
function reconcileSessionFrames(source, initialCursor, initialProgress, reset) {
|
|
322
|
+
let cursor = initialCursor;
|
|
323
|
+
let position = progressPosition(initialProgress);
|
|
324
|
+
return source.pipe(Stream.mapEffect((incoming) => Effect.gen(function* () {
|
|
325
|
+
let frame = incoming === "reset" ? yield* reset() : incoming;
|
|
326
|
+
if (frame.kind === "heartbeat")
|
|
327
|
+
return [frame];
|
|
328
|
+
if (frame.kind === "progress") {
|
|
329
|
+
const previous = position?.messageId === frame.progress.messageId
|
|
330
|
+
? position
|
|
331
|
+
: null;
|
|
332
|
+
if (previous !== null &&
|
|
333
|
+
previous.revision >= frame.progress.revision)
|
|
334
|
+
return [];
|
|
335
|
+
const text = previous?.text ?? 0;
|
|
336
|
+
const thinking = previous?.thinking ?? 0;
|
|
337
|
+
if (frame.progress.text.offset > text ||
|
|
338
|
+
frame.progress.thinking.offset > thinking)
|
|
339
|
+
frame = yield* reset();
|
|
340
|
+
else {
|
|
341
|
+
position = {
|
|
342
|
+
messageId: frame.progress.messageId,
|
|
343
|
+
revision: frame.progress.revision,
|
|
344
|
+
text: frame.progress.text.offset +
|
|
345
|
+
frame.progress.text.append.length,
|
|
346
|
+
thinking: frame.progress.thinking.offset +
|
|
347
|
+
frame.progress.thinking.append.length,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (frame.kind === "reset") {
|
|
352
|
+
cursor = frame.cursor;
|
|
353
|
+
position = progressPosition(frame.baseline.progress);
|
|
354
|
+
return [frame];
|
|
355
|
+
}
|
|
356
|
+
if (frame.kind !== "update")
|
|
357
|
+
return [frame];
|
|
358
|
+
const previous = cursor === null ? undefined : decodeWoodstockCursor(cursor);
|
|
359
|
+
const next = frame.cursor === null
|
|
360
|
+
? undefined
|
|
361
|
+
: decodeWoodstockCursor(frame.cursor);
|
|
362
|
+
if (previous !== undefined &&
|
|
363
|
+
next !== undefined &&
|
|
364
|
+
previous.generation !== next.generation) {
|
|
365
|
+
const baseline = yield* reset();
|
|
366
|
+
cursor = baseline.cursor;
|
|
367
|
+
position = progressPosition(baseline.baseline.progress);
|
|
368
|
+
return [baseline];
|
|
369
|
+
}
|
|
370
|
+
const sequence = previous?.sequence ?? 0;
|
|
371
|
+
const candidateCursor = next !== undefined && next.sequence > sequence
|
|
372
|
+
? frame.cursor
|
|
373
|
+
: cursor;
|
|
374
|
+
const duplicate = (rows) => rows?.some((row) => row.sequence <= sequence) === true;
|
|
375
|
+
if (candidateCursor === frame.cursor &&
|
|
376
|
+
!duplicate(frame.messages) &&
|
|
377
|
+
!duplicate(frame.terminals) &&
|
|
378
|
+
!frame.compactions?.some((marker) => compactionEntrySequence(marker) <= sequence) &&
|
|
379
|
+
!duplicate(frame.compactionNotices)) {
|
|
380
|
+
cursor = frame.cursor;
|
|
381
|
+
return [frame];
|
|
382
|
+
}
|
|
383
|
+
const rows = {
|
|
384
|
+
...frame,
|
|
385
|
+
cursor: candidateCursor,
|
|
386
|
+
messages: frame.messages.filter((row) => row.sequence > sequence),
|
|
387
|
+
terminals: frame.terminals.filter((row) => row.sequence > sequence),
|
|
388
|
+
};
|
|
389
|
+
if (frame.compactions !== undefined)
|
|
390
|
+
rows.compactions = frame.compactions.filter((marker) => compactionEntrySequence(marker) > sequence);
|
|
391
|
+
if (frame.compactionNotices !== undefined)
|
|
392
|
+
rows.compactionNotices = frame.compactionNotices.filter((row) => row.sequence > sequence);
|
|
393
|
+
cursor = rows.cursor;
|
|
394
|
+
return [rows];
|
|
395
|
+
})), Stream.flattenIterable);
|
|
396
|
+
}
|
|
397
|
+
function subscribeSession(options, runtime, feeds, sessionId, input) {
|
|
398
|
+
return Stream.unwrap(runtime.assertOpen.pipe(Effect.andThen(decodeSubscriptionInput(sessionId, input)), Effect.flatMap((decoded) => Effect.acquireRelease(feeds.acquire(decoded.sessionId, decoded.options.includeSandboxFailures === true), ({ queue }) => Effect.sync(queue.close)).pipe(Effect.map(({ feed, queue }) => {
|
|
399
|
+
const checkpoint = () => {
|
|
400
|
+
queue.checkpoint(feed.registration.revision);
|
|
401
|
+
};
|
|
402
|
+
return reconcileSessionFrames(subscriptionFrames(options, runtime, decoded.sessionId, decoded.options.after, { kind: "replay", checkpoint }, decoded.options.includeSandboxFailures === true).pipe(Stream.concat(queuedSessionFrames(queue))), decoded.options.after ?? null, null, () => resetFrame(options, runtime, decoded.sessionId, decoded.options.includeSandboxFailures === true, { beforeRead: checkpoint }));
|
|
403
|
+
})))));
|
|
404
|
+
}
|
|
405
|
+
function decodeWatchInput(sessionId, input) {
|
|
406
|
+
return Effect.try({
|
|
407
|
+
try: () => Object.freeze({
|
|
408
|
+
sessionId: decodeClientSessionId(sessionId),
|
|
409
|
+
options: decodeClientReadOptions(input),
|
|
410
|
+
}),
|
|
411
|
+
catch: (error) => error instanceof ShiitakeError
|
|
412
|
+
? error
|
|
413
|
+
: new ShiitakeError("contract", "invalid", "Watch options are invalid"),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function watchSession(options, runtime, feeds, sessionId, input) {
|
|
417
|
+
return runtime.assertOpen.pipe(Effect.andThen(decodeWatchInput(sessionId, input)), Effect.flatMap((decoded) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
418
|
+
const { feed, queue } = yield* feeds.acquire(decoded.sessionId, decoded.options.includeSandboxFailures === true);
|
|
419
|
+
const checkpoint = () => {
|
|
420
|
+
queue.checkpoint(feed.registration.revision);
|
|
421
|
+
};
|
|
422
|
+
return yield* restore(runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, decoded.sessionId, decoded.options, { beforeRead: checkpoint })), Effect.map((snapshot) => {
|
|
423
|
+
return Object.freeze({
|
|
424
|
+
snapshot,
|
|
425
|
+
changes: reconcileSessionFrames(queuedSessionFrames(queue), snapshot.cursor, snapshot.progress, () => resetFrame(options, runtime, decoded.sessionId, decoded.options.includeSandboxFailures === true, { beforeRead: checkpoint })).pipe(Stream.ensuring(Effect.sync(queue.close))),
|
|
426
|
+
close: Effect.sync(queue.close),
|
|
427
|
+
});
|
|
428
|
+
}))).pipe(Effect.onError(() => Effect.sync(queue.close)));
|
|
429
|
+
}))));
|
|
430
|
+
}
|
|
431
|
+
/** Shared live observation; Woodstock remains the bounded baseline and replay owner. */
|
|
432
|
+
export function openSessionObservation(options, runtime, observation, ownerContext) {
|
|
433
|
+
const feeds = openLiveSessionFeeds(options, runtime, observation, ownerContext);
|
|
434
|
+
return Object.freeze({
|
|
435
|
+
read: (sessionId, input) => readSessionWithContext(options, runtime, sessionId, input),
|
|
436
|
+
subscribe: (sessionId, input) => subscribeSession(options, runtime, feeds, sessionId, input),
|
|
437
|
+
watch: (sessionId, input) => watchSession(options, runtime, feeds, sessionId, input),
|
|
438
|
+
close: feeds.close,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionFrame, SessionExtensionSlots, SessionPendingPrompts, SessionSnapshot } from "../../client.js";
|
|
1
|
+
import type { SessionFrame, SessionCompactionMarker, SessionExtensionSlots, SessionPendingPrompts, SessionSnapshot } from "../../client.js";
|
|
2
2
|
import type { ClientReadOptions } from "../client-boundary.js";
|
|
3
3
|
import type { CommandSettlement, SqliteOwner } from "../contracts.js";
|
|
4
4
|
import type { ObservedActivity } from "../session-status.js";
|
|
@@ -8,6 +8,8 @@ import { type AdmissionIdentity, type StoredSessionRow } from "./session-store.j
|
|
|
8
8
|
* and retry text can contain private provider bodies, not safe diagnostics.
|
|
9
9
|
*/
|
|
10
10
|
export declare function diagnosticsByWorkId(path: readonly StoredSessionRow[]): ReadonlyMap<string, string>;
|
|
11
|
+
/** Native entry order, not the marker's display horizon; requires no SQL read. */
|
|
12
|
+
export declare function compactionEntrySequence(marker: SessionCompactionMarker): number;
|
|
11
13
|
/** Same payload projection as the FIFO page, selected independently of that page's bounds. */
|
|
12
14
|
export declare function readLatestPendingPrompt(owner: SqliteOwner, sessionId: string, identity?: AdmissionIdentity): SessionPendingPrompts["prompts"][number] | null;
|
|
13
15
|
/**
|
|
@@ -150,6 +150,14 @@ function projectTerminal(row, { commandIds, diagnostics, cancellations }) {
|
|
|
150
150
|
: undefined;
|
|
151
151
|
return Object.freeze(meta === undefined ? terminal : { ...terminal, meta });
|
|
152
152
|
}
|
|
153
|
+
const compactionEntrySequences = new WeakMap();
|
|
154
|
+
/** Native entry order, not the marker's display horizon; requires no SQL read. */
|
|
155
|
+
export function compactionEntrySequence(marker) {
|
|
156
|
+
const sequence = compactionEntrySequences.get(marker);
|
|
157
|
+
if (sequence === undefined)
|
|
158
|
+
throw unavailable("Session compaction entry position is unavailable");
|
|
159
|
+
return sequence;
|
|
160
|
+
}
|
|
153
161
|
/**
|
|
154
162
|
* Full-history client projections show every durable entry, including what a
|
|
155
163
|
* compaction summarized away. Each compaction entry in `rows` contributes one
|
|
@@ -170,14 +178,16 @@ function compactionMarkersIn(owner, lineage, rows) {
|
|
|
170
178
|
lineageEntrySequence(owner, lineage, entry.head);
|
|
171
179
|
if (kept === undefined)
|
|
172
180
|
throw unavailable("Session compaction horizon is unavailable");
|
|
173
|
-
|
|
181
|
+
const marker = Object.freeze({
|
|
174
182
|
id: row.id,
|
|
175
183
|
sequence: kept,
|
|
176
184
|
at: row.timestamp,
|
|
177
185
|
tokensBefore: entry.data.tokensBefore,
|
|
178
186
|
tokensAfter: entry.data.tokensAfter ?? null,
|
|
179
187
|
trigger: entry.data.reason ?? null,
|
|
180
|
-
})
|
|
188
|
+
});
|
|
189
|
+
compactionEntrySequences.set(marker, row.sequence);
|
|
190
|
+
markers.push(marker);
|
|
181
191
|
}
|
|
182
192
|
return Object.freeze(markers);
|
|
183
193
|
}
|
|
@@ -12,7 +12,8 @@ import { type ClientReplayPage } from "./client-projection.js";
|
|
|
12
12
|
import { type LiveRetryEvent } from "./session-entry.js";
|
|
13
13
|
import { type CompactionNoticeInput, type AcceptedCommand, type EnqueueAcceptedWork, type AppendCompactionInput, type PreparedSessionRun, type RunContract, type RecordAcceptedCancellation, type SessionQueueHead, type StoredSessionRow } from "./session-store.js";
|
|
14
14
|
export * from "./extension-store.js";
|
|
15
|
-
export {
|
|
15
|
+
export { decodeWoodstockCursor } from "./cursor.js";
|
|
16
|
+
export { compactionEntrySequence, readLatestPendingPrompt, } from "./client-projection.js";
|
|
16
17
|
export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
|
|
17
18
|
export * from "./session-attention.js";
|
|
18
19
|
export * from "./work-senders.js";
|
|
@@ -101,6 +102,11 @@ export declare function readSettlementContent(owner: SqliteOwner, input: Readonl
|
|
|
101
102
|
sessionId: string;
|
|
102
103
|
commandFactId: string;
|
|
103
104
|
}>): Effect.Effect<string, ShiitakeError>;
|
|
105
|
+
/** Last non-empty text in an extension stretch, including its initiating work. */
|
|
106
|
+
export declare function readSettlementStretchContent(owner: SqliteOwner, input: Readonly<{
|
|
107
|
+
sessionId: string;
|
|
108
|
+
commandFactId: string;
|
|
109
|
+
}>): Effect.Effect<string, ShiitakeError>;
|
|
104
110
|
/**
|
|
105
111
|
* The host metadata that settles with one accepted command: the
|
|
106
112
|
* cancellation's when a host-attributed cancel ended it, otherwise the
|
|
@@ -12,7 +12,8 @@ import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNo
|
|
|
12
12
|
import { readSessionInboxPage, recordSessionInboxDeletion, } from "./session-inbox.js";
|
|
13
13
|
import { readSessionAttention } from "./session-attention.js";
|
|
14
14
|
export * from "./extension-store.js";
|
|
15
|
-
export {
|
|
15
|
+
export { decodeWoodstockCursor } from "./cursor.js";
|
|
16
|
+
export { compactionEntrySequence, readLatestPendingPrompt, } from "./client-projection.js";
|
|
16
17
|
export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
|
|
17
18
|
export * from "./session-attention.js";
|
|
18
19
|
export * from "./work-senders.js";
|
|
@@ -155,8 +156,49 @@ export function readSettlementContent(owner, input) {
|
|
|
155
156
|
const entries = readSessionEntriesForWork(owner, input.sessionId, admission.entry.workId);
|
|
156
157
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
157
158
|
const entry = entries[index]?.entry;
|
|
158
|
-
if (entry?.kind === "message" && entry.value.role === "assistant")
|
|
159
|
-
|
|
159
|
+
if (entry?.kind === "message" && entry.value.role === "assistant") {
|
|
160
|
+
const text = assistantText(entry.value);
|
|
161
|
+
if (text !== "")
|
|
162
|
+
return text;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return "";
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** Last non-empty text in an extension stretch, including its initiating work. */
|
|
169
|
+
export function readSettlementStretchContent(owner, input) {
|
|
170
|
+
return Effect.gen(function* () {
|
|
171
|
+
let commandFactId = input.commandFactId;
|
|
172
|
+
while (commandFactId !== undefined) {
|
|
173
|
+
const content = yield* readSettlementContent(owner, {
|
|
174
|
+
sessionId: input.sessionId,
|
|
175
|
+
commandFactId,
|
|
176
|
+
});
|
|
177
|
+
if (content !== "")
|
|
178
|
+
return content;
|
|
179
|
+
const currentId = commandFactId;
|
|
180
|
+
commandFactId = yield* storeEffect(() => {
|
|
181
|
+
const admission = readSessionCommandAdmission(owner, input.sessionId, currentId);
|
|
182
|
+
if (admission === undefined)
|
|
183
|
+
throw rejected("Accepted Session command is unavailable");
|
|
184
|
+
if (readExtensionAdmissionOrigin(owner, input.sessionId, currentId, admission.entry.workId) === null)
|
|
185
|
+
return undefined;
|
|
186
|
+
let row = readSessionEntryById(owner, currentId);
|
|
187
|
+
if (row === undefined)
|
|
188
|
+
throw rejected("Accepted Session command entry is unavailable");
|
|
189
|
+
// Walk only this command's ancestry. Strictly decreasing sequences
|
|
190
|
+
// bound the walk and exclude sibling branches or newer queued work.
|
|
191
|
+
while (row.parentId !== null) {
|
|
192
|
+
const parent = readSessionEntryById(owner, row.parentId);
|
|
193
|
+
if (parent?.sessionId !== input.sessionId ||
|
|
194
|
+
parent.sequence >= row.sequence)
|
|
195
|
+
throw rejected("Session stretch ancestry is unavailable");
|
|
196
|
+
if (parent.entry.kind === "command")
|
|
197
|
+
return parent.id;
|
|
198
|
+
row = parent;
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
});
|
|
160
202
|
}
|
|
161
203
|
return "";
|
|
162
204
|
});
|
|
@@ -447,7 +447,7 @@ export declare function decodeStoredSessionEntry(value: string): Result.Result<{
|
|
|
447
447
|
readonly run: MuleIdentity;
|
|
448
448
|
readonly turn: MuleIdentity;
|
|
449
449
|
readonly message: MuleIdentity;
|
|
450
|
-
readonly role: "
|
|
450
|
+
readonly role: "toolResult" | "system" | "user" | "assistant";
|
|
451
451
|
readonly workId: string;
|
|
452
452
|
} | {
|
|
453
453
|
readonly kind: "message";
|
|
@@ -533,8 +533,8 @@ export declare function decodeStoredSessionEntry(value: string): Result.Result<{
|
|
|
533
533
|
readonly workId: string;
|
|
534
534
|
readonly result: "failed";
|
|
535
535
|
readonly trigger: "manual" | "threshold" | "overflow";
|
|
536
|
-
readonly diagnostic?: string;
|
|
537
536
|
readonly usage?: Usage;
|
|
537
|
+
readonly diagnostic?: string;
|
|
538
538
|
} | {
|
|
539
539
|
readonly kind: "compaction_notice";
|
|
540
540
|
readonly workId: string;
|