@fungi.computer/shiitake 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +1 -1
  3. package/dist/agent-control.js +40 -21
  4. package/dist/client-visitor.d.ts +2 -0
  5. package/dist/client-visitor.js +6 -1
  6. package/dist/client-wire.d.ts +4 -0
  7. package/dist/client-wire.js +26 -15
  8. package/dist/client.d.ts +23 -11
  9. package/dist/client.js +22 -0
  10. package/dist/cloudflare.d.ts +20 -0
  11. package/dist/cloudflare.js +4 -1
  12. package/dist/delegation.d.ts +5 -0
  13. package/dist/delegation.js +24 -8
  14. package/dist/effect.d.ts +1 -1
  15. package/dist/errors.d.ts +2 -2
  16. package/dist/errors.js +2 -0
  17. package/dist/extensions.d.ts +1 -1
  18. package/dist/fleet.d.ts +14 -2
  19. package/dist/fleet.js +81 -19
  20. package/dist/http-routes.d.ts +12 -16
  21. package/dist/http.d.ts +1 -0
  22. package/dist/http.js +1 -0
  23. package/dist/index.d.ts +1 -1
  24. package/dist/internal/admission-door.d.ts +1 -1
  25. package/dist/internal/admission-door.js +8 -4
  26. package/dist/internal/contracts.d.ts +5 -0
  27. package/dist/internal/coordinator.js +107 -50
  28. package/dist/internal/effect-contracts.d.ts +5 -0
  29. package/dist/internal/extension-runtime.js +5 -3
  30. package/dist/internal/observation-kernel.js +12 -3
  31. package/dist/internal/promise-facade.js +2 -0
  32. package/dist/internal/run-scope.d.ts +1 -0
  33. package/dist/internal/run-scope.js +29 -7
  34. package/dist/internal/runtime-coordinator.d.ts +14 -0
  35. package/dist/internal/runtime-coordinator.js +57 -10
  36. package/dist/internal/runtime-core.d.ts +3 -0
  37. package/dist/internal/runtime-core.js +1 -0
  38. package/dist/internal/runtime-reconciliation.js +4 -1
  39. package/dist/internal/session-context.d.ts +5 -5
  40. package/dist/internal/session-context.js +80 -83
  41. package/dist/internal/session-status.d.ts +4 -2
  42. package/dist/internal/session-status.js +22 -4
  43. package/dist/internal/woodstock/client-projection.js +5 -11
  44. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  45. package/dist/internal/woodstock/fleet-store.js +24 -4
  46. package/dist/internal/woodstock/index.d.ts +3 -3
  47. package/dist/internal/woodstock/index.js +18 -7
  48. package/dist/internal/woodstock/progress.d.ts +13 -1
  49. package/dist/internal/woodstock/progress.js +130 -24
  50. package/dist/internal/woodstock/session-delegator.d.ts +2 -1
  51. package/dist/internal/woodstock/session-store.d.ts +14 -2
  52. package/dist/internal/woodstock/session-store.js +329 -66
  53. package/dist/node.js +11 -2
  54. package/dist/server-visitor-stream.js +33 -24
  55. package/dist/testing.d.ts +2 -0
  56. package/dist/testing.js +2 -0
  57. package/docs/api/agent-control.md +3 -2
  58. package/docs/api/client.md +79 -19
  59. package/docs/api/cloudflare.md +16 -0
  60. package/docs/api/delegation.md +84 -50
  61. package/docs/api/effect.md +85 -7
  62. package/docs/api/extensions.md +8 -8
  63. package/docs/api/fleet.md +118 -29
  64. package/docs/api/http.md +28 -0
  65. package/docs/api/index.md +39 -15
  66. package/docs/api/testing.md +20 -7
  67. package/package.json +5 -5
@@ -14,103 +14,100 @@ function settledContextTokens(store, sessionId) {
14
14
  return messages.length === 0 ? null : estimateContextTokens(messages).tokens;
15
15
  }
16
16
  /**
17
- * The Session's context use as compaction measures it, against the window of
18
- * the model a run would select now. Undefined while the Session has no
19
- * context or its context cannot be read; a window the host cannot describe is
20
- * null, never guessed.
17
+ * Read the stored figure synchronously with the Session baseline. Only model
18
+ * description remains in the returned Effect, outside the SQLite transaction
19
+ * and the request's ancestry scope. Undefined without readable context; a model
20
+ * window the host cannot describe is null, never guessed.
21
21
  */
