@fungi.computer/shiitake 0.2.0 → 0.3.1

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 (114) hide show
  1. package/README.md +155 -34
  2. package/dist/agent-control.d.ts +77 -5
  3. package/dist/agent-control.js +271 -65
  4. package/dist/client-entry.d.ts +15 -8
  5. package/dist/client-entry.js +7 -5
  6. package/dist/client-wire.d.ts +24 -8
  7. package/dist/client-wire.js +53 -11
  8. package/dist/client.d.ts +56 -11
  9. package/dist/client.js +1 -1
  10. package/dist/cloudflare.d.ts +42 -23
  11. package/dist/cloudflare.js +76 -54
  12. package/dist/cold-sessions.d.ts +28 -0
  13. package/dist/cold-sessions.js +1 -0
  14. package/dist/delegation.d.ts +214 -0
  15. package/dist/delegation.js +319 -0
  16. package/dist/effect.d.ts +1 -1
  17. package/dist/errors.d.ts +2 -2
  18. package/dist/errors.js +2 -2
  19. package/dist/extension-contract.d.ts +10 -2
  20. package/dist/extensions.d.ts +6 -2
  21. package/dist/fleet.d.ts +43 -9
  22. package/dist/fleet.js +88 -25
  23. package/dist/hosted.js +1 -0
  24. package/dist/http-routes.d.ts +113 -29
  25. package/dist/http-routes.js +18 -1
  26. package/dist/index.d.ts +3 -2
  27. package/dist/internal/accepted-run-executor.d.ts +5 -0
  28. package/dist/internal/accepted-run-executor.js +10 -4
  29. package/dist/internal/admission-door.d.ts +3 -0
  30. package/dist/internal/admission-door.js +12 -2
  31. package/dist/internal/client-boundary.js +7 -1
  32. package/dist/internal/compaction/compaction-slot.d.ts +36 -0
  33. package/dist/internal/compaction/compaction-slot.js +41 -0
  34. package/dist/internal/compaction/compaction.d.ts +11 -8
  35. package/dist/internal/compaction/compaction.js +62 -34
  36. package/dist/internal/compaction/session-compaction.d.ts +8 -16
  37. package/dist/internal/compaction/session-compaction.js +358 -119
  38. package/dist/internal/compaction/settings.d.ts +1 -1
  39. package/dist/internal/compaction/settings.js +5 -0
  40. package/dist/internal/compaction/utils.d.ts +19 -16
  41. package/dist/internal/compaction/utils.js +30 -84
  42. package/dist/internal/contracts.d.ts +40 -13
  43. package/dist/internal/coordinator.js +28 -10
  44. package/dist/internal/effect-client.d.ts +4 -2
  45. package/dist/internal/effect-client.js +29 -0
  46. package/dist/internal/effect-contracts.d.ts +17 -4
  47. package/dist/internal/extension-runtime.d.ts +5 -0
  48. package/dist/internal/extension-runtime.js +184 -89
  49. package/dist/internal/host-meta.d.ts +7 -0
  50. package/dist/internal/model-acquisition.js +7 -1
  51. package/dist/internal/model-preference.d.ts +4 -2
  52. package/dist/internal/model-preference.js +2 -1
  53. package/dist/internal/promise-facade.js +4 -16
  54. package/dist/internal/run-failure-reason.d.ts +10 -0
  55. package/dist/internal/run-failure-reason.js +19 -0
  56. package/dist/internal/run-scope.d.ts +4 -1
  57. package/dist/internal/run-scope.js +38 -5
  58. package/dist/internal/runtime-coordinator.d.ts +2 -1
  59. package/dist/internal/runtime-coordinator.js +65 -4
  60. package/dist/internal/runtime-core.js +1 -0
  61. package/dist/internal/runtime-reconciliation.js +42 -6
  62. package/dist/internal/session-context.d.ts +1 -2
  63. package/dist/internal/session-context.js +8 -30
  64. package/dist/internal/session-status.d.ts +2 -1
  65. package/dist/internal/session-status.js +4 -1
  66. package/dist/internal/woodstock/client-projection.d.ts +6 -5
  67. package/dist/internal/woodstock/client-projection.js +79 -53
  68. package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
  69. package/dist/internal/woodstock/cold-sessions.js +90 -0
  70. package/dist/internal/woodstock/extension-store.d.ts +2 -0
  71. package/dist/internal/woodstock/extension-store.js +4 -0
  72. package/dist/internal/woodstock/fleet-store.d.ts +5 -1
  73. package/dist/internal/woodstock/fleet-store.js +17 -5
  74. package/dist/internal/woodstock/index.d.ts +11 -3
  75. package/dist/internal/woodstock/index.js +33 -14
  76. package/dist/internal/woodstock/progress.d.ts +14 -1
  77. package/dist/internal/woodstock/progress.js +97 -19
  78. package/dist/internal/woodstock/run-recovery.js +6 -4
  79. package/dist/internal/woodstock/session-attention.d.ts +1 -1
  80. package/dist/internal/woodstock/session-attention.js +2 -2
  81. package/dist/internal/woodstock/session-entry.d.ts +110 -43
  82. package/dist/internal/woodstock/session-entry.js +63 -20
  83. package/dist/internal/woodstock/session-inbox.js +2 -0
  84. package/dist/internal/woodstock/session-store.d.ts +104 -24
  85. package/dist/internal/woodstock/session-store.js +937 -246
  86. package/dist/internal/woodstock/storage-admission.d.ts +8 -8
  87. package/dist/internal/woodstock/storage-admission.js +8 -34
  88. package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
  89. package/dist/internal/woodstock/storage-conformance.js +489 -0
  90. package/dist/model-preference.d.ts +2 -2
  91. package/dist/model-preference.js +2 -2
  92. package/dist/node.js +1 -1
  93. package/dist/remote-client.js +8 -1
  94. package/dist/session-attention.d.ts +8 -1
  95. package/dist/session-attention.js +6 -1
  96. package/dist/testing.d.ts +1 -1
  97. package/dist/testing.js +1 -0
  98. package/docs/api/README.md +18 -16
  99. package/docs/api/agent-control.md +171 -1
  100. package/docs/api/client-entry.md +26 -10
  101. package/docs/api/client.md +153 -37
  102. package/docs/api/cloudflare.md +76 -23
  103. package/docs/api/delegation.md +485 -0
  104. package/docs/api/effect.md +20 -2
  105. package/docs/api/extensions.md +21 -9
  106. package/docs/api/fleet.md +159 -104
  107. package/docs/api/http.md +4 -1
  108. package/docs/api/index.md +116 -20
  109. package/docs/api/internal/woodstock/storage-conformance.md +81 -0
  110. package/docs/api/model-preference.md +27 -10
  111. package/docs/api/testing.md +16 -4
  112. package/package.json +26 -14
  113. package/dist/internal/browser-sessions.d.ts +0 -31
  114. package/dist/internal/browser-sessions.js +0 -173
