@fungi.computer/shiitake 0.5.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 (88) hide show
  1. package/README.md +59 -21
  2. package/dist/agent-control.d.ts +94 -2
  3. package/dist/agent-control.js +147 -26
  4. package/dist/client-entry.d.ts +20 -0
  5. package/dist/client-entry.js +5 -0
  6. package/dist/client-visitor.d.ts +33 -0
  7. package/dist/client-visitor.js +231 -0
  8. package/dist/client-wire.d.ts +9 -0
  9. package/dist/client-wire.js +24 -3
  10. package/dist/client.d.ts +27 -6
  11. package/dist/client.js +23 -0
  12. package/dist/cloudflare.d.ts +20 -0
  13. package/dist/cloudflare.js +4 -1
  14. package/dist/delegation.d.ts +79 -53
  15. package/dist/delegation.js +26 -21
  16. package/dist/effect.d.ts +26 -1
  17. package/dist/effect.js +14 -1
  18. package/dist/errors.d.ts +2 -2
  19. package/dist/errors.js +2 -0
  20. package/dist/extensions.d.ts +22 -3
  21. package/dist/extensions.js +1 -1
  22. package/dist/fleet.d.ts +25 -2
  23. package/dist/fleet.js +108 -24
  24. package/dist/hosted.js +3 -0
  25. package/dist/http-routes.d.ts +23 -9
  26. package/dist/http.d.ts +1 -0
  27. package/dist/http.js +1 -0
  28. package/dist/index.d.ts +1 -1
  29. package/dist/internal/accepted-run-executor.js +2 -1
  30. package/dist/internal/admission-door.d.ts +6 -1
  31. package/dist/internal/admission-door.js +33 -3
  32. package/dist/internal/contracts.d.ts +19 -1
  33. package/dist/internal/coordinator.js +115 -52
  34. package/dist/internal/effect-client.js +1 -1
  35. package/dist/internal/effect-contracts.d.ts +9 -2
  36. package/dist/internal/extension-command-registry.d.ts +2 -1
  37. package/dist/internal/extension-command-registry.js +3 -1
  38. package/dist/internal/extension-runtime.d.ts +3 -1
  39. package/dist/internal/extension-runtime.js +18 -8
  40. package/dist/internal/model-acquisition.js +2 -0
  41. package/dist/internal/observation-kernel.d.ts +2 -0
  42. package/dist/internal/observation-kernel.js +17 -6
  43. package/dist/internal/promise-facade.js +2 -0
  44. package/dist/internal/run-scope.d.ts +14 -0
  45. package/dist/internal/run-scope.js +58 -15
  46. package/dist/internal/runtime-coordinator.d.ts +14 -0
  47. package/dist/internal/runtime-coordinator.js +60 -11
  48. package/dist/internal/runtime-core.d.ts +3 -0
  49. package/dist/internal/runtime-core.js +1 -0
  50. package/dist/internal/runtime-reconciliation.js +123 -25
  51. package/dist/internal/session-context.d.ts +5 -5
  52. package/dist/internal/session-context.js +80 -83
  53. package/dist/internal/session-status.d.ts +2 -2
  54. package/dist/internal/session-status.js +5 -3
  55. package/dist/internal/woodstock/agent-reply.d.ts +5 -0
  56. package/dist/internal/woodstock/agent-reply.js +3 -0
  57. package/dist/internal/woodstock/client-projection.js +13 -11
  58. package/dist/internal/woodstock/fleet-store.d.ts +3 -2
  59. package/dist/internal/woodstock/fleet-store.js +24 -4
  60. package/dist/internal/woodstock/index.d.ts +7 -3
  61. package/dist/internal/woodstock/index.js +23 -6
  62. package/dist/internal/woodstock/progress.d.ts +11 -1
  63. package/dist/internal/woodstock/progress.js +126 -25
  64. package/dist/internal/woodstock/session-delegator.d.ts +13 -0
  65. package/dist/internal/woodstock/session-delegator.js +44 -0
  66. package/dist/internal/woodstock/session-entry.d.ts +12 -2
  67. package/dist/internal/woodstock/session-store.d.ts +16 -2
  68. package/dist/internal/woodstock/session-store.js +349 -66
  69. package/dist/internal/woodstock/work-senders.d.ts +32 -0
  70. package/dist/internal/woodstock/work-senders.js +70 -0
  71. package/dist/node.js +11 -2
  72. package/dist/remote-client.js +7 -2
  73. package/dist/server-visitor-stream.d.ts +13 -0
  74. package/dist/server-visitor-stream.js +124 -0
  75. package/dist/session-attention.d.ts +30 -0
  76. package/dist/session-attention.js +14 -0
  77. package/docs/api/agent-control.md +212 -15
  78. package/docs/api/client-entry.md +3 -0
  79. package/docs/api/client.md +209 -5
  80. package/docs/api/cloudflare.md +23 -1
  81. package/docs/api/delegation.md +284 -77
  82. package/docs/api/effect.md +100 -12
  83. package/docs/api/extensions.md +73 -12
  84. package/docs/api/fleet.md +138 -25
  85. package/docs/api/http.md +28 -0
  86. package/docs/api/index.md +43 -17
  87. package/docs/api/testing.md +9 -7
  88. package/package.json +10 -6