22
- export function readSessionContext(options, sessionId, cursor, cache = true) {
23
- return Effect.gen(function* () {
24
- const savedPreference = yield* Effect.result(Effect.try({
25
- try: () => readSessionModelPreference(options.store, sessionId),
26
- catch: (error) => error,
27
- }));
28
- if (savedPreference._tag === "Failure") {
29
- reportCatchFailure("session-context.preference", savedPreference.failure);
30
- return undefined;
31
- }
32
- const preference = savedPreference.success;
33
- const preferenceKey = JSON.stringify(preference);
34
- const ownerCache = contextByOwner.get(options) ?? new Map();
35
- contextByOwner.set(options, ownerCache);
36
- const previous = cache ? ownerCache.get(sessionId) : undefined;
37
- const describe = options.model.describe;
38
- if (previous?.cursor === cursor && previous.preference === preferenceKey) {
39
- if (previous.value === undefined || describe === undefined)
40
- return previous.value;
41
- const previousValue = previous.value;
42
- // A null Session override follows a mutable Agent default. Check the
43
- // selected window without decoding history again; a changed window is
44
- // a distinct effective preference for this cursor.
45
- return yield* describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => {
46
- if (previousValue.window === contextWindow)
47
- return previousValue;
48
- const value = Object.freeze({
49
- tokens: previousValue.tokens,
50
- window: contextWindow,
51
- });
52
- if (cache && ownerCache.get(sessionId)?.cursor === cursor)
53
- ownerCache.set(sessionId, {
54
- cursor,
55
- preference: preferenceKey,
56
- value,
57
- });
58
- return value;
59
- }), Effect.catch((error) => Effect.sync(() => {
60
- reportCatchFailure("session-context.describe", error);
22
+ export function prepareSessionContext(options, sessionId, cursor, cache = true) {
23
+ let preference;
24
+ try {
25
+ preference = readSessionModelPreference(options.store, sessionId);
26
+ }
27
+ catch (error) {
28
+ reportCatchFailure("session-context.preference", error);
29
+ return Effect.succeed(undefined);
30
+ }
31
+ const preferenceKey = JSON.stringify(preference);
32
+ const ownerCache = contextByOwner.get(options) ?? new Map();
33
+ contextByOwner.set(options, ownerCache);
34
+ const previous = cache ? ownerCache.get(sessionId) : undefined;
35
+ const describe = options.model.describe;
36
+ if (previous?.cursor === cursor && previous.preference === preferenceKey) {
37
+ if (previous.value === undefined || describe === undefined)
38
+ return Effect.succeed(previous.value);
39
+ const previousValue = previous.value;
40
+ // A null Session override follows a mutable Agent default. Check the
41
+ // selected window without decoding history again; a changed window is
42
+ // a distinct effective preference for this cursor.
43
+ return describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => {
44
+ if (previousValue.window === contextWindow)
61
45
  return previousValue;
62
- })));
63
- }
64
- // At most one cached whole-window read per Session, cursor, and
65
- // preference: a late describe may publish only while this cursor still
66
- // owns the cache entry. Reads before the tip bypass the cache and each read
67
- // the whole window. A cold-cache connection also reads the whole kept
68
- // window.
69
- const contextRead = yield* Effect.result(Effect.try({
70
- try: () => settledContextTokens(options.store, sessionId),
71
- catch: (error) => error,
72
- }));
73
- if (contextRead._tag === "Failure") {
74
- reportCatchFailure("session-context.figure", contextRead.failure);
75
- return undefined;
76
- }
77
- const tokens = contextRead.success;
78
- if (tokens === null) {
79
- if (cache)
46
+ const value = Object.freeze({
47
+ tokens: previousValue.tokens,
48
+ window: contextWindow,
49
+ });
50
+ if (cache && ownerCache.get(sessionId)?.cursor === cursor)
80
51
  ownerCache.set(sessionId, {
81
52
  cursor,
82
53
  preference: preferenceKey,
83
- value: undefined,
54
+ value,
84
55
  });
85
- return undefined;
86
- }
87
- if (describe === undefined) {
88
- const value = Object.freeze({ tokens, window: null });
89
- if (cache)
90
- ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
91
56
  return value;
92
- }
93
- // Publish the decoded figure before yielding to the host's model
94
- // description. Concurrent watchers then reuse these tokens rather than
95
- // decoding the same Session window while `describe` is in flight.
57
+ }), Effect.catch((error) => Effect.sync(() => {
58
+ reportCatchFailure("session-context.describe", error);
59
+ return previousValue;
60
+ })));
61
+ }
62
+ // At most one cached whole-window read per Session, cursor, and
63
+ // preference: a late describe may publish only while this cursor still
64
+ // owns the cache entry. Reads before the tip bypass the cache and each read
65
+ // the whole window. A cold-cache connection also reads the whole kept
66
+ // window.
67
+ let tokens;
68
+ try {
69
+ tokens = settledContextTokens(options.store, sessionId);
70
+ }
71
+ catch (error) {
72
+ reportCatchFailure("session-context.figure", error);
73
+ return Effect.succeed(undefined);
74
+ }
75
+ if (tokens === null) {
96
76
  if (cache)
97
77
  ownerCache.set(sessionId, {
98
78
  cursor,
99
79
  preference: preferenceKey,
100
- value: Object.freeze({ tokens, window: null }),
80
+ value: undefined,
101
81
  });
102
- // A null Session override follows a mutable Agent default. Resolve through
103
- // the host's turn-model selector on every read, without acquiring a turn.
104
- const window = yield* describe({
105
- sessionId,
106
- preference,
107
- }).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
108
- reportCatchFailure("session-context.describe", error);
109
- return null;
110
- })));
82
+ return Effect.succeed(undefined);
83
+ }
84
+ if (describe === undefined) {
85
+ const value = Object.freeze({ tokens, window: null });
86
+ if (cache)
87
+ ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
88
+ return Effect.succeed(value);
89
+ }
90
+ // Publish the decoded figure before yielding to the host's model
91
+ // description. Concurrent watchers then reuse these tokens rather than
92
+ // decoding the same Session window while `describe` is in flight.
93
+ if (cache)
94
+ ownerCache.set(sessionId, {
95
+ cursor,
96
+ preference: preferenceKey,
97
+ value: Object.freeze({ tokens, window: null }),
98
+ });
99
+ // A null Session override follows a mutable Agent default. Resolve through
100
+ // the host's turn-model selector on every read, without acquiring a turn.
101
+ return describe({
102
+ sessionId,
103
+ preference,
104
+ }).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
105
+ reportCatchFailure("session-context.describe", error);
106
+ return null;
107
+ })), Effect.map((window) => {
111
108
  const value = Object.freeze({ tokens, window });
112
109
  if (cache && ownerCache.get(sessionId)?.cursor === cursor)
113
110
  ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
114
111
  return value;
115
- });
112
+ }));
116
113
  }
