@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
@@ -90,6 +90,12 @@ export function createFleetDelegationStore(storage) {
90
90
  storage.sql
91
91
  .exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_undeliverable_delegations (
92
92
  request_id TEXT PRIMARY KEY
93
+ ) STRICT`);
94
+ // A remote settlement confirmed by this owner. Without this local fact an
95
+ // accepted sender receipt remains indistinguishable from outstanding work.
96
+ storage.sql
97
+ .exec(`CREATE TABLE IF NOT EXISTS shiitake_fleet_settled_delegations (
98
+ request_id TEXT PRIMARY KEY
93
99
  ) STRICT`);
94
100
  return {
95
101
  read: (requestId) => storage.sql
@@ -108,13 +114,27 @@ export function createFleetDelegationStore(storage) {
108
114
  ON terminal.request_id = delegation.request_id
109
115
  WHERE delegation.request_id = ?`, requestId)
110
116
  .toArray()[0],
111
- /** Every recipient-accepted delegation, newest first. */
112
- sent: () => storage.sql
117
+ /** Accepted sends not yet observed settled, for owner-local archive admission. */
118
+ unsettled: () => storage.sql
113
119
  .exec(`SELECT input, mail, 'accepted' AS state, receipt
114
- FROM shiitake_fleet_delegations
120
+ FROM shiitake_fleet_delegations AS delegation
115
121
  WHERE receipt IS NOT NULL
116
- ORDER BY rowid DESC`)
122
+ AND NOT EXISTS (
123
+ SELECT 1 FROM shiitake_fleet_settled_delegations AS settled
124
+ WHERE settled.request_id = delegation.request_id
125
+ )
126
+ ORDER BY delegation.rowid DESC`)
117
127
  .toArray(),
128
+ recordSettled(requestId) {
129
+ storage.transactionSync(() => {
130
+ const accepted = storage.sql
131
+ .exec("SELECT 1 FROM shiitake_fleet_delegations WHERE request_id = ? AND receipt IS NOT NULL", requestId)
132
+ .toArray();
133
+ if (accepted.length !== 1)
134
+ throw new Error("Cannot record settlement without acceptance");
135
+ storage.sql.exec("INSERT OR IGNORE INTO shiitake_fleet_settled_delegations(request_id) VALUES(?)", requestId);
136
+ });
137
+ },
118
138
  reserve(requestId, input, mail) {
119
139
  storage.transactionSync(() => {
120
140
  storage.sql.exec("INSERT OR IGNORE INTO shiitake_fleet_delegations(request_id, input, mail) VALUES (?, ?, ?)", requestId, input, mail);
@@ -119,7 +119,7 @@ export declare function readSettlementDiagnostic(owner: SqliteOwner, input: Read
119
119
  commandFactId: string;
120
120
  }>): Effect.Effect<string | undefined, ShiitakeError>;
121
121
  /** Read one bounded public Session snapshot. */
122
- export declare function readSession(owner: SqliteOwner, sessionId: string, activity: ObservedActivity, input?: ReadOptions, projectExtensions?: () => SessionExtensionSlots): Effect.Effect<SessionSnapshot, ShiitakeError>;
122
+ export declare function readSession<A>(owner: SqliteOwner, sessionId: string, activity: () => ObservedActivity, input: ReadOptions | undefined, projectExtensions: () => SessionExtensionSlots, complete: (snapshot: SessionSnapshot) => A): Effect.Effect<A, ShiitakeError>;
123
123
  /** Read one bounded folder-resume summary through Woodstock's Effect boundary. */
124
124
  export declare function readSessionSummary(owner: SqliteOwner, sessionId: string): Effect.Effect<import("./session-store.js").StoredSessionSummary | undefined, ShiitakeError>;
125
125
  /**
@@ -142,8 +142,8 @@ export declare function readColdSessionPage(owner: SqliteOwner, request: ColdSes
142
142
  export declare function readSessionInbox(owner: SqliteOwner, after: string | undefined): Effect.Effect<import("../../client.js").SessionSummaryPage, ShiitakeError>;
143
143
  /** Read a Session's open attention items from the store alone. */
144
144
  export declare function readStoredSessionAttention(owner: SqliteOwner, sessionId: string): Effect.Effect<readonly import("../../client.js").SessionAttention[], ShiitakeError>;
145
- /** Read one strict-after page from Woodstock's selected lineage. */
146
- export declare function readSessionReplayPage(owner: SqliteOwner, sessionId: string, after: string | undefined, delivery: "replay" | "live", activity: ObservedActivity, projectExtensions?: () => SessionExtensionSlots, includeSandboxFailures?: boolean): Effect.Effect<ClientReplayPage, ShiitakeError>;
145
+ /** Read and complete one strict-after page in the same Session transaction. */
146
+ export declare function readSessionReplayPage<A>(owner: SqliteOwner, sessionId: string, after: string | undefined, delivery: "replay" | "live", activity: ObservedActivity, projectExtensions: () => SessionExtensionSlots, includeSandboxFailures: boolean, complete: (page: ClientReplayPage) => A): Effect.Effect<A, ShiitakeError>;
147
147
  /** Persist an operator-only sandbox diagnostic beside its accepted Session work. */
148
148
  export declare function recordSandboxFailure(owner: SqliteOwner, accepted: Pick<AcceptedCommand, "sessionId" | "commandId">, event: SandboxFailureEvent): Effect.Effect<void, ShiitakeError>;
149
149
  /** Extension that admitted one accepted command; null for other producers. */
@@ -5,9 +5,10 @@ import { publishOwnerObservation } from "../observation-kernel.js";
5
5
  import { readColdSessionPageOwned } from "./cold-sessions.js";
6
6
  import { diagnosticsByWorkId, projectCommandSettlement, readClientReplayPage, readClientSnapshot, } from "./client-projection.js";
7
7
  import { sessionIds } from "./identities.js";
8
+ import { decodeWoodstockCursor } from "./cursor.js";
8
9
  import { openProgress, openToolProgress } from "./progress.js";
9
10
  import { encodeStoredSessionEntry, } from "./session-entry.js";
10
- import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNoticeOwned, placeCompactionTransitionOwned, acceptSessionAbort, acceptSessionCancellation, completeSessionLeaf, prepareSessionRun, prepareSessionStore, readSessionEntriesForWork, readSessionCommandAdmission, settleSessionControls, takePendingSessionControls, readSessionEntryById, selectSessionBranch, forkSessionAt, readStoredSessionSummary as readSessionSummaryOwned, readStoredSessionFactPage, createStoredSession, createStoredSessionWithinTransaction, deleteStoredSession, appendOperatorEntry, } from "./session-store.js";
11
+ import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNoticeOwned, placeCompactionTransitionOwned, acceptSessionAbort, acceptSessionCancellation, completeSessionLeaf, prepareSessionRun, prepareSessionStore, readSessionEntriesForWork, readSessionCommandAdmission, settleSessionControls, takePendingSessionControls, readSessionEntryById, selectSessionBranch, forkSessionAt, readStoredSessionSummary as readSessionSummaryOwned, readStoredSessionFactPage, createStoredSession, createStoredSessionWithinTransaction, deleteStoredSession, appendOperatorEntry, withSessionRead, } from "./session-store.js";
11
12
  import { readSessionInboxPage, recordSessionInboxDeletion, } from "./session-inbox.js";
12
13
  import { readSessionAttention } from "./session-attention.js";
13
14
  export * from "./extension-store.js";
@@ -191,8 +192,14 @@ export function readSettlementDiagnostic(owner, input) {
191
192
  });
192
193
  }
193
194
  /** Read one bounded public Session snapshot. */
194
- export function readSession(owner, sessionId, activity, input, projectExtensions = () => ({})) {
195
- return storeEffect(() => readClientSnapshot(owner, decodeClientSessionId(sessionId), decodeClientReadOptions(input), activity, projectExtensions));
195
+ export function readSession(owner, sessionId, activity, input, projectExtensions, complete) {
196
+ return storeEffect(() => {
197
+ const id = decodeClientSessionId(sessionId);
198
+ const options = decodeClientReadOptions(input);
199
+ return withSessionRead(owner, id, () => complete(readClientSnapshot(owner, id, options, activity(), projectExtensions)), options.before === undefined
200
+ ? undefined
201
+ : decodeWoodstockCursor(options.before)?.sequence);
202
+ });
196
203
  }
197
204
  /** Read one bounded folder-resume summary through Woodstock's Effect boundary. */
198
205
  export function readSessionSummary(owner, sessionId) {
@@ -235,9 +242,12 @@ export function readSessionInbox(owner, after) {
235
242
  export function readStoredSessionAttention(owner, sessionId) {
236
243
  return storeEffect(() => readSessionAttention(owner, sessionId));
237
244
  }
238
- /** Read one strict-after page from Woodstock's selected lineage. */
239
- export function readSessionReplayPage(owner, sessionId, after, delivery, activity, projectExtensions = () => ({}), includeSandboxFailures = false) {
240
- return storeEffect(() => readClientReplayPage(owner, decodeClientSessionId(sessionId), after, delivery, activity, projectExtensions, includeSandboxFailures));
245
+ /** Read and complete one strict-after page in the same Session transaction. */
246
+ export function readSessionReplayPage(owner, sessionId, after, delivery, activity, projectExtensions, includeSandboxFailures, complete) {
247
+ return storeEffect(() => {
248
+ const id = decodeClientSessionId(sessionId);
249
+ return withSessionRead(owner, id, () => complete(readClientReplayPage(owner, id, after, delivery, activity, projectExtensions, includeSandboxFailures)));
250
+ });
241
251
  }
242
252
  function sameEntry(left, right) {
243
253
  return encodeStoredSessionEntry(left) === encodeStoredSessionEntry(right);
@@ -1,10 +1,20 @@
1
1
  import type { MuleEvent } from "@fungi.computer/mule";
2
2
  import * as Effect from "effect/Effect";
3
3
  import type * as Scope from "effect/Scope";
4
- import type { SessionProgress, SessionToolOutput } from "../../client.js";
4
+ import type { SessionProgress, SessionProgressDelta, SessionToolOutput } from "../../client.js";
5
5
  import { ShiitakeError } from "../../errors.js";
6
6
  import type { SqliteOwner } from "../contracts.js";
7
7
  import type { AcceptedCommand } from "./session-store.js";
8
+ /** Per-watch position; a gap in the retained tail requires a fresh baseline. */
9
+ export type ProgressPosition = Readonly<{
10
+ messageId: string;
11
+ revision: number;
12
+ text: number;
13
+ thinking: number;
14
+ }>;
15
+ export declare function progressPosition(value: SessionProgress | null): ProgressPosition | null;
16
+ /** Undefined means continuity was lost; null means no new model progress. */
17
+ export declare function readProcessProgressDelta(owner: SqliteOwner, sessionId: string, workId: string, after: ProgressPosition | null): SessionProgressDelta | null | undefined;
8
18
  export declare function readProcessToolOutputForWork(owner: SqliteOwner, sessionId: string, workId: string): readonly SessionToolOutput[];
9
19
  /** Run-scoped, replaceable tool snapshots: never stored in Woodstock. */
10
20
  export declare function openToolProgress(owner: SqliteOwner, accepted: AcceptedCommand): Effect.Effect<{
@@ -3,6 +3,92 @@ import * as Schema from "effect/Schema";
3
3
  import * as Result from "effect/Result";
4
4
  import { ShiitakeError } from "../../errors.js";
5
5
  import { publishOwnerObservation } from "../observation-kernel.js";
6
+ const tailBytes = 65_536;
7
+ const chunkUnits = 2048;
8
+ const encoder = new TextEncoder();
9
+ const emptyTail = () => ({ chunks: [], length: 0, bytes: 0 });
10
+ /** Bounded chunks: no concatenation or encoding of the reply-so-far. */
11
+ function appendTail(tail, append) {
12
+ for (let index = 0; index < append.length;) {
13
+ let end = Math.min(index + chunkUnits, append.length);
14
+ // Do not cut a Unicode scalar when bounding a provider's large delta.
15
+ const code = append.charCodeAt(end - 1);
16
+ if (end < append.length && code >= 0xd800 && code <= 0xdbff)
17
+ end--;
18
+ const text = append.slice(index, end);
19
+ const bytes = encoder.encode(text).byteLength;
20
+ const last = tail.chunks.at(-1);
21
+ if (last !== undefined && last.text.length + text.length <= chunkUnits) {
22
+ last.text += text;
23
+ last.bytes += bytes;
24
+ }
25
+ else {
26
+ tail.chunks.push({ offset: tail.length, text, bytes });
27
+ }
28
+ tail.length += text.length;
29
+ tail.bytes += bytes;
30
+ index = end;
31
+ while (tail.bytes > tailBytes && tail.chunks.length > 1) {
32
+ const dropped = tail.chunks.shift();
33
+ if (dropped !== undefined)
34
+ tail.bytes -= dropped.bytes;
35
+ }
36
+ }
37
+ }
38
+ function tailOffset(tail) {
39
+ return tail.chunks[0]?.offset ?? 0;
40
+ }
41
+ function tailAfter(tail, offset) {
42
+ return tail.chunks
43
+ .flatMap((chunk) => chunk.offset + chunk.text.length <= offset
44
+ ? []
45
+ : [chunk.text.slice(Math.max(0, offset - chunk.offset))])
46
+ .join("");
47
+ }
48
+ function snapshot(value) {
49
+ return value === null
50
+ ? null
51
+ : {
52
+ messageId: value.messageId,
53
+ revision: value.revision,
54
+ text: tailAfter(value.text, tailOffset(value.text)),
55
+ thinking: tailAfter(value.thinking, tailOffset(value.thinking)),
56
+ textOffset: tailOffset(value.text),
57
+ thinkingOffset: tailOffset(value.thinking),
58
+ textTruncated: tailOffset(value.text) > 0,
59
+ thinkingTruncated: tailOffset(value.thinking) > 0,
60
+ };
61
+ }
62
+ export function progressPosition(value) {
63
+ return value === null
64
+ ? null
65
+ : {
66
+ messageId: value.messageId,
67
+ revision: value.revision,
68
+ text: value.textOffset + value.text.length,
69
+ thinking: value.thinkingOffset + value.thinking.length,
70
+ };
71
+ }
72
+ /** Undefined means continuity was lost; null means no new model progress. */
73
+ export function readProcessProgressDelta(owner, sessionId, workId, after) {
74
+ const current = progressByOwner.get(owner)?.get(sessionId);
75
+ const value = current?.workId === workId ? current.value : null;
76
+ if (value === null)
77
+ return null;
78
+ const position = after?.messageId === value.messageId ? after : null;
79
+ if (position !== null && position.revision >= value.revision)
80
+ return null;
81
+ const text = position?.text ?? 0;
82
+ const thinking = position?.thinking ?? 0;
83
+ if (text < tailOffset(value.text) || thinking < tailOffset(value.thinking))
84
+ return undefined;
85
+ return {
86
+ messageId: value.messageId,
87
+ revision: value.revision,
88
+ text: { offset: text, append: tailAfter(value.text, text) },
89
+ thinking: { offset: thinking, append: tailAfter(value.thinking, thinking) },
90
+ };
91
+ }
6
92
  const progressByOwner = new WeakMap();
7
93
  export function readProcessToolOutputForWork(owner, sessionId, workId) {
8
94
  const current = progressByOwner.get(owner)?.get(sessionId);
@@ -96,23 +182,12 @@ function rejected(message) {
96
182
  }
97
183
  /** Read the current process-local progress value for one addressed Session. */
98
184
  export function readProcessProgress(owner, sessionId) {
99
- return progressFor(owner).get(sessionId)?.value ?? null;
185
+ return snapshot(progressFor(owner).get(sessionId)?.value ?? null);
100
186
  }
101
187
  /** Read progress only when it belongs to the current durable work head. */
102
188
  export function readProcessProgressForWork(owner, sessionId, workId) {
103
189
  const current = progressFor(owner).get(sessionId);
104
- return current?.workId === workId ? current.value : null;
105
- }
106
- function textFor(event) {
107
- let text = "";
108
- let thinking = "";
109
- for (const content of event.value.content) {
110
- if (content.type === "text")
111
- text += content.text;
112
- if (content.type === "thinking")
113
- thinking += content.thinking;
114
- }
115
- return { text, thinking };
190
+ return snapshot(current?.workId === workId ? current.value : null);
116
191
  }
117
192
  /**
118
193
  * Keep live model progress in the current process only.
@@ -124,7 +199,23 @@ export function openProgress(owner, accepted, complete) {
124
199
  return Effect.gen(function* () {
125
200
  const current = yield* openProcessProgress(owner, accepted);
126
201
  let started;
202
+ let publishedLength = 0;
203
+ let flushTimer;
204
+ const cancelFlush = () => {
205
+ if (flushTimer !== undefined)
206
+ clearTimeout(flushTimer);
207
+ flushTimer = undefined;
208
+ };
209
+ const publish = () => {
210
+ cancelFlush();
211
+ const value = current.value;
212
+ if (value === null)
213
+ return;
214
+ publishedLength = value.text.length + value.thinking.length;
215
+ publishOwnerObservation(owner, accepted.sessionId, "progress");
216
+ };
127
217
  const clear = Effect.sync(() => {
218
+ cancelFlush();
128
219
  const hadProgress = current.value !== null;
129
220
  current.value = null;
130
221
  if (hadProgress)
@@ -134,8 +225,10 @@ export function openProgress(owner, accepted, complete) {
134
225
  yield* Effect.addFinalizer(() => clear);
135
226
  return Object.freeze({
136
227
  started: (event) => Effect.sync(() => {
228
+ cancelFlush();
137
229
  started = event;
138
230
  current.value = null;
231
+ publishedLength = 0;
139
232
  }),
140
233
  update: (event) => Effect.suspend(() => {
141
234
  const messageStart = started;
@@ -145,20 +238,28 @@ export function openProgress(owner, accepted, complete) {
145
238
  messageStart.turn.value !== event.turn.value ||
146
239
  messageStart.message.value !== event.message.value)
147
240
  return Effect.fail(rejected("Mule progress identity conflicts"));
148
- const previous = current.value;
149
- const content = textFor(event);
150
- if (previous?.text === content.text &&
151
- previous.thinking === content.thinking)
241
+ const delta = event.event;
242
+ if ((delta.type !== "text_delta" && delta.type !== "thinking_delta") ||
243
+ delta.delta.length === 0)
152
244
  return Effect.void;
153
- current.value = Object.freeze({
245
+ const first = current.value === null;
246
+ const value = current.value ?? {
154
247
  messageId: event.message.value,
155
- revision: (previous?.revision ?? 0) + 1,
156
- text: content.text,
157
- thinking: content.thinking,
158
- textTruncated: false,
159
- thinkingTruncated: false,
160
- });
161
- publishOwnerObservation(owner, accepted.sessionId, "progress");
248
+ revision: 0,
249
+ text: emptyTail(),
250
+ thinking: emptyTail(),
251
+ };
252
+ appendTail(delta.type === "text_delta" ? value.text : value.thinking, delta.delta);
253
+ value.revision++;
254
+ current.value = value;
255
+ const length = value.text.length + value.thinking.length;
256
+ // Coalesce tiny tokens, but flush a paused provider within 100 ms.
257
+ // One scope-owned timer holds no reply text and never writes SQLite.
258
+ if (first || length - publishedLength >= 4096) {
259
+ publish();
260
+ }
261
+ else
262
+ flushTimer ??= setTimeout(publish, 100);
162
263
  return Effect.void;
163
264
  }),
164
265
  terminal: (event) => clear.pipe(Effect.andThen(complete(event))),
@@ -1,4 +1,5 @@
1
1
  import type { BirdDogDelegator, BirdDogMail } from "@fungi.computer/bird-dog";
2
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
3
  import type { SqliteOwner } from "../contracts.js";
3
4
  /** Woodstock keeps each Session's delegator of record beside the Session. */
4
5
  export declare function prepareSessionDelegators(owner: SqliteOwner): void;
@@ -9,4 +10,4 @@ export declare function prepareSessionDelegators(owner: SqliteOwner): void;
9
10
  */
10
11
  export declare function recordSessionDelegator(owner: SqliteOwner, sessionId: string, mail: BirdDogMail | undefined): void;
11
12
  /** The Session's delegator of record, if any Agent has sent it work. */
12
- export declare function readSessionDelegator(owner: SqliteOwner, sessionId: string): BirdDogDelegator | undefined;
13
+ export declare function readSessionDelegator(owner: WatchdogSqliteOwner, sessionId: string): BirdDogDelegator | undefined;
@@ -167,6 +167,7 @@ export type EnqueueAcceptedWork = (work: Readonly<{
167
167
  jobId: string;
168
168
  sessionId: string;
169
169
  commandFactId: string;
170
+ delivery: "steer" | "followUp";
170
171
  }>) => void;
171
172
  export type RecordAcceptedCancellation = (input: Readonly<{
172
173
  jobId: string;
@@ -192,6 +193,8 @@ export type ForkSessionAtInput = SelectSessionBranchInput;
192
193
  export declare function hasStoredSession(owner: SqliteOwner, sessionId: string): boolean;
193
194
  /** Read one retained Session entry by its globally unique row identity. */
194
195
  export declare function readSessionEntryById(owner: SqliteOwner, id: string): StoredSessionRow | undefined;
196
+ /** A written run terminal closes the window for attaching another steer. */
197
+ export declare function hasRecordedRunTerminal(owner: SqliteOwner, sessionId: string, workId: string): boolean;
195
198
  /** Host-owned metadata identity, interpreted by SQLite's JSON decoder only. */
196
199
  export type AdmissionIdentity = Readonly<{
197
200
  path: string;
@@ -332,9 +335,18 @@ export declare const lineageRowLimit = 200;
332
335
  * deduplicating; the fork walk keeps every link for its whole-lineage check.
333
336
  */
334
337
  declare const lineageWalks: {
335
- readonly selected: "WITH RECURSIVE branch (id, parent_id, sequence, state) AS (\n SELECT id, parent_id, sequence, state\n FROM session_entries\n WHERE session_id = ? AND id = ?\n UNION ALL\n SELECT parent.id, parent.parent_id, parent.sequence, parent.state\n FROM session_entries AS parent\n JOIN branch AS child ON parent.id = child.parent_id\n WHERE parent.session_id = ? AND child.sequence > ?\n AND +parent.sequence < child.sequence\n )";
336
- readonly fork: "WITH RECURSIVE branch (id, parent_id, sequence, state) AS (\n SELECT id, parent_id, sequence, state\n FROM session_entries\n WHERE session_id = ? AND id = ? AND state != 'streaming'\n UNION\n SELECT parent.id, parent.parent_id, parent.sequence, parent.state\n FROM session_entries AS parent\n JOIN branch AS child ON parent.id = child.parent_id\n WHERE parent.session_id = ? AND child.sequence > ?\n AND parent.state != 'streaming'\n )";
338
+ readonly selected: "WITH RECURSIVE branch (id, parent_id, sequence, state) AS (\n SELECT id, parent_id, sequence, state\n FROM session_entries\n WHERE session_id = ? AND id = ?\n UNION ALL\n SELECT parent.id, parent.parent_id, parent.sequence, parent.state\n FROM branch AS child\n CROSS JOIN session_entries AS parent\n WHERE parent.id = child.parent_id AND parent.session_id = ?\n AND child.sequence > ?\n AND +parent.sequence < child.sequence\n )";
339
+ readonly fork: "WITH RECURSIVE branch (id, parent_id, sequence, state) AS (\n SELECT id, parent_id, sequence, state\n FROM session_entries\n WHERE session_id = ? AND id = ? AND state != 'streaming'\n UNION\n SELECT parent.id, parent.parent_id, parent.sequence, parent.state\n FROM branch AS child\n CROSS JOIN session_entries AS parent\n WHERE parent.id = child.parent_id AND parent.session_id = ?\n AND child.sequence > ?\n AND parent.state != 'streaming'\n )";
337
340
  };
341
+ /**
342
+ * One synchronous, transaction-consistent Session baseline. Activity, timeline,
343
+ * extension projections and context figures share its identity-only ancestry;
344
+ * asynchronous model description runs after this scope has ended. Nothing is
345
+ * retained between reads, and payload windows keep their row and byte bounds.
346
+ */
347
+ export declare function withSessionRead<A>(owner: SqliteOwner, sessionId: string, read: () => A, before?: number): A;
348
+ /** Join the shared Session baseline, or keep a direct projection atomic. */
349
+ export declare function withSessionReadTransaction<A>(owner: SqliteOwner, sessionId: string, read: () => A): A;
338
350
  /**
339
351
  * One root-to-tip lineage. Its integrity is enforced where entries are
340
352
  * written (`guardSessionLineage`), so locating it reads only its tip and its