@@ -14,103 +14,100 @@ function settledContextTokens(store, sessionId) {
14
14
  return messages.length === 0 ? null : estimateContextTokens(messages).tokens;
15
15
  }
16
16
  /**
17
- * The Session's context use as compaction measures it, against the window of
18
- * the model a run would select now. Undefined while the Session has no
19
- * context or its context cannot be read; a window the host cannot describe is
20
- * null, never guessed.
17
+ * Read the stored figure synchronously with the Session baseline. Only model
18
+ * description remains in the returned Effect, outside the SQLite transaction
19
+ * and the request's ancestry scope. Undefined without readable context; a model
20
+ * window the host cannot describe is null, never guessed.
21
21
  */
22
- export function readSessionContext(options, sessionId, cursor, cache = true) {
23
- return Effect.gen(function* () {
24
- const savedPreference = yield* Effect.result(Effect.try({
25
- try: () => readSessionModelPreference(options.store, sessionId),
26
- catch: (error) => error,
27
- }));
28
- if (savedPreference._tag === "Failure") {
29
- reportCatchFailure("session-context.preference", savedPreference.failure);
30
- return undefined;
31
- }
32
- const preference = savedPreference.success;
33
- const preferenceKey = JSON.stringify(preference);
34
- const ownerCache = contextByOwner.get(options) ?? new Map();
35
- contextByOwner.set(options, ownerCache);
36
- const previous = cache ? ownerCache.get(sessionId) : undefined;
37
- const describe = options.model.describe;
38
- if (previous?.cursor === cursor && previous.preference === preferenceKey) {
39
- if (previous.value === undefined || describe === undefined)
40
- return previous.value;
41
- const previousValue = previous.value;
42
- // A null Session override follows a mutable Agent default. Check the
43
- // selected window without decoding history again; a changed window is
44
- // a distinct effective preference for this cursor.
45
- return yield* describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => {
46
- if (previousValue.window === contextWindow)
47
- return previousValue;
48
- const value = Object.freeze({
49
- tokens: previousValue.tokens,
50
- window: contextWindow,
51
- });
52
- if (cache && ownerCache.get(sessionId)?.cursor === cursor)
53
- ownerCache.set(sessionId, {
54
- cursor,
55
- preference: preferenceKey,
56
- value,
57
- });
58
- return value;
59
- }), Effect.catch((error) => Effect.sync(() => {
60
- reportCatchFailure("session-context.describe", error);
22
+ export function prepareSessionContext(options, sessionId, cursor, cache = true) {
23
+ let preference;
24
+ try {
25
+ preference = readSessionModelPreference(options.store, sessionId);
26
+ }
27
+ catch (error) {
28
+ reportCatchFailure("session-context.preference", error);
29
+ return Effect.succeed(undefined);
30
+ }
31
+ const preferenceKey = JSON.stringify(preference);
32
+ const ownerCache = contextByOwner.get(options) ?? new Map();
33
+ contextByOwner.set(options, ownerCache);
34
+ const previous = cache ? ownerCache.get(sessionId) : undefined;
35
+ const describe = options.model.describe;
36
+ if (previous?.cursor === cursor && previous.preference === preferenceKey) {
37
+ if (previous.value === undefined || describe === undefined)
38
+ return Effect.succeed(previous.value);
39
+ const previousValue = previous.value;
40
+ // A null Session override follows a mutable Agent default. Check the
41
+ // selected window without decoding history again; a changed window is
42
+ // a distinct effective preference for this cursor.
43
+ return describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => {
44
+ if (previousValue.window === contextWindow)
61
45
  return previousValue;
62
- })));
63
- }
64
- // At most one cached whole-window read per Session, cursor, and
65
- // preference: a late describe may publish only while this cursor still
66
- // owns the cache entry. Reads before the tip bypass the cache and each read
67
- // the whole window. A cold-cache connection also reads the whole kept
68
- // window.
69
- const contextRead = yield* Effect.result(Effect.try({
70
- try: () => settledContextTokens(options.store, sessionId),
71
- catch: (error) => error,
72
- }));
73
- if (contextRead._tag === "Failure") {
74
- reportCatchFailure("session-context.figure", contextRead.failure);
75
- return undefined;
76
- }
77
- const tokens = contextRead.success;
78
- if (tokens === null) {
79
- if (cache)
46
+ const value = Object.freeze({
47
+ tokens: previousValue.tokens,
48
+ window: contextWindow,
49
+ });
50
+ if (cache && ownerCache.get(sessionId)?.cursor === cursor)
80
51
  ownerCache.set(sessionId, {
81
52
  cursor,
82
53
  preference: preferenceKey,
83
- value: undefined,
54
+ value,
84
55
  });
85
- return undefined;
86
- }
87
- if (describe === undefined) {
88
- const value = Object.freeze({ tokens, window: null });
89
- if (cache)
90
- ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
91
56
  return value;
92
- }
93
- // Publish the decoded figure before yielding to the host's model
94
- // description. Concurrent watchers then reuse these tokens rather than
95
- // decoding the same Session window while `describe` is in flight.
57
+ }), Effect.catch((error) => Effect.sync(() => {
58
+ reportCatchFailure("session-context.describe", error);
59
+ return previousValue;
60
+ })));
61
+ }
62
+ // At most one cached whole-window read per Session, cursor, and
63
+ // preference: a late describe may publish only while this cursor still
64
+ // owns the cache entry. Reads before the tip bypass the cache and each read
65
+ // the whole window. A cold-cache connection also reads the whole kept
66
+ // window.
67
+ let tokens;
68
+ try {
69
+ tokens = settledContextTokens(options.store, sessionId);
70
+ }
71
+ catch (error) {
72
+ reportCatchFailure("session-context.figure", error);
73
+ return Effect.succeed(undefined);
74
+ }
75
+ if (tokens === null) {
96
76
  if (cache)
97
77
  ownerCache.set(sessionId, {
98
78
  cursor,
99
79
  preference: preferenceKey,
100
- value: Object.freeze({ tokens, window: null }),
80
+ value: undefined,
101
81
  });
102
- // A null Session override follows a mutable Agent default. Resolve through
103
- // the host's turn-model selector on every read, without acquiring a turn.
104
- const window = yield* describe({
105
- sessionId,
106
- preference,
107
- }).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
108
- reportCatchFailure("session-context.describe", error);
109
- return null;
110
- })));
82
+ return Effect.succeed(undefined);
83
+ }
84
+ if (describe === undefined) {
85
+ const value = Object.freeze({ tokens, window: null });
86
+ if (cache)
87
+ ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
88
+ return Effect.succeed(value);
89
+ }
90
+ // Publish the decoded figure before yielding to the host's model
91
+ // description. Concurrent watchers then reuse these tokens rather than
92
+ // decoding the same Session window while `describe` is in flight.
93
+ if (cache)
94
+ ownerCache.set(sessionId, {
95
+ cursor,
96
+ preference: preferenceKey,
97
+ value: Object.freeze({ tokens, window: null }),
98
+ });
99
+ // A null Session override follows a mutable Agent default. Resolve through
100
+ // the host's turn-model selector on every read, without acquiring a turn.
101
+ return describe({
102
+ sessionId,
103
+ preference,
104
+ }).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
105
+ reportCatchFailure("session-context.describe", error);
106
+ return null;
107
+ })), Effect.map((window) => {
111
108
  const value = Object.freeze({ tokens, window });
112
109
  if (cache && ownerCache.get(sessionId)?.cursor === cursor)
113
110
  ownerCache.set(sessionId, { cursor, preference: preferenceKey, value });
114
111
  return value;
115
- });
112
+ }));
116
113
  }