@@ -1,11 +1,13 @@
1
1
  import type { WatchdogQueueHead } from "@fungi.computer/watchdog/effect";
2
2
  import type { AgentRunningWork, SessionActivity, SessionProgress, SessionToolOutput } from "../client.js";
3
3
  import type { SqliteOwner } from "./contracts.js";
4
- /** Public activity facts plus the process-local progress read with them. */
4
+ /** Activity facts plus an explicit, on-demand bounded progress read. */
5
5
  export type ObservedActivity = Readonly<{
6
6
  facts: SessionActivity;
7
- progress: SessionProgress | null;
7
+ readProgress: () => SessionProgress | null;
8
8
  toolOutput: readonly SessionToolOutput[];
9
9
  }>;
10
+ /** Project only the occupying Session's activity, without disclosing its data. */
11
+ export declare function readSessionPhase(owner: SqliteOwner, sessionId: string, head: WatchdogQueueHead): AgentRunningWork["phase"];
10
12
  /** Join Watchdog authority with Woodstock facts and process-local progress. */
11
13
  export declare function readSessionActivity(owner: SqliteOwner, sessionId: string, head: WatchdogQueueHead, runningWork: AgentRunningWork | null): ObservedActivity;
@@ -1,4 +1,20 @@
1
- import { readProcessProgressForWork, readProcessToolOutputForWork, readProcessCompaction, readLatestLiveRetryForWork, readActiveToolForWork, readLastModelChoiceSequence, } from "./woodstock/index.js";
1
+ import { readProcessProgressForWork, readProcessToolOutputForWork, readProcessCompaction, readLatestLiveRetryForWork, readActiveToolForWork, readLastModelChoiceSequence, readSessionAttention, hasProcessTextForWork, } from "./woodstock/index.js";
2
+ /** Project only the occupying Session's activity, without disclosing its data. */
3
+ export function readSessionPhase(owner, sessionId, head) {
4
+ const { facts } = readSessionActivity(owner, sessionId, head, null);
5
+ if (facts.cancellationRequested)
6
+ return "cancelling";
7
+ if (readSessionAttention(owner, sessionId).some((item) => item.inbox !== false && item.waitingOn === undefined))
8
+ return "waiting_on_you";
9
+ if (facts.compaction !== null || facts.retry !== null)
10
+ return "thinking";
11
+ if (facts.activeTool !== null)
12
+ return "running_tool";
13
+ if (head.state === "running" &&
14
+ hasProcessTextForWork(owner, sessionId, head.jobId))
15
+ return "streaming";
16
+ return "thinking";
17
+ }
2
18
  /** Join Watchdog authority with Woodstock facts and process-local progress. */