@@ -8,6 +8,7 @@ import { ShiitakeError } from "../errors.js";
8
8
  import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.js";
9
9
  import { openAdmissionDoor } from "./admission-door.js";
10
10
  import { openCompactionSettings } from "./compaction/settings.js";
11
+ import { admitExtensionRosterV0, } from "./extension-roster.js";
11
12
  import { coreConsumer, openExtensionRuntime, } from "./extension-runtime.js";
12
13
  import { reconcileRuntime } from "./runtime-reconciliation.js";
13
14
  import { advanceCursor, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
@@ -36,6 +37,9 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
36
37
  }).pipe(Effect.uninterruptible);
37
38
  const boundRuntime = {};
38
39
  const extensionsRef = { current: undefined };
40
+ let turnOptions = options;
41
+ let pendingConfiguration;
42
+ let preparing = false;
39
43
  const requestBoundWake = Effect.suspend(() => boundRuntime.requestWake ??
40
44
  Effect.fail(runtimeFailure("Runtime host is unavailable")));
41
45
  const hostExecutor = { current: undefined };
@@ -64,7 +68,17 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
64
68
  }), () => job.lane === options.hostJobs?.lane
65
69
  ? (hostExecutor.current?.execute(job) ??
66
70
  Effect.fail(new WatchdogError("execution_failed")))
67
- : executeAcceptedRun(options, coreDigest, job, extensionsRef.current), () => Effect.sync(() => {
71
+ : Effect.gen(function* () {
72
+ const pending = pendingConfiguration;
73
+ if (pending !== undefined) {
74
+ yield* pending.apply;
75
+ const { tools: oldTools, ...base } = options;
76
+ void oldTools;
77
+ turnOptions = { ...base, ...pending.configuration };
78
+ pendingConfiguration = undefined;
79
+ }
80
+ return yield* executeAcceptedRun(turnOptions, coreDigest, job, extensionsRef.current);
81
+ }).pipe(Effect.tapError((error) => Effect.logError("configuration_apply_failed", error)), Effect.mapError(() => new WatchdogError("execution_failed"))), () => Effect.sync(() => {
68
82
  if (job.lane !== options.hostJobs?.lane)
69
83
  activeSessions.delete(job.queue);
70
84
  runningSlots.delete(job.lane === options.hostJobs?.lane ? "host" : "serial");
@@ -100,7 +114,7 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
100
114
  : runtimeFailure("Compaction settings are unavailable"),
101
115
  });
102
116
  const storage = yield* Effect.try({
103
- try: () => openStorageAdmission(options.storageBudget),
117
+ try: () => openStorageAdmission(options.storagePressure),
104
118
  catch: (error) => error instanceof ShiitakeError
105
119
  ? error
106
120
  : runtimeFailure("Storage budget is unavailable"),
@@ -160,9 +174,24 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
160
174
  try: () => extensions.timerWake(now),
161
175
  catch: () => runtimeFailure("Extension timers are unavailable"),
162
176
  });
177
+ const mailWake = mailRoom === undefined
178
+ ? undefined
179
+ : yield* mailRoom
180
+ .nextWakeAt()
181
+ .pipe(Effect.mapError(() => runtimeFailure("Reply retry wake is unavailable")));
182
+ // Carrier owns reply retry state. Keep it in the same durable answer
183
+ // as runtime work, so a host cannot erase its wake with an idle result.
184
+ const nextWakeAt = timers.nextWakeAt === null
185
+ ? (mailWake ?? null)
186
+ : mailWake === undefined
187
+ ? timers.nextWakeAt
188
+ : Math.min(timers.nextWakeAt, mailWake);
163
189
  return {
164
- pending: watchdogPending || settledLag || timers.due,
165
- nextWakeAt: timers.nextWakeAt,
190
+ pending: watchdogPending ||
191
+ settledLag ||
192
+ timers.due ||
193
+ (mailWake !== undefined && mailWake <= now),
194
+ nextWakeAt,
166
195
  };
167
196
  });