@@ -1,10 +1,10 @@
1
1
  import type { WatchdogQueueHead } from "@fungi.computer/watchdog/effect";
2
2
  import type { AgentRunningWork, SessionActivity, SessionProgress, SessionToolOutput } from "../client.js";
3
3
  import type { SqliteOwner } from "./contracts.js";
4
- /** Public activity facts plus the process-local progress read with them. */
4
+ /** Activity facts plus an explicit, on-demand bounded progress read. */
5
5
  export type ObservedActivity = Readonly<{
6
6
  facts: SessionActivity;
7
- progress: SessionProgress | null;
7
+ readProgress: () => SessionProgress | null;
8
8
  toolOutput: readonly SessionToolOutput[];
9
9
  }>;
10
10
  /** Join Watchdog authority with Woodstock facts and process-local progress. */
@@ -12,7 +12,7 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
12
12
  lastModelChoiceSequence,
13
13
  compaction: null,
14
14
  },
15
- progress: null,
15
+ readProgress: () => null,
16
16
  toolOutput: [],
17
17
  };
18
18
  if (head.state === "queued")
@@ -27,7 +27,7 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
27
27
  lastModelChoiceSequence,
28
28
  compaction: null,
29
29
  },
30
- progress: null,
30
+ readProgress: () => null,
31
31
  toolOutput: [],