3
19
  export function readSessionActivity(owner, sessionId, head, runningWork) {
4
20
  const lastModelChoiceSequence = readLastModelChoiceSequence(owner, sessionId);
@@ -12,7 +28,7 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
12
28
  lastModelChoiceSequence,
13
29
  compaction: null,
14
30
  },
15
- progress: null,
31
+ readProgress: () => null,
16
32
  toolOutput: [],
17
33
  };
18
34
  if (head.state === "queued")
@@ -27,7 +43,7 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
27
43
  lastModelChoiceSequence,
28
44
  compaction: null,
29
45
  },
30
- progress: null,
46
+ readProgress: () => null,
31
47
  toolOutput: [],
32
48
  };
33
49
  const latestRetry = readLatestLiveRetryForWork(owner, sessionId, head.jobId);
@@ -49,7 +65,9 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
49
65
  lastModelChoiceSequence,
50
66
  compaction: readProcessCompaction(owner, sessionId),
51
67
  },
52
- progress: readProcessProgressForWork(owner, sessionId, head.jobId),
68
+ // Only baselines need a flattened bounded tail. Live subscribers read
69
+ // appends directly and must not materialize it on every token.
70
+ readProgress: () => readProcessProgressForWork(owner, sessionId, head.jobId),
53
71
  toolOutput: readProcessToolOutputForWork(owner, sessionId, head.jobId),
54
72
  };
55
73
  }
@@ -5,7 +5,7 @@ import { readSessionDelegator } from "./session-delegator.js";
5
5
  import { ShiitakeError } from "../../errors.js";
6
6
  import { decodeWoodstockCursor, encodeWoodstockCursor } from "./cursor.js";
7
7
  import { operatorClientMessage, promptCommand } from "./session-entry.js";
8
- import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, hasSessionWorkTerminal, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, } from "./session-store.js";
8
+ import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, hasSessionWorkTerminal, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, withSessionReadTransaction, } from "./session-store.js";
9
9
  const maximumPagePayloadBytes = 2_097_152;
10
10
  const replayPageLimit = 200;
11
11
  const maximumRowsPerPage = lineageRowLimit;