168
197
  const bound = yield* options.runtime
@@ -179,6 +208,38 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
179
208
  // Every host must get one post-bind tick to recover durable work after a cold reopen.
180
209
  yield* bound.requestWake.pipe(Effect.onError(() => Effect.all([bound.close, extensions.close]).pipe(Effect.orDie)));
181
210
  return Object.freeze({
211
+ prepareConfiguration: (configuration) => Effect.gen(function* () {
212
+ if (lifecycleState !== "open" ||
213
+ preparing ||
214
+ pendingConfiguration !== undefined)
215
+ return yield* Effect.fail(runtimeFailure("A configuration change is already pending, or the Agent is closing"));
216
+ preparing = true;
217
+ const staged = yield* admitExtensionRosterV0(configuration.extensions).pipe(Effect.flatMap((next) => extensions.stage(next)), Effect.onError(() => Effect.sync(() => {
218
+ preparing = false;
219
+ })));
220
+ let settled = false;
221
+ return {
222
+ commit: Effect.try({
223
+ try: () => {
224
+ if (settled || lifecycleState !== "open")
225
+ throw runtimeFailure("Configuration preparation is no longer available");
226
+ pendingConfiguration = { configuration, apply: staged.apply };
227
+ settled = true;
228
+ preparing = false;
229
+ },
230
+ catch: (cause) => cause instanceof ShiitakeError
231
+ ? cause
232
+ : runtimeFailure("Configuration commit failed"),
233
+ }),
234
+ discard: Effect.suspend(() => {
235
+ if (settled)
236
+ return Effect.void;
237
+ settled = true;
238
+ preparing = false;
239
+ return staged.discard;
240
+ }),
241
+ };
242
+ }),
182
243
  activeSessions: Effect.sync(() => [...activeSessions]),
183
244
  runningWork: (jobId) => watchdog.read(jobId).pipe(Effect.map((result) => result.status === "found"
184
245
  ? (runningSlots.get(result.job.lane === options.hostJobs?.lane
@@ -23,6 +23,7 @@ export function openRuntimeCore(options, mailRoom) {
23
23
  });
24
24
  const owner = Object.freeze({
25
25
  ...agent,
26
+ prepareConfiguration: runtime.prepareConfiguration,
26
27
  grants: coordinator.grants,
27
28
  withMeta: (meta) => Object.freeze({
28
29
  ...agent,
@@ -7,7 +7,7 @@ import { ShiitakeError } from "../errors.js";
7
7
  import { ExtensionId } from "../extension-contract.js";
8
8
  import { coreConsumer } from "./extension-runtime.js";
9
9
  import { publishOwnerObservation } from "./observation-kernel.js";
10
- import { advanceCursor, readCursor, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSettlementMeta, recordWorkSettlement, settleControls, } from "./woodstock/index.js";
10
+ import { advanceCursor, readCursor, readExtensionAdmissionOrigin, readSettledRunFacts, readSettlementContent, readSettlementDiagnostic, readSettlementMeta, recordWorkSettlement, sessionLineageRefused, settleControls, } from "./woodstock/index.js";
11
11
  const pageSize = 64;
12
12
  function runtimeFailure(message) {
13
13
  return new ShiitakeError("runtime", "rejected", message);
@@ -66,6 +66,7 @@ function settleTargetedControls(store, watchdog, job) {
66
66
  sessionId: payload.work.sessionId,
67
67
  parentWorkId: job.jobId,
68
68
  outcome,
69
+ diagnostic: job.failureReason,
69
70
  enqueue: (work) => {
70
71
  watchdog.transactional.enqueueAcceptedJob(agentJobFromInput({ work: { ...work, recovery: agentTurnRecovery } }));
71
72
  },
@@ -79,10 +80,41 @@ function drainMail(mailRoom) {
79
80
  .drainOutbox()
80
81
  .pipe(Effect.mapError(() => runtimeFailure("Agent reply delivery is unavailable")));
81
82
  }
82
- /** Join one core-processed settled record with its exact Woodstock facts. */
83
+ /**
84
+ * Whether the settled work belongs to a Session the lineage audit refused.
85
+ * Nothing more can be recorded in that Session: its Watchdog settlement, with
86
+ * the refusal as its failure reason, is the work's whole terminal.
87
+ */
88
+ function settledInRefusedSession(store, job) {
89
+ return Effect.gen(function* () {
90
+ const payload = yield* Effect.try({
91
+ try: () => decodeAgentJob(job),
92
+ catch: () => runtimeFailure("Agent work payload is invalid"),
93
+ });
94
+ const refused = yield* Effect.try({
95
+ try: () => sessionLineageRefused(store, payload.work.sessionId),
96
+ catch: () => runtimeFailure("Session lineage state is unavailable"),
97
+ });
98
+ if (refused)
99
+ yield* Effect.logWarning("shiitake_settlement_session_refused").pipe(Effect.annotateLogs({ workId: job.jobId }));
100
+ return refused;
101
+ });
102
+ }
103
+ /**
104
+ * Join one core-processed settled record with its exact Woodstock facts. A
105
+ * run of a Session the lineage audit refused reaches extensions with only
106
+ * Watchdog's outcome and its admission origin: no terminal and no tool
107
+ * calls can be read from that history.
108
+ */
83
109
  function settledRun(store, record) {
84
110
  const payload = decodeAgentJob(record.job);
85
- const facts = readSettledRunFacts(store, payload.work.sessionId, payload.work.commandFactId, record.job.jobId);
111
+ const facts = sessionLineageRefused(store, payload.work.sessionId)
112
+ ? Object.freeze({
113
+ terminal: "missing",
114
+ toolCalls: 0,
115
+ origin: readExtensionAdmissionOrigin(store, payload.work.sessionId, payload.work.commandFactId, record.job.jobId),
116
+ })
117
+ : readSettledRunFacts(store, payload.work.sessionId, payload.work.commandFactId, record.job.jobId);
86
118
  return Object.freeze({
87
119
  sequence: record.sequence,
88
120
  sessionId: payload.work.sessionId,
@@ -111,9 +143,13 @@ function settleCore(store, watchdog, mailRoom, records) {
111
143
  // leave live watchers reporting the finished run as active. A replay
112
144
  // after a crash repeats the same announcement.
113
145
  publishOwnerObservation(store, record.job.queue);
114
- yield* repairWorkSettlement(store, record.job);
115
- yield* settleTargetedControls(store, watchdog, record.job);
116
- yield* fanOutSettlement(store, mailRoom, record.job);
146
+ // A refused Session can neither record a repair nor give a reply
147
+ // its content: the failed Watchdog settlement is all there is.
148
+ if (!(yield* settledInRefusedSession(store, record.job))) {
149
+ yield* repairWorkSettlement(store, record.job);
150
+ yield* settleTargetedControls(store, watchdog, record.job);
151
+ yield* fanOutSettlement(store, mailRoom, record.job);
152
+ }
117
153
  yield* Effect.try({
118
154
  try: () => {
119
155
  advanceCursor(store, coreConsumer, record.sequence);
@@ -1,11 +1,10 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import type { SessionContext } from "../client.js";
3
3
  import type { ShiitakeEffectOptions } from "./effect-contracts.js";
4
- import type { RuntimeCoordinator } from "./runtime-coordinator.js";
5
4
  /**
6
5
  * The Session's context use as compaction measures it, against the window of
7
6
  * the model a run would select now. Undefined while the Session has no
8
7
  * context or its context cannot be read; a window the host cannot describe is
9
8
  * null, never guessed.
10
9
  */
11
- export declare function readSessionContext(options: ShiitakeEffectOptions, runtime: RuntimeCoordinator, sessionId: string): Effect.Effect<SessionContext | undefined>;
10
+ export declare function readSessionContext(options: ShiitakeEffectOptions, sessionId: string): Effect.Effect<SessionContext | undefined>;
@@ -1,5 +1,5 @@
1
1
  import * as Effect from "effect/Effect";
2
- import { estimateThresholdContextTokens } from "./compaction/session-compaction.js";
2
+ import { estimateContextTokens } from "@earendil-works/pi-ai/utils/estimate";
3
3
  import { reportCatchFailure } from "./failure.js";
4
4
  import { buildSessionContext, readSessionContextPath, readSessionModelPreference, } from "./woodstock/index.js";
5
5
  /**
@@ -10,22 +10,7 @@ import { buildSessionContext, readSessionContextPath, readSessionModelPreference
10
10
  function settledContextTokens(store, sessionId) {
11
11
  const path = readSessionContextPath(store, { sessionId });
12
12
  const messages = buildSessionContext(path);
13
- return messages.length === 0
14
- ? null
15
- : estimateThresholdContextTokens(path, messages);
16
- }
17
- /**
18
- * Each runtime keeps the last described window per Session. A saved model
19
- * preference changes the key, so `/model` is described again at once.
20
- */
21
- const describedWindows = new WeakMap();
22
- function windowsFor(runtime) {
23
- const existing = describedWindows.get(runtime);
24
- if (existing !== undefined)
25
- return existing;
26
- const created = new Map();
27
- describedWindows.set(runtime, created);
28
- return created;
13
+ return messages.length === 0 ? null : estimateContextTokens(messages).tokens;
29
14
  }
30
15
  /**
31
16
  * The Session's context use as compaction measures it, against the window of
@@ -33,7 +18,7 @@ function windowsFor(runtime) {
33
18
  * context or its context cannot be read; a window the host cannot describe is
34
19
  * null, never guessed.
35
20
  */
36
- export function readSessionContext(options, runtime, sessionId) {
21
+ export function readSessionContext(options, sessionId) {
37
22
  return Effect.gen(function* () {
38
23
  // A context too large to read safely has no footer figure.
39
24
  const read = yield* Effect.result(Effect.try({
@@ -46,22 +31,15 @@ export function readSessionContext(options, runtime, sessionId) {
46
31
  if (read._tag === "Failure" || read.success.tokens === null)
47
32
  return undefined;
48
33
  const { tokens, preference } = read.success;
49
- const windows = windowsFor(runtime);
50
- const key = JSON.stringify(preference);
51
- const cached = windows.get(sessionId);
52
- if (cached?.preference === key)
53
- return Object.freeze({ tokens, window: cached.window });
54
34
  const describe = options.model.describe;
55
35
  if (describe === undefined)
56
36
  return Object.freeze({ tokens, window: null });
57
- const described = yield* describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
37
+ // A null Session override follows a mutable Agent default. Resolve through
38
+ // the host's turn-model selector on every read, without acquiring a turn.
39
+ const window = yield* describe({ sessionId, preference }).pipe(Effect.map(({ contextWindow }) => contextWindow), Effect.catch((error) => Effect.sync(() => {
58
40
  reportCatchFailure("session-context.describe", error);
59
- return undefined;
41
+ return null;
60
42
  })));
61
- // A failed description is retried on the next read, never cached.
62
- if (described === undefined)
63
- return Object.freeze({ tokens, window: null });
64
- windows.set(sessionId, { preference: key, window: described });
65
- return Object.freeze({ tokens, window: described });
43
+ return Object.freeze({ tokens, window });
66
44
  });
67
45
  }
@@ -1,10 +1,11 @@
1
1
  import type { WatchdogQueueHead } from "@fungi.computer/watchdog/effect";
2
- import type { AgentRunningWork, SessionActivity, SessionProgress } from "../client.js";
2
+ import type { AgentRunningWork, SessionActivity, SessionProgress, SessionToolOutput } from "../client.js";
3
3
  import type { SqliteOwner } from "./contracts.js";
4
4
  /** Public activity facts plus the process-local progress read with them. */
5
5
  export type ObservedActivity = Readonly<{
6
6
  facts: SessionActivity;
7
7
  progress: SessionProgress | null;
8
+ toolOutput: readonly SessionToolOutput[];
8
9
  }>;
9
10
  /** Join Watchdog authority with Woodstock facts and process-local progress. */
10
11
  export declare function readSessionActivity(owner: SqliteOwner, sessionId: string, head: WatchdogQueueHead, runningWork: AgentRunningWork | null): ObservedActivity;
@@ -1,4 +1,4 @@
1
- import { readProcessProgressForWork, readProcessCompaction, readLatestLiveRetryForWork, readActiveToolForWork, readLastModelChoiceSequence, } from "./woodstock/index.js";
1
+ import { readProcessProgressForWork, readProcessToolOutputForWork, readProcessCompaction, readLatestLiveRetryForWork, readActiveToolForWork, readLastModelChoiceSequence, } from "./woodstock/index.js";
2
2
  /** Join Watchdog authority with Woodstock facts and process-local progress. */
3
3
  export function readSessionActivity(owner, sessionId, head, runningWork) {
4
4
  const lastModelChoiceSequence = readLastModelChoiceSequence(owner, sessionId);
@@ -13,6 +13,7 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
13
13
  compaction: null,
14
14
  },
15
15
  progress: null,
16
+ toolOutput: [],
16
17
  };
17
18
  if (head.state === "queued")
18
19
  // The queue job id is the admitted command id: the one identity a person
@@ -27,6 +28,7 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
27
28
  compaction: null,
28
29
  },
29
30
  progress: null,
31
+ toolOutput: [],
30
32
  };
31
33
  const latestRetry = readLatestLiveRetryForWork(owner, sessionId, head.jobId);
32
34
  const retry = latestRetry?.event.type === "auto_retry_start"
@@ -48,5 +50,6 @@ export function readSessionActivity(owner, sessionId, head, runningWork) {
48
50
  compaction: readProcessCompaction(owner, sessionId),
49
51
  },
50
52
  progress: readProcessProgressForWork(owner, sessionId, head.jobId),
53
+ toolOutput: readProcessToolOutputForWork(owner, sessionId, head.jobId),
51
54
  };
52
55
  }
@@ -1,14 +1,15 @@
1
- import type { SessionFrame, SessionExtensionSlots, SessionSnapshot } from "../../client.js";
1
+ import type { SessionFrame, SessionExtensionSlots, SessionPendingPrompts, SessionSnapshot } from "../../client.js";
2
2
  import type { ClientReadOptions } from "../client-boundary.js";
3
3
  import type { CommandSettlement, SqliteOwner } from "../contracts.js";
4
4
  import type { ObservedActivity } from "../session-status.js";
5
- import { type StoredSessionRow } from "./session-store.js";
5
+ import { type AdmissionIdentity, type StoredSessionRow } from "./session-store.js";
6
6
  /**
7
- * Newest failure message each work recorded: a work-terminal diagnostic, an
8
- * assistant error or a final retry error. Raw text; clients and the reply
9
- * fan-out decide how to present and bound it.
7
+ * Newest owner-produced terminal diagnostic for each work. Pi assistant errors
8
+ * and retry text can contain private provider bodies, not safe diagnostics.
10
9
  */
11
10
  export declare function diagnosticsByWorkId(path: readonly StoredSessionRow[]): ReadonlyMap<string, string>;
11
+ /** Same payload projection as the FIFO page, selected independently of that page's bounds. */
12
+ export declare function readLatestPendingPrompt(owner: SqliteOwner, sessionId: string, identity?: AdmissionIdentity): SessionPendingPrompts["prompts"][number] | null;
12
13
  /**
13
14
  * Read one bounded public baseline from the selected Session lineage. Only
14
15
  * the page's rows are decoded; `before` pages back through the whole saved
@@ -1,8 +1,9 @@
1
+ import { messageAuthor } from "@fungi.computer/agar/agents";
1
2
  import { readSessionAttention } from "./session-attention.js";
2
3
  import { ShiitakeError } from "../../errors.js";
3
4
  import { decodeWoodstockCursor, encodeWoodstockCursor } from "./cursor.js";
4
5
  import { operatorClientMessage, promptCommand } from "./session-entry.js";
5
- import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, } from "./session-store.js";
6
+ import { lineageEntrySequence, lineageHasSequence, lineageRowLimit, readLineageWindow, readLineageWorkFacts, readSessionEntriesForWork, readSessionLineage, readPendingSessionCommands, readSessionCommandAdmission, readNewestPendingSessionCommand, } from "./session-store.js";
6
7
  const maximumPagePayloadBytes = 2_097_152;
7
8
  const replayPageLimit = 200;
8
9
  const maximumRowsPerPage = lineageRowLimit;
@@ -80,6 +81,7 @@ function projectMessage(row, work) {
80
81
  id: row.id,
81
82
  sequence: row.sequence,
82
83
  commandId: prompt.workId,
84
+ requestId: prompt.requestId,
83
85
  value: prompt.message,
84
86
  };
85
87
  let projected = message;
@@ -94,6 +96,7 @@ function projectMessage(row, work) {
94
96
  id: row.id,
95
97
  sequence: row.sequence,
96
98
  commandId: null,
99
+ requestId: row.entry.requestId,
97
100
  value: operatorClientMessage(row.entry),
98
101
  };
99
102
  let projected = message;
@@ -101,34 +104,30 @@ function projectMessage(row, work) {
101
104
  projected = { ...projected, meta: row.entry.meta };
102
105
  return Object.freeze(projected);
103
106
  }
104
- if (row.entry.kind === "message")
105
- return Object.freeze({
107
+ if (row.entry.kind === "message") {
108
+ const message = {
106
109
  id: row.entry.message.value,
107
110
  sequence: row.sequence,
108
111
  commandId: work.commandIds.get(row.entry.workId) ?? null,
109
112
  value: row.entry.value,
110
- });
113
+ };
114
+ return Object.freeze(row.entry.data === undefined
115
+ ? message
116
+ : { ...message, data: row.entry.data });
117
+ }
111
118
  return undefined;
112
119
  }
113
120
  /**
114
- * Newest failure message each work recorded: a work-terminal diagnostic, an
115
- * assistant error or a final retry error. Raw text; clients and the reply
116
- * fan-out decide how to present and bound it.
121
+ * Newest owner-produced terminal diagnostic for each work. Pi assistant errors
122
+ * and retry text can contain private provider bodies, not safe diagnostics.
117
123
  */
118
124
  export function diagnosticsByWorkId(path) {
119
125
  const diagnostics = new Map();
120
126
  for (const { entry } of path) {
121
- if (entry.kind === "work_terminal" && entry.diagnostic !== undefined) {
127
+ if ((entry.kind === "work_terminal" || entry.kind === "run_terminal") &&
128
+ entry.diagnostic !== undefined) {
122
129
  diagnostics.set(entry.workId, entry.diagnostic);
123
130
  }
124
- else if (entry.kind === "message" && entry.value.role === "assistant") {
125
- if (entry.value.errorMessage !== undefined)
126
- diagnostics.set(entry.workId, entry.value.errorMessage);
127
- }
128
- else if (entry.kind === "live_retry" &&
129
- entry.event.type === "auto_retry_end" &&
130
- entry.event.finalError !== undefined)
131
- diagnostics.set(entry.workId, entry.event.finalError);
132
131
  }
133
132
  return diagnostics;
134
133
  }
@@ -156,6 +155,8 @@ function projectTerminal(row, { commandIds, diagnostics, cancellations }) {
156
155
  * compaction kept (the same boundary `buildSessionContext` resumes from) so
157
156
  * the client can render "the agent's memory starts below" accurately even
158
157
  * when the kept entries are not the compaction row's immediate predecessors.
158
+ * A kept entry that is itself one of the validated page rows needs no lineage
159
+ * lookup; only one outside the page is checked against the lineage.
159
160
  */
160
161
  function compactionMarkersIn(owner, lineage, rows) {
161
162
  const markers = [];
@@ -163,16 +164,17 @@ function compactionMarkersIn(owner, lineage, rows) {
163
164
  const entry = row.entry;
164
165
  if (entry.kind !== "compaction")
165
166
  continue;
166
- const kept = lineageEntrySequence(owner, lineage, entry.firstKeptEntryId);
167
+ const kept = rows.find((candidate) => candidate.id === entry.head)?.sequence ??
168
+ lineageEntrySequence(owner, lineage, entry.head);
167
169
  if (kept === undefined)
168
170
  throw unavailable("Session compaction horizon is unavailable");
169
171
  markers.push(Object.freeze({
170
172
  id: row.id,
171
173
  sequence: kept,
172
174
  at: row.timestamp,
173
- tokensBefore: entry.tokensBefore,
174
- tokensAfter: entry.tokensAfter ?? null,
175
- trigger: entry.trigger ?? null,
175
+ tokensBefore: entry.data.tokensBefore,
176
+ tokensAfter: entry.data.tokensAfter ?? null,
177
+ trigger: entry.data.reason ?? null,
176
178
  }));
177
179
  }
178
180
  return Object.freeze(markers);
@@ -187,7 +189,9 @@ function compactionNoticesIn(rows) {
187
189
  at: timestamp,
188
190
  trigger: entry.trigger,
189
191
  result: entry.result,
190
- diagnostic: entry.diagnostic ?? null,
192
+ diagnostic: entry.result === "stale" || entry.result === "insufficient"
193
+ ? null
194
+ : (entry.diagnostic ?? null),
191
195
  }),
192
196
  ]
193
197
  : []));
@@ -268,37 +272,7 @@ function readPendingPrompts(owner, sessionId) {
268
272
  const prompts = [];
269
273
  let bytes = encoder.encode(JSON.stringify({ prompts: [], hasMore: false, total })).byteLength;
270
274
  for (const row of rows) {
271
- const entry = promptCommand(row.entry);
272
- if (entry === undefined)
273
- throw unavailable("Pending Session prompt is unavailable");
274
- const content = entry.message.content;
275
- const prompt = isPromptParts(content)
276
- ? {
277
- commandId: row.entry.workId,
278
- text: content
279
- .filter((part) => part.type === "text")
280
- .map((part) => part.text)
281
- .join(""),
282
- images: content.filter((part) => part.type === "image"),
283
- delivery: entry.delivery ?? "followUp",
284
- }
285
- : {
286
- commandId: row.entry.workId,
287
- text: content,
288
- delivery: entry.delivery ?? "followUp",
289
- };
290
- const withMeta = row.entry.meta === undefined
291
- ? prompt
292
- : { ...prompt, meta: row.entry.meta };
293
- const attributed = row.origin.kind === "extension"
294
- ? {
295
- ...withMeta,
296
- origin: {
297
- extensionId: row.origin.extensionId,
298
- correlation: row.origin.correlation,
299
- },
300
- }
301
- : withMeta;
275
+ const attributed = projectPendingPrompt(row);
302
276
  const size = encoder.encode(JSON.stringify(attributed)).byteLength +
303
277
  (prompts.length === 0 ? 0 : 1);
304
278
  if (prompts.length === maximumRowsPerPage ||
@@ -309,6 +283,45 @@ function readPendingPrompts(owner, sessionId) {
309
283
  }
310
284
  return { prompts, hasMore: false, total };
311
285
  }
286
+ /** Same payload projection as the FIFO page, selected independently of that page's bounds. */
287
+ export function readLatestPendingPrompt(owner, sessionId, identity) {
288
+ const row = readNewestPendingSessionCommand(owner, sessionId, identity);
289
+ return row === undefined ? null : projectPendingPrompt(row);
290
+ }
291
+ function projectPendingPrompt(row) {
292
+ const entry = promptCommand(row.entry);
293
+ if (entry === undefined)
294
+ throw unavailable("Pending Session prompt is unavailable");
295
+ const content = entry.message.content;
296
+ const prompt = isPromptParts(content)
297
+ ? {
298
+ commandId: row.entry.workId,
299
+ requestId: entry.requestId,
300
+ text: content
301
+ .filter((part) => part.type === "text")
302
+ .map((part) => part.text)
303
+ .join(""),
304
+ images: content.filter((part) => part.type === "image"),
305
+ delivery: entry.delivery ?? "followUp",
306
+ }
307
+ : {
308
+ commandId: row.entry.workId,
309
+ requestId: entry.requestId,
310
+ text: content,
311
+ delivery: entry.delivery ?? "followUp",
312
+ };
313
+ const withMeta = row.entry.meta === undefined ? prompt : { ...prompt, meta: row.entry.meta };
314
+ const attributed = row.origin.kind === "extension"
315
+ ? {
316
+ ...withMeta,
317
+ origin: {
318
+ extensionId: row.origin.extensionId,
319
+ correlation: row.origin.correlation,
320
+ },
321
+ }
322
+ : withMeta;
323
+ return attributed;
324
+ }
312
325
  /**
313
326
  * The exclusive upper sequence a baseline read pages back from: none for the
314
327
  * latest page, or exactly the entry a `before` cursor names. Undefined means
@@ -345,6 +358,7 @@ export function readClientSnapshot(owner, sessionId, options, activity, projectE
345
358
  pending,
346
359
  activity: activity.facts,
347
360
  progress: activity.progress,
361
+ toolOutput: activity.toolOutput,
348
362
  extensions: projectExtensions(),
349
363
  attention: readSessionAttention(owner, sessionId),
350
364
  });
@@ -379,6 +393,7 @@ export function readClientSnapshot(owner, sessionId, options, activity, projectE
379
393
  pending,
380
394
  activity: activity.facts,
381
395
  progress: options.before === undefined ? activity.progress : null,
396
+ toolOutput: options.before === undefined ? activity.toolOutput : [],
382
397
  extensions: projectExtensions(),
383
398
  attention: readSessionAttention(owner, sessionId),
384
399
  };
@@ -433,7 +448,16 @@ function replayFrames(owner, lineage, start, delivery, activity, extensions, att
433
448
  }));
434
449
  return Object.freeze({
435
450
  kind: "page",
436
- frames: Object.freeze(frames),
451
+ frames: Object.freeze(!page.hasMore
452
+ ? [
453
+ ...frames,
454
+ {
455
+ kind: "tool_output",
456
+ toolOutput: activity.toolOutput,
457
+ activity: activity.facts,
458
+ },
459
+ ]
460
+ : frames),
437
461
  hasMore: page.hasMore,
438
462
  });
439
463
  }
@@ -494,7 +518,9 @@ export function projectCommandSettlement(owner, sessionId, commandId) {
494
518
  : []);
495
519
  const last = answers.at(-1);
496
520
  const meta = terminal.outcome === "cancelled" ? terminal.meta : admission.entry.meta;
521
+ const sender = messageAuthor(admission.entry.meta);
497
522
  return {
523
+ acceptedSender: sender?.kind === "agent" ? sender.id : null,
498
524
  terminal: meta === undefined ? terminal : { ...terminal, meta },
499
525
  content: terminal.outcome === "cancelled"
500
526
  ? ""
@@ -0,0 +1,4 @@
1
+ import type { ColdSessionPage, ColdSessionPageRequest } from "../../cold-sessions.js";
2
+ import type { SqliteOwner } from "../contracts.js";
3
+ /** Read one bounded page of Sessions with stored bytes, coldest first. */
4
+ export declare function readColdSessionPageOwned(owner: SqliteOwner, request: ColdSessionPageRequest): ColdSessionPage;