32
32
  };
33
33
  const latestRetry = readLatestLiveRetryForWork(owner, sessionId, head.jobId);
@@ -49,7 +49,9 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
49
49
  lastModelChoiceSequence,
50
50
  compaction: readProcessCompaction(owner, sessionId),
51
51
  },
52
- progress: readProcessProgressForWork(owner, sessionId, head.jobId),
52
+ // Only baselines need a flattened bounded tail. Live subscribers read
53
+ // appends directly and must not materialize it on every token.
54
+ readProgress: () => readProcessProgressForWork(owner, sessionId, head.jobId),
53
55
  toolOutput: readProcessToolOutputForWork(owner, sessionId, head.jobId),
54
56
  };
55
57
  }
@@ -9,6 +9,11 @@ export declare const VerifiedAgentReplySchema: Schema.Union<readonly [Schema.Str
9
9
  readonly commandId: Schema.String;
10
10
  readonly outcome: Schema.Literals<readonly ["completed", "failed", "interrupted", "cancelled", "outcome_unknown"]>;
11
11
  readonly diagnostic: Schema.optionalKey<Schema.String>;
12
+ readonly waitingOnYou: Schema.optionalKey<Schema.$Array<Schema.Struct<{
13
+ readonly itemId: Schema.String;
14
+ readonly kind: Schema.String;
15
+ readonly summary: Schema.String;
16
+ }>>>;
12
17
  readonly agentId: Schema.String;
13
18
  readonly sessionId: Schema.String;
14
19
  }>, Schema.Struct<{
@@ -42,6 +42,9 @@ function opening(reply) {
42
42
  attributes.push(["outcome", reply.outcome]);
43
43
  if (reply.outcome !== "undeliverable" && reply.diagnostic !== undefined)
44
44
  attributes.push(["diagnostic", reply.diagnostic]);
45
+ if (reply.outcome !== "undeliverable" && reply.waitingOnYou !== undefined)
46
+ // A host fact, outside the child's text: the items the child left waiting on the recipient.
47
+ attributes.push(["waiting_on_you", JSON.stringify(reply.waitingOnYou)]);
45
48
  if (reply.outcome === "undeliverable")
46
49
  attributes.push(["reason", reply.reason]);
47
50
  const rendered = attributes
@@ -1,9 +1,11 @@
1
1
  import { messageAuthor } from "@fungi.computer/agar/agents";
2
2
  import { readSessionAttention } from "./session-attention.js";
3
+ import { readWaitingOnSender } from "./work-senders.js";
4
+ import { readSessionDelegator } from "./session-delegator.js";
3
5
  import { ShiitakeError } from "../../errors.js";
4
6
  import { decodeWoodstockCursor, encodeWoodstockCursor } from "./cursor.js";
5
7
  import { operatorClientMessage, promptCommand } from "./session-entry.js";
6
- import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, 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";
7
9
  const maximumPagePayloadBytes = 2_097_152;
8
10
  const replayPageLimit = 200;
9
11
  const maximumRowsPerPage = lineageRowLimit;
@@ -342,7 +344,7 @@ function resolveHistoryAnchor(owner, lineage, before) {
342
344
  * history, including what compaction summarized away.
343
345
  */
344
346
  export function readClientSnapshot(owner, sessionId, options, activity, projectExtensions = () => ({})) {
345
- return owner.transactionSync(() => {
347
+ return withSessionReadTransaction(owner, sessionId, () => {
346
348
  const pending = readPendingPrompts(owner, sessionId);
347
349
  const lineage = readSessionLineage(owner, { sessionId });
348
350
  if (lineage === undefined)
@@ -357,7 +359,7 @@ export function readClientSnapshot(owner, sessionId, options, activity, projectE
357
359
  compactionNotices: Object.freeze([]),
358
360
  pending,
359
361
  activity: activity.facts,
360
- progress: activity.progress,
362
+ progress: activity.readProgress(),
361
363
  toolOutput: activity.toolOutput,
362
364
  extensions: projectExtensions(),
363
365
  attention: readSessionAttention(owner, sessionId),
@@ -392,7 +394,7 @@ export function readClientSnapshot(owner, sessionId, options, activity, projectE
392
394
  compactionNotices: compactionNoticesIn(consumedRows),
393
395
  pending,
394
396
  activity: activity.facts,
395
- progress: options.before === undefined ? activity.progress : null,
397
+ progress: options.before === undefined ? activity.readProgress() : null,
396
398
  toolOutput: options.before === undefined ? activity.toolOutput : [],
397
399
  extensions: projectExtensions(),
398
400
  attention: readSessionAttention(owner, sessionId),
@@ -440,12 +442,6 @@ function replayFrames(owner, lineage, start, delivery, activity, extensions, att
440
442
  ? { ...frame, sandboxFailures: page.sandboxFailures }
441
443
  : frame),
442
444
  ];
443
- if (delivery === "live" && !page.hasMore && activity.progress !== null)
444
- frames.push(Object.freeze({
445
- kind: "progress",
446
- progress: activity.progress,
447
- activity: activity.facts,
448
- }));
449
445
  return Object.freeze({
450
446
  kind: "page",
451
447
  frames: Object.freeze(!page.hasMore
@@ -466,7 +462,7 @@ export function readClientReplayPage(owner, sessionId, after, delivery, activity
466
462
  const position = after === undefined ? undefined : decodeWoodstockCursor(after);
467
463
  if (after !== undefined && position === undefined)
468
464
  return resetReplayPage;
469
- return owner.transactionSync(() => {
465
+ return withSessionReadTransaction(owner, sessionId, () => {
470
466
  const pending = readPendingPrompts(owner, sessionId);
471
467
  const lineage = readSessionLineage(owner, { sessionId });
472
468
  if (lineage === undefined) {
@@ -503,6 +499,10 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
503
499
  const admission = readSessionCommandAdmission(owner, sessionId, commandId);
504
500
  if (admission === undefined)
505
501
  throw unavailable("Accepted Session command is unavailable");
502
+ // A live work has no settlement to project. The owner's terminal index
503
+ // answers this without decoding the work again on every polling tick.
504
+ if (!hasSessionWorkTerminal(owner, sessionId, admission.entry.workId))
505
+ return undefined;
506
506
  const rows = readSessionEntriesForWork(owner, sessionId, admission.entry.workId);
507
507
  const work = workFacts(owner, sessionId, rows);
508
508
  const terminal = rows
@@ -521,6 +521,7 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
521
521
  const sender = messageAuthor(admission.entry.meta);
522
522
  return {
523
523
  acceptedSender: sender?.kind === "agent" ? sender.id : null,
524
+ delegator: readSessionDelegator(owner, sessionId)?.replyTo[0]?.agentId ?? null,
524
525
  terminal: meta === undefined ? terminal : { ...terminal, meta },
525
526
  content: terminal.outcome === "cancelled"
526
527
  ? ""
@@ -534,5 +535,6 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
534
535
  usage,
535
536
  })),
536
537
  toolCalls: new Set(rows.flatMap(({ entry }) => entry.kind === "tool_intent" ? [entry.call.id] : [])).size,
538
+ waitingOnYou: readWaitingOnSender(owner, sessionId, admission.entry.workId),
537
539
  };
538
540
  }
@@ -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);
@@ -15,6 +15,8 @@ export * from "./extension-store.js";
15
15
  export { readLatestPendingPrompt } from "./client-projection.js";
16
16
  export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
17
17
  export * from "./session-attention.js";
18
+ export * from "./work-senders.js";
19
+ export * from "./session-delegator.js";
18
20
  export { deliverSessionGrants, prepareSessionGrants, readSessionGrants, type GrantsDelivery, type GrantsDeliveryResult, } from "./session-grants.js";
19
21
  export * from "./progress.js";
20
22
  export * from "./compaction-activity.js";
@@ -117,7 +119,7 @@ export declare function readSettlementDiagnostic(owner: SqliteOwner, input: Read
117
119
  commandFactId: string;
118
120
  }>): Effect.Effect<string | undefined, ShiitakeError>;
119
121
  /** Read one bounded public Session snapshot. */
120
- 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>;
121
123
  /** Read one bounded folder-resume summary through Woodstock's Effect boundary. */
122
124
  export declare function readSessionSummary(owner: SqliteOwner, sessionId: string): Effect.Effect<import("./session-store.js").StoredSessionSummary | undefined, ShiitakeError>;
123
125
  /**
@@ -138,8 +140,10 @@ export declare function readSessionFactPage(owner: SqliteOwner, after?: string):
138
140
  export declare function readColdSessionPage(owner: SqliteOwner, request: ColdSessionPageRequest): Effect.Effect<ColdSessionPage, ShiitakeError>;
139
141
  /** Read one bounded inbox page in tier, then newest-activity order. */
140
142
  export declare function readSessionInbox(owner: SqliteOwner, after: string | undefined): Effect.Effect<import("../../client.js").SessionSummaryPage, ShiitakeError>;
141
- /** Read one strict-after page from Woodstock's selected lineage. */
142
- export declare function readSessionReplayPage(owner: SqliteOwner, sessionId: string, after: string | undefined, delivery: "replay" | "live", activity: ObservedActivity, projectExtensions?: () => SessionExtensionSlots, includeSandboxFailures?: boolean): Effect.Effect<ClientReplayPage, ShiitakeError>;
143
+ /** Read a Session's open attention items from the store alone. */
144
+ export declare function readStoredSessionAttention(owner: SqliteOwner, sessionId: string): Effect.Effect<readonly import("../../client.js").SessionAttention[], 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>;
143
147
  /** Persist an operator-only sandbox diagnostic beside its accepted Session work. */
144
148
  export declare function recordSandboxFailure(owner: SqliteOwner, accepted: Pick<AcceptedCommand, "sessionId" | "commandId">, event: SandboxFailureEvent): Effect.Effect<void, ShiitakeError>;
145
149
  /** Extension that admitted one accepted command; null for other producers. */
@@ -5,14 +5,18 @@ 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";
13
+ import { readSessionAttention } from "./session-attention.js";
12
14
  export * from "./extension-store.js";
13
15
  export { readLatestPendingPrompt } from "./client-projection.js";
14
16
  export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
15
17
  export * from "./session-attention.js";
18
+ export * from "./work-senders.js";
19
+ export * from "./session-delegator.js";
16
20
  export { deliverSessionGrants, prepareSessionGrants, readSessionGrants, } from "./session-grants.js";
17
21
  export * from "./progress.js";
18
22
  export * from "./compaction-activity.js";
@@ -188,8 +192,14 @@ export function readSettlementDiagnostic(owner, input) {
188
192
  });
189
193
  }
190
194
  /** Read one bounded public Session snapshot. */
191
- export function readSession(owner, sessionId, activity, input, projectExtensions = () => ({})) {
192
- 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
+ });
193
203
  }
194
204
  /** Read one bounded folder-resume summary through Woodstock's Effect boundary. */
195
205
  export function readSessionSummary(owner, sessionId) {
@@ -228,9 +238,16 @@ export function readColdSessionPage(owner, request) {
228
238
  export function readSessionInbox(owner, after) {
229
239
  return storeEffect(() => readSessionInboxPage(owner, after));
230
240
  }
231
- /** Read one strict-after page from Woodstock's selected lineage. */
232
- export function readSessionReplayPage(owner, sessionId, after, delivery, activity, projectExtensions = () => ({}), includeSandboxFailures = false) {
233
- return storeEffect(() => readClientReplayPage(owner, decodeClientSessionId(sessionId), after, delivery, activity, projectExtensions, includeSandboxFailures));
241
+ /** Read a Session's open attention items from the store alone. */
242
+ export function readStoredSessionAttention(owner, sessionId) {
243
+ return storeEffect(() => readSessionAttention(owner, sessionId));
244
+ }
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
+ });
234
251
  }
235
252
  function sameEntry(left, right) {
236
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<{