@@ -344,7 +344,7 @@ function resolveHistoryAnchor(owner, lineage, before) {
344
344
  * history, including what compaction summarized away.
345
345
  */
346
346
  export function readClientSnapshot(owner, sessionId, options, activity, projectExtensions = () => ({})) {
347
- return owner.transactionSync(() => {
347
+ return withSessionReadTransaction(owner, sessionId, () => {
348
348
  const pending = readPendingPrompts(owner, sessionId);
349
349
  const lineage = readSessionLineage(owner, { sessionId });
350
350
  if (lineage === undefined)
@@ -359,7 +359,7 @@ export function readClientSnapshot(owner, sessionId, options, activity, projectE
359
359
  compactionNotices: Object.freeze([]),
360
360
  pending,
361
361
  activity: activity.facts,
362
- progress: activity.progress,
362
+ progress: activity.readProgress(),
363
363
  toolOutput: activity.toolOutput,
364
364
  extensions: projectExtensions(),
365
365
  attention: readSessionAttention(owner, sessionId),
@@ -394,7 +394,7 @@ export function readClientSnapshot(owner, sessionId, options, activity, projectE
394
394
  compactionNotices: compactionNoticesIn(consumedRows),
395
395
  pending,
396
396
  activity: activity.facts,
397
- progress: options.before === undefined ? activity.progress : null,
397
+ progress: options.before === undefined ? activity.readProgress() : null,
398
398
  toolOutput: options.before === undefined ? activity.toolOutput : [],
399
399
  extensions: projectExtensions(),
400
400
  attention: readSessionAttention(owner, sessionId),
@@ -442,12 +442,6 @@ function replayFrames(owner, lineage, start, delivery, activity, extensions, att
442
442
  ? { ...frame, sandboxFailures: page.sandboxFailures }
443
443
  : frame),
444
444
  ];
445
- if (delivery === "live" && !page.hasMore && activity.progress !== null)
446
- frames.push(Object.freeze({
447
- kind: "progress",
448
- progress: activity.progress,
449
- activity: activity.facts,
450
- }));
451
445
  return Object.freeze({
452
446
  kind: "page",
453
447
  frames: Object.freeze(!page.hasMore
@@ -468,7 +462,7 @@ export function readClientReplayPage(owner, sessionId, after, delivery, activity
468
462
  const position = after === undefined ? undefined : decodeWoodstockCursor(after);
469
463
  if (after !== undefined && position === undefined)
470
464
  return resetReplayPage;
471
- return owner.transactionSync(() => {
465
+ return withSessionReadTransaction(owner, sessionId, () => {
472
466
  const pending = readPendingPrompts(owner, sessionId);
473
467
  const lineage = readSessionLineage(owner, { sessionId });
474
468
  if (lineage === undefined) {
@@ -27,10 +27,11 @@ export declare function createFleetDelegationStore(storage: WatchdogSqliteOwner)
27
27
  read: (requestId: string) => {
28
28
  readonly [x: string]: string | number | ArrayBuffer | null;
29
29
  } | undefined;
30
- /** Every recipient-accepted delegation, newest first. */
31
- sent: () => readonly {
30
+ /** Accepted sends not yet observed settled, for owner-local archive admission. */
31
+ unsettled: () => readonly {
32
32
  readonly [x: string]: string | number | ArrayBuffer | null;
33
33
  }[];
34
+ recordSettled(requestId: string): void;
34
35
  reserve(requestId: string, input: string, mail: string): void;
35
36
  recordReceipt(requestId: string, receipt: string, expectedMail: string, acceptedMail: string): void;
36
37
  recordHeld(requestId: string): void;
@@ -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);
@@ -523,10 +533,11 @@ export function recordLiveRetryEvent(owner, accepted, event, turn) {
523
533
  sameIdentity(current.event.run, event.run));
524
534
  }).pipe(Effect.mapError(listenerFailure));
525
535
  }
526
- const listenerFailure = () => ({
536
+ const listenerFailure = (cause) => ({
527
537
  _tag: "MuleError",
528
538
  phase: "listener",
529
539
  message: "Woodstock could not record the Mule event",
540
+ cause,
530
541
  });
531
542
  /**
532
543
  * Open the one scoped listener that projects Mule's exact event stream into
@@ -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<{
@@ -22,6 +32,8 @@ export declare function openToolProgress(owner: SqliteOwner, accepted: AcceptedC
22
32
  export declare function readProcessProgress(owner: SqliteOwner, sessionId: string): SessionProgress | null;
23
33
  /** Read progress only when it belongs to the current durable work head. */
24
34
  export declare function readProcessProgressForWork(owner: SqliteOwner, sessionId: string, workId: string): SessionProgress | null;
35
+ /** Read a phase fact without flattening another Session's private text tails. */
36
+ export declare function hasProcessTextForWork(owner: SqliteOwner, sessionId: string, workId: string): boolean;
25
37
  export type ProgressMessageStart = Extract<MuleEvent, {
26
38
  type: "message_start";
27
39
  }>;