@fungi.computer/shiitake 0.2.0 → 0.3.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 (112) 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 +106 -22
  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 +7 -4
  35. package/dist/internal/compaction/compaction.js +59 -30
  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.js +5 -0
  39. package/dist/internal/compaction/utils.d.ts +19 -16
  40. package/dist/internal/compaction/utils.js +30 -84
  41. package/dist/internal/contracts.d.ts +40 -13
  42. package/dist/internal/coordinator.js +28 -10
  43. package/dist/internal/effect-client.d.ts +4 -2
  44. package/dist/internal/effect-client.js +29 -0
  45. package/dist/internal/effect-contracts.d.ts +17 -4
  46. package/dist/internal/extension-runtime.d.ts +5 -0
  47. package/dist/internal/extension-runtime.js +184 -89
  48. package/dist/internal/host-meta.d.ts +7 -0
  49. package/dist/internal/model-acquisition.js +7 -1
  50. package/dist/internal/model-preference.d.ts +2 -0
  51. package/dist/internal/model-preference.js +2 -1
  52. package/dist/internal/promise-facade.js +4 -16
  53. package/dist/internal/run-failure-reason.d.ts +10 -0
  54. package/dist/internal/run-failure-reason.js +19 -0
  55. package/dist/internal/run-scope.d.ts +4 -1
  56. package/dist/internal/run-scope.js +38 -5
  57. package/dist/internal/runtime-coordinator.d.ts +2 -1
  58. package/dist/internal/runtime-coordinator.js +65 -4
  59. package/dist/internal/runtime-core.js +1 -0
  60. package/dist/internal/runtime-reconciliation.js +42 -6
  61. package/dist/internal/session-context.d.ts +1 -2
  62. package/dist/internal/session-context.js +8 -30
  63. package/dist/internal/session-status.d.ts +2 -1
  64. package/dist/internal/session-status.js +4 -1
  65. package/dist/internal/woodstock/client-projection.d.ts +6 -5
  66. package/dist/internal/woodstock/client-projection.js +79 -53
  67. package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
  68. package/dist/internal/woodstock/cold-sessions.js +90 -0
  69. package/dist/internal/woodstock/extension-store.d.ts +2 -0
  70. package/dist/internal/woodstock/extension-store.js +4 -0
  71. package/dist/internal/woodstock/fleet-store.d.ts +5 -1
  72. package/dist/internal/woodstock/fleet-store.js +17 -5
  73. package/dist/internal/woodstock/index.d.ts +11 -3
  74. package/dist/internal/woodstock/index.js +33 -14
  75. package/dist/internal/woodstock/progress.d.ts +14 -1
  76. package/dist/internal/woodstock/progress.js +97 -19
  77. package/dist/internal/woodstock/run-recovery.js +6 -4
  78. package/dist/internal/woodstock/session-attention.d.ts +1 -1
  79. package/dist/internal/woodstock/session-attention.js +2 -2
  80. package/dist/internal/woodstock/session-entry.d.ts +109 -42
  81. package/dist/internal/woodstock/session-entry.js +63 -20
  82. package/dist/internal/woodstock/session-inbox.js +2 -0
  83. package/dist/internal/woodstock/session-store.d.ts +104 -24
  84. package/dist/internal/woodstock/session-store.js +937 -246
  85. package/dist/internal/woodstock/storage-admission.d.ts +8 -8
  86. package/dist/internal/woodstock/storage-admission.js +8 -34
  87. package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
  88. package/dist/internal/woodstock/storage-conformance.js +489 -0
  89. package/dist/model-preference.d.ts +2 -2
  90. package/dist/model-preference.js +2 -2
  91. package/dist/node.js +1 -1
  92. package/dist/remote-client.js +8 -1
  93. package/dist/session-attention.d.ts +8 -1
  94. package/dist/session-attention.js +6 -1
  95. package/dist/testing.js +1 -0
  96. package/docs/api/README.md +18 -16
  97. package/docs/api/agent-control.md +171 -1
  98. package/docs/api/client-entry.md +26 -10
  99. package/docs/api/client.md +153 -37
  100. package/docs/api/cloudflare.md +76 -23
  101. package/docs/api/delegation.md +485 -0
  102. package/docs/api/effect.md +20 -2
  103. package/docs/api/extensions.md +21 -9
  104. package/docs/api/fleet.md +159 -104
  105. package/docs/api/http.md +4 -1
  106. package/docs/api/index.md +116 -20
  107. package/docs/api/internal/woodstock/storage-conformance.md +81 -0
  108. package/docs/api/model-preference.md +17 -0
  109. package/docs/api/testing.md +16 -4
  110. package/package.json +26 -14
  111. package/dist/internal/browser-sessions.d.ts +0 -31
  112. package/dist/internal/browser-sessions.js +0 -173
@@ -0,0 +1,90 @@
1
+ import * as Schema from "effect/Schema";
2
+ import { ShiitakeError } from "../../errors.js";
3
+ /**
4
+ * The store keeper's read of cold Sessions. It is one indexed keyset page over
5
+ * the inbox, with each Session's bytes summed from its own rows, so no counter
6
+ * can drift and nothing needs reconciling.
7
+ */
8
+ const defaultLimit = 50;
9
+ const maxLimit = 200;
10
+ const closed = { onExcessProperty: "error" };
11
+ const encoder = new TextEncoder();
12
+ const timestamp = Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)));
13
+ const boundedId = Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.makeFilter((value) => encoder.encode(value).byteLength <= 256)));
14
+ const RequestSchema = Schema.Struct({
15
+ limit: Schema.optionalKey(Schema.Int.pipe(Schema.check(Schema.isBetween({ minimum: 1, maximum: maxLimit })))),
16
+ after: Schema.optionalKey(Schema.String),
17
+ });
18
+ const decodeRequest = Schema.decodeUnknownResult(RequestSchema, closed);
19
+ const CursorSchema = Schema.fromJsonString(Schema.Tuple([timestamp, boundedId]));
20
+ const decodeCursor = Schema.decodeUnknownResult(CursorSchema);
21
+ const encodeCursor = Schema.encodeSync(CursorSchema);
22
+ const RowSchema = Schema.Struct({
23
+ session_id: boundedId,
24
+ last_activity_at: timestamp,
25
+ state: Schema.Literals(["needs_you", "running", "errored", "done"]),
26
+ bytes: Schema.Int.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0))),
27
+ });
28
+ const decodeRow = Schema.decodeUnknownResult(RowSchema, closed);
29
+ const invalid = (message) => new ShiitakeError("contract", "invalid", message);
30
+ const payloadBytes = (table, column) => `(SELECT COALESCE(SUM(LENGTH(CAST(${column} AS BLOB))), 0) FROM ${table} WHERE session_id = p.session_id)`;
31
+ /**
32
+ * The coldest-first window is chosen from 'session_inbox' alone; stored bytes
33
+ * are summed only for its first 'limit' rows. The extra lookahead row only
34
+ * proves another page exists and is discarded, so it is never measured.
35
+ */
36
+ const selectColdSessions = (where) => `
37
+ WITH p AS (
38
+ SELECT i.session_id, i.last_activity_at, i.state,
39
+ ROW_NUMBER() OVER (ORDER BY i.last_activity_at ASC, i.session_id ASC) AS position
40
+ FROM session_inbox i
41
+ ${where}
42
+ ORDER BY i.last_activity_at ASC, i.session_id ASC LIMIT ?
43
+ )
44
+ SELECT p.session_id, p.last_activity_at, p.state,
45
+ CASE WHEN p.position <= ? THEN
46
+ ${payloadBytes("session_entries", "data_json")}
47
+ + ${payloadBytes("session_command_admissions", "data_json")}
48
+ + ${payloadBytes("session_attention", "value_json")}
49
+ + ${payloadBytes("session_tool_grants", "grants_json")}
50
+ ELSE 0 END AS bytes
51
+ FROM p ORDER BY p.position`;
52
+ /** Read one bounded page of Sessions with stored bytes, coldest first. */
53
+ export function readColdSessionPageOwned(owner, request) {
54
+ const decodedRequest = decodeRequest(request);
55
+ if (decodedRequest._tag === "Failure")
56
+ throw invalid("Cold Session page request is invalid");
57
+ const limit = decodedRequest.success.limit ?? defaultLimit;
58
+ const after = decodedRequest.success.after;
59
+ let cursor;
60
+ if (after !== undefined) {
61
+ const decoded = decodeCursor(after);
62
+ if (decoded._tag === "Failure")
63
+ throw invalid("Cold Session cursor is invalid");
64
+ cursor = decoded.success;
65
+ }
66
+ // One extra row tells a full last page from a page with more behind it.
67
+ const raws = cursor === undefined
68
+ ? owner.sql.exec(selectColdSessions(""), limit + 1, limit).toArray()
69
+ : owner.sql
70
+ .exec(selectColdSessions(`WHERE i.last_activity_at > ?
71
+ OR (i.last_activity_at = ? AND i.session_id > ?)`), cursor[0], cursor[0], cursor[1], limit + 1, limit)
72
+ .toArray();
73
+ const sessions = [];
74
+ for (const raw of raws.slice(0, limit)) {
75
+ const decoded = decodeRow(raw);
76
+ if (decoded._tag === "Failure")
77
+ throw new ShiitakeError("store", "unavailable", "Stored Session row is invalid");
78
+ const row = decoded.success;
79
+ sessions.push({
80
+ sessionId: row.session_id,
81
+ bytes: row.bytes,
82
+ lastActivityAt: row.last_activity_at,
83
+ idle: row.state === "done" || row.state === "errored",
84
+ });
85
+ }
86
+ const last = sessions.at(-1);
87
+ return raws.length > limit && last !== undefined
88
+ ? { sessions, next: encodeCursor([last.lastActivityAt, last.sessionId]) }
89
+ : { sessions };
90
+ }
@@ -43,6 +43,8 @@ export declare function writeStatus(owner: SqliteOwner, extensionId: string, sta
43
43
  export declare function writeTimer(owner: SqliteOwner, extensionId: string, key: string, dueAt: number): void;
44
44
  /** Delete one timer; absent keys are a no-op. */
45
45
  export declare function deleteTimer(owner: SqliteOwner, extensionId: string, key: string): void;
46
+ /** Release all timer registrations when their extension leaves the roster. */
47
+ export declare function deleteExtensionTimers(owner: SqliteOwner, extensionId: string): void;
46
48
  /** Read one timer's due instant. */
47
49
  export declare function readTimer(owner: SqliteOwner, extensionId: string, key: string): number | undefined;
48
50
  /** Due timers of the given extensions, oldest first, bounded by `limit`. */
@@ -194,6 +194,10 @@ export function writeTimer(owner, extensionId, key, dueAt) {
194
194
  export function deleteTimer(owner, extensionId, key) {
195
195
  owner.sql.exec("DELETE FROM shiitake_extension_timers WHERE extension_id = ? AND key = ?", extensionId, key);
196
196
  }
197
+ /** Release all timer registrations when their extension leaves the roster. */
198
+ export function deleteExtensionTimers(owner, extensionId) {
199
+ owner.sql.exec("DELETE FROM shiitake_extension_timers WHERE extension_id = ?", extensionId);
200
+ }
197
201
  /** Read one timer's due instant. */
198
202
  export function readTimer(owner, extensionId, key) {
199
203
  const rows = owner.sql
@@ -10,7 +10,7 @@ export declare function createFleetDirectoryStore(sql: WatchdogSqliteOwner["sql"
10
10
  findByName: (name: string) => {
11
11
  readonly [x: string]: string | number | ArrayBuffer | null;
12
12
  } | undefined;
13
- reserve(agentId: string, name: string, actorUserId: string, now: number, requestId: string, configuration: string): void;
13
+ reserve(agentId: string, name: string, actorUserId: string, now: number, requestId: string, configuration: string, createdBy: string | null): void;
14
14
  list: () => readonly {
15
15
  readonly [x: string]: string | number | ArrayBuffer | null;
16
16
  }[];
@@ -27,6 +27,10 @@ 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 delegation this owner has sent and had accepted, newest first. */
31
+ sent: () => readonly {
32
+ readonly [x: string]: string | number | ArrayBuffer | null;
33
+ }[];
30
34
  reserve(requestId: string, input: string, mail: string): void;
31
35
  recordReceipt(requestId: string, receipt: string): void;
32
36
  };
@@ -6,9 +6,17 @@ export function createFleetDirectoryStore(sql) {
6
6
  agent_id TEXT PRIMARY KEY,
7
7
  name TEXT NOT NULL UNIQUE,
8
8
  actor_user_id TEXT NOT NULL,
9
+ created_by TEXT,
9
10
  created_at INTEGER NOT NULL,
10
11
  updated_at INTEGER NOT NULL
11
12
  ) STRICT`);
13
+ // Existing pre-release Agents have no recorded maker. Never infer one from
14
+ // correspondence or actor_user_id (the human credential owner).
15
+ const columns = input.sql
16
+ .exec("PRAGMA table_info(hub_team_agents)")
17
+ .toArray();
18
+ if (!columns.some((column) => column.name === "created_by"))
19
+ input.sql.exec("ALTER TABLE hub_team_agents ADD COLUMN created_by TEXT");
12
20
  input.sql.exec(`CREATE TABLE IF NOT EXISTS hub_team_agent_create_receipts (
13
21
  request_id TEXT PRIMARY KEY,
14
22
  actor_user_id TEXT NOT NULL,
@@ -28,21 +36,21 @@ export function createFleetDirectoryStore(sql) {
28
36
  input.sql.exec("INSERT OR IGNORE INTO hub_team_agent_erasure_scope(singleton,complete) VALUES (1,1)");
29
37
  return {
30
38
  read: (agentId) => input.sql
31
- .exec("SELECT agent_id, name, actor_user_id, created_at, updated_at FROM hub_team_agents WHERE agent_id = ?", agentId)
39
+ .exec("SELECT agent_id, name, actor_user_id, created_by, created_at, updated_at FROM hub_team_agents WHERE agent_id = ?", agentId)
32
40
  .toArray()[0],
33
41
  readReceipt: (requestId) => input.sql
34
42
  .exec("SELECT request_id, actor_user_id, name, agent_id, computer_mode FROM hub_team_agent_create_receipts WHERE request_id = ?", requestId)
35
43
  .toArray()[0],
36
44
  findByName: (name) => input.sql
37
- .exec("SELECT agent_id, name, actor_user_id, created_at, updated_at FROM hub_team_agents WHERE name = ?", name)
45
+ .exec("SELECT agent_id, name, actor_user_id, created_by, created_at, updated_at FROM hub_team_agents WHERE name = ?", name)
38
46
  .toArray()[0],
39
- reserve(agentId, name, actorUserId, now, requestId, configuration) {
40
- input.sql.exec("INSERT INTO hub_team_agents (agent_id, name, actor_user_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", agentId, name, actorUserId, now, now);
47
+ reserve(agentId, name, actorUserId, now, requestId, configuration, createdBy) {
48
+ input.sql.exec("INSERT INTO hub_team_agents (agent_id, name, actor_user_id, created_by, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)", agentId, name, actorUserId, createdBy, now, now);
41
49
  input.sql.exec("INSERT INTO hub_team_agent_erasure_ids(agent_id) VALUES (?)", agentId);
42
50
  input.sql.exec("INSERT INTO hub_team_agent_create_receipts (request_id, actor_user_id, name, agent_id, computer_mode) VALUES (?, ?, ?, ?, ?)", requestId, actorUserId, name, agentId, configuration);
43
51
  },
44
52
  list: () => input.sql
45
- .exec("SELECT agent_id, name, actor_user_id, created_at, updated_at FROM hub_team_agents ORDER BY created_at DESC, agent_id DESC")
53
+ .exec("SELECT agent_id, name, actor_user_id, created_by, created_at, updated_at FROM hub_team_agents ORDER BY created_at DESC, agent_id DESC")
46
54
  .toArray(),
47
55
  rename(agentId, name, now) {
48
56
  input.sql.exec("UPDATE hub_team_agents SET name = ?, updated_at = ? WHERE agent_id = ?", name, now, agentId);
@@ -79,6 +87,10 @@ export function createFleetDelegationStore(storage) {
79
87
  read: (requestId) => storage.sql
80
88
  .exec("SELECT input, mail, receipt FROM shiitake_fleet_delegations WHERE request_id = ?", requestId)
81
89
  .toArray()[0],
90
+ /** Every delegation this owner has sent and had accepted, newest first. */
91
+ sent: () => storage.sql
92
+ .exec("SELECT input, mail, receipt FROM shiitake_fleet_delegations WHERE receipt IS NOT NULL ORDER BY rowid DESC")
93
+ .toArray(),
82
94
  reserve(requestId, input, mail) {
83
95
  storage.transactionSync(() => {
84
96
  storage.sql.exec("INSERT OR IGNORE INTO shiitake_fleet_delegations(request_id, input, mail) VALUES (?, ?, ?)", requestId, input, mail);
@@ -1,8 +1,9 @@
1
1
  import type { SandboxFailureEvent } from "@fungi.computer/mycelium";
2
2
  import type { Json } from "../../extensions.js";
3
- import type { MuleError, MuleIdentity, MuleListener } from "@fungi.computer/mule/effect";
3
+ import type { MuleError, MuleEvent, MuleIdentity } from "@fungi.computer/mule/effect";
4
4
  import * as Effect from "effect/Effect";
5
5
  import type * as Scope from "effect/Scope";
6
+ import type { ColdSessionPage, ColdSessionPageRequest } from "../../cold-sessions.js";
6
7
  import type { ReadOptions, SessionExtensionSlots, SessionSnapshot } from "../../client.js";
7
8
  import { ShiitakeError } from "../../errors.js";
8
9
  import type { CommandSettlement, SqliteOwner } from "../contracts.js";
@@ -11,6 +12,7 @@ import { type ClientReplayPage } from "./client-projection.js";
11
12
  import { type LiveRetryEvent } from "./session-entry.js";
12
13
  import { type CompactionNoticeInput, type AcceptedCommand, type EnqueueAcceptedWork, type AppendCompactionInput, type PreparedSessionRun, type RunContract, type RecordAcceptedCancellation, type SessionQueueHead, type StoredSessionRow } from "./session-store.js";
13
14
  export * from "./extension-store.js";
15
+ export { readLatestPendingPrompt } from "./client-projection.js";
14
16
  export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
15
17
  export * from "./session-attention.js";
16
18
  export { deliverSessionGrants, prepareSessionGrants, readSessionGrants, type GrantsDelivery, type GrantsDeliveryResult, } from "./session-grants.js";
@@ -26,6 +28,8 @@ export * from "./storage-admission.js";
26
28
  export declare function appendCompactionNotice(owner: SqliteOwner, input: CompactionNoticeInput): StoredSessionRow;
27
29
  /** Append compaction through Woodstock's ordinary owner-observation boundary. */
28
30
  export declare function appendCompactionTransition(owner: SqliteOwner, input: AppendCompactionInput): StoredSessionRow;
31
+ /** Place a background summary, head-checked in the append transaction. */
32
+ export declare function placeCompactionTransition(owner: SqliteOwner, input: AppendCompactionInput): StoredSessionRow | "stale";
29
33
  /** Prepare Woodstock's retained Session and private admission schema. */
30
34
  export declare function prepareWoodstockSchema(owner: SqliteOwner): Effect.Effect<void, ShiitakeError>;
31
35
  /** Atomically cancel one exact owner-issued command. */
@@ -52,6 +56,7 @@ export declare function settleControls(owner: SqliteOwner, input: Readonly<{
52
56
  sessionId: string;
53
57
  parentWorkId: string;
54
58
  outcome: "completed" | "cancelled" | "failed" | "interrupted";
59
+ diagnostic: string | undefined;
55
60
  enqueue: EnqueueAcceptedWork;
56
61
  }>): Effect.Effect<void, ShiitakeError>;
57
62
  /** Resolve Session-scoped abort to private work and record it atomically. */
@@ -127,6 +132,8 @@ export declare function deleteSession(owner: SqliteOwner, input: Readonly<{
127
132
  }>): Effect.Effect<boolean, ShiitakeError>;
128
133
  /** Read a page of stored conversation facts; status needs the Session owner. */
129
134
  export declare function readSessionFactPage(owner: SqliteOwner, after?: string): Effect.Effect<import("./session-store.js").StoredSessionSummaryPage, ShiitakeError>;
135
+ /** Read one bounded page of Sessions with stored bytes, coldest first. */
136
+ export declare function readColdSessionPage(owner: SqliteOwner, request: ColdSessionPageRequest): Effect.Effect<ColdSessionPage, ShiitakeError>;
130
137
  /** Read one bounded inbox page in tier, then newest-activity order. */
131
138
  export declare function readSessionInbox(owner: SqliteOwner, after: string | undefined): Effect.Effect<import("../../client.js").SessionSummaryPage, ShiitakeError>;
132
139
  /** Read one strict-after page from Woodstock's selected lineage. */
@@ -149,7 +156,7 @@ export type SettledRunFacts = Readonly<{
149
156
  export declare function readExtensionAdmissionOrigin(owner: SqliteOwner, sessionId: string, commandFactId: string, workId: string): ExtensionAdmissionOrigin;
150
157
  /** Read the terminal and admission origin of one settled accepted command. */
151
158
  export declare function readSettledRunFacts(owner: SqliteOwner, sessionId: string, commandFactId: string, workId: string): SettledRunFacts;
152
- /** Record unsuccessful Watchdog settlement when Mule has no recorded terminal. */
159
+ /** Record settlement only when Mule has no matching recorded terminal. */
153
160
  export declare function recordWorkSettlement(owner: SqliteOwner, input: Readonly<{
154
161
  sessionId: string;
155
162
  commandFactId: string;
@@ -160,11 +167,12 @@ export declare function recordWorkSettlement(owner: SqliteOwner, input: Readonly
160
167
  }>): Effect.Effect<void, ShiitakeError>;
161
168
  /** Record one retained live Pi retry observation on the Session lineage. */
162
169
  export declare function recordLiveRetryEvent(owner: SqliteOwner, accepted: AcceptedCommand, event: LiveRetryEvent, turn: MuleIdentity): Effect.Effect<void, MuleError>;
170
+ export type MuleRecorder = (event: MuleEvent, diagnostic?: string) => Effect.Effect<void, MuleError>;
163
171
  /**
164
172
  * Open the one scoped listener that projects Mule's exact event stream into
165
173
  * retained Session facts and bounded process-local progress.
166
174
  */
167
- export declare function openMuleRecorder(owner: SqliteOwner, accepted: AcceptedCommand, contract: RunContract): Effect.Effect<MuleListener, never, Scope.Scope>;
175
+ export declare function openMuleRecorder(owner: SqliteOwner, accepted: AcceptedCommand, contract: RunContract): Effect.Effect<MuleRecorder, never, Scope.Scope>;
168
176
  export { declareSessionDocument } from "./session-documents.js";
169
177
  /** Read exact settled work facts through Woodstock's native command/work indexes. */
170
178
  export declare function readCommandSettlement(owner: SqliteOwner, sessionId: string, commandId: string): Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
@@ -2,13 +2,15 @@ import * as Effect from "effect/Effect";
2
2
  import { ShiitakeError } from "../../errors.js";
3
3
  import { decodeClientReadOptions, decodeClientSessionCreationRequestId, decodeClientSessionId, } from "../client-boundary.js";
4
4
  import { publishOwnerObservation } from "../observation-kernel.js";
5
+ import { readColdSessionPageOwned } from "./cold-sessions.js";
5
6
  import { diagnosticsByWorkId, projectCommandSettlement, readClientReplayPage, readClientSnapshot, } from "./client-projection.js";
6
7
  import { sessionIds } from "./identities.js";
7
- import { openProgress } from "./progress.js";
8
+ import { openProgress, openToolProgress } from "./progress.js";
8
9
  import { encodeStoredSessionEntry, } from "./session-entry.js";
9
- import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNoticeOwned, acceptSessionAbort, acceptSessionCancellation, completeSessionLeaf, prepareSessionRun, prepareSessionStore, readSessionEntriesForWork, readSessionCommandAdmission, settleSessionControls, takePendingSessionControls, readSessionEntryById, selectSessionBranch, forkSessionAt, readStoredSessionSummary as readSessionSummaryOwned, readStoredSessionFactPage, createStoredSession, deleteStoredSession, appendOperatorEntry, } from "./session-store.js";
10
+ import { appendSessionEntry, appendCompactionTransitionOwned, appendCompactionNoticeOwned, placeCompactionTransitionOwned, acceptSessionAbort, acceptSessionCancellation, completeSessionLeaf, prepareSessionRun, prepareSessionStore, readSessionEntriesForWork, readSessionCommandAdmission, settleSessionControls, takePendingSessionControls, readSessionEntryById, selectSessionBranch, forkSessionAt, readStoredSessionSummary as readSessionSummaryOwned, readStoredSessionFactPage, createStoredSession, deleteStoredSession, appendOperatorEntry, } from "./session-store.js";
10
11
  import { readSessionInboxPage, recordSessionInboxDeletion, } from "./session-inbox.js";
11
12
  export * from "./extension-store.js";
13
+ export { readLatestPendingPrompt } from "./client-projection.js";
12
14
  export { createFleetDirectoryStore, createFleetDelegationStore, } from "./fleet-store.js";
13
15
  export * from "./session-attention.js";
14
16
  export { deliverSessionGrants, prepareSessionGrants, readSessionGrants, } from "./session-grants.js";
@@ -32,6 +34,13 @@ export function appendCompactionTransition(owner, input) {
32
34
  publishOwnerObservation(owner, input.sessionId);
33
35
  return row;
34
36
  }
37
+ /** Place a background summary, head-checked in the append transaction. */
38
+ export function placeCompactionTransition(owner, input) {
39
+ const placed = placeCompactionTransitionOwned(owner, input);
40
+ if (placed !== "stale")
41
+ publishOwnerObservation(owner, input.sessionId);
42
+ return placed;
43
+ }
35
44
  function rejected(message) {
36
45
  return new ShiitakeError("store", "rejected", message);
37
46
  }
@@ -207,6 +216,10 @@ export function deleteSession(owner, input) {
207
216
  export function readSessionFactPage(owner, after) {
208
217
  return storeEffect(() => readStoredSessionFactPage(owner, after === undefined ? undefined : decodeClientSessionId(after)));
209
218
  }
219
+ /** Read one bounded page of Sessions with stored bytes, coldest first. */
220
+ export function readColdSessionPage(owner, request) {
221
+ return storeEffect(() => readColdSessionPageOwned(owner, request));
222
+ }
210
223
  /** Read one bounded inbox page in tier, then newest-activity order. */
211
224
  export function readSessionInbox(owner, after) {
212
225
  return storeEffect(() => readSessionInboxPage(owner, after));
@@ -300,7 +313,7 @@ export function readSettledRunFacts(owner, sessionId, commandFactId, workId) {
300
313
  toolCalls,
301
314
  });
302
315
  }
303
- /** Record unsuccessful Watchdog settlement when Mule has no recorded terminal. */
316
+ /** Record settlement only when Mule has no matching recorded terminal. */
304
317
  export function recordWorkSettlement(owner, input) {
305
318
  return storeEffect(() => {
306
319
  const admission = readSessionCommandAdmission(owner, input.sessionId, input.commandFactId);
@@ -319,9 +332,9 @@ export function recordWorkSettlement(owner, input) {
319
332
  });
320
333
  });
321
334
  }
322
- function recordLifecycle(owner, accepted, contract, event) {
335
+ function recordLifecycle(owner, accepted, contract, event, diagnostic) {
323
336
  return storeEffect(() => {
324
- const entry = event.type === "run_start"
337
+ let entry = event.type === "run_start"
325
338
  ? {
326
339
  kind: "run_started",
327
340
  workId: accepted.commandId,
@@ -342,6 +355,8 @@ function recordLifecycle(owner, accepted, contract, event) {
342
355
  terminal: event.terminal,
343
356
  outcome: event.outcome,
344
357
  };
358
+ if (entry.kind === "run_terminal" && diagnostic !== undefined)
359
+ entry = { ...entry, diagnostic };
345
360
  appendRecordedEntry(owner, accepted, entry, ({ entry: current }) => {
346
361
  if (current.kind !== entry.kind)
347
362
  return false;
@@ -422,18 +437,19 @@ function recordMessageEnd(owner, accepted, event) {
422
437
  message: event.message,
423
438
  value: event.value,
424
439
  };
440
+ const completed = event.data === undefined ? entry : { ...entry, data: event.data };
425
441
  const delivery = owner.transactionSync(() => {
426
442
  const existing = readSessionEntriesForWork(owner, accepted.sessionId, accepted.commandId).find(({ entry: current }) => (current.kind === "message_start" || current.kind === "message") &&
427
443
  sameIdentity(current.message, event.message));
428
444
  if (existing?.entry.kind === "message") {
429
- if (!sameEntry(existing.entry, entry))
445
+ if (!sameEntry(existing.entry, completed))
430
446
  throw conflict("Mule completed-message identity conflicts");
431
447
  return "replay";
432
448
  }
433
449
  completeSessionLeaf(owner, {
434
450
  sessionId: accepted.sessionId,
435
451
  id: event.message.value,
436
- entry,
452
+ entry: completed,
437
453
  });
438
454
  return "new";
439
455
  });
@@ -460,6 +476,7 @@ function recordToolIntent(owner, accepted, contract, event) {
460
476
  }
461
477
  function recordToolOutcome(owner, accepted, event) {
462
478
  return storeEffect(() => {
479
+ const { data, ...result } = event.result;
463
480
  const entry = {
464
481
  kind: "tool_outcome",
465
482
  workId: accepted.commandId,
@@ -467,9 +484,9 @@ function recordToolOutcome(owner, accepted, event) {
467
484
  turn: event.turn,
468
485
  toolCall: event.toolCall,
469
486
  call: event.call,
470
- result: event.result,
487
+ result,
471
488
  };
472
- appendRecordedEntry(owner, accepted, entry, ({ entry: current }) => current.kind === "tool_outcome" &&
489
+ appendRecordedEntry(owner, accepted, data === undefined ? entry : { ...entry, data }, ({ entry: current }) => current.kind === "tool_outcome" &&
473
490
  sameIdentity(current.toolCall, event.toolCall));
474
491
  });
475
492
  }
@@ -505,14 +522,15 @@ const listenerFailure = () => ({
505
522
  export function openMuleRecorder(owner, accepted, contract) {
506
523
  return Effect.gen(function* () {
507
524
  const progress = yield* openProgress(owner, accepted, (event) => recordMessageEnd(owner, accepted, event));
508
- const record = (event) => {
525
+ const toolProgress = yield* openToolProgress(owner, accepted);
526
+ const record = (event, diagnostic) => {
509
527
  switch (event.type) {
510
528
  case "model_prompt":
511
529
  return recordModelPrompt(owner, accepted, contract, event);
512
530
  case "run_start":
513
531
  case "turn_start":
514
532
  case "run_end":
515
- return recordLifecycle(owner, accepted, contract, event);
533
+ return recordLifecycle(owner, accepted, contract, event, diagnostic);
516
534
  case "message_start":
517
535
  if (event.value.role === "user")
518
536
  return Effect.void;
@@ -528,15 +546,16 @@ export function openMuleRecorder(owner, accepted, contract) {
528
546
  ? progress.terminal(event)
529
547
  : recordMessageEnd(owner, accepted, event);
530
548
  case "tool_execution_start":
531
- return recordToolIntent(owner, accepted, contract, event);
549
+ return recordToolIntent(owner, accepted, contract, event).pipe(Effect.andThen(toolProgress.started(event)));
532
550
  case "tool_execution_end":
533
- return recordToolOutcome(owner, accepted, event);
551
+ return recordToolOutcome(owner, accepted, event).pipe(Effect.andThen(toolProgress.terminal(event)));
534
552
  case "tool_execution_update":
553
+ return toolProgress.update(event);
535
554
  case "turn_end":
536
555
  return Effect.void;
537
556
  }
538
557
  };
539
- return (event) => record(event).pipe(Effect.mapError(listenerFailure));
558
+ return (event, diagnostic) => record(event, diagnostic).pipe(Effect.mapError(listenerFailure));
540
559
  });
541
560
  }
542
561
  export { declareSessionDocument } from "./session-documents.js";
@@ -1,10 +1,23 @@
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 } from "../../client.js";
4
+ import type { SessionProgress, 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
+ export declare function readProcessToolOutputForWork(owner: SqliteOwner, sessionId: string, workId: string): readonly SessionToolOutput[];
9
+ /** Run-scoped, replaceable tool snapshots: never stored in Woodstock. */
10
+ export declare function openToolProgress(owner: SqliteOwner, accepted: AcceptedCommand): Effect.Effect<{
11
+ started: (event: Extract<MuleEvent, {
12
+ type: "tool_execution_start";
13
+ }>) => Effect.Effect<void, never, never>;
14
+ update: (event: Extract<MuleEvent, {
15
+ type: "tool_execution_update";
16
+ }>) => Effect.Effect<void, never, never>;
17
+ terminal: (event: Extract<MuleEvent, {
18
+ type: "tool_execution_end";
19
+ }>) => Effect.Effect<void, never, never>;
20
+ }, never, Scope.Scope>;
8
21
  /** Read the current process-local progress value for one addressed Session. */
9
22
  export declare function readProcessProgress(owner: SqliteOwner, sessionId: string): SessionProgress | null;
10
23
  /** Read progress only when it belongs to the current durable work head. */
@@ -1,7 +1,65 @@
1
1
  import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import * as Result from "effect/Result";
2
4
  import { ShiitakeError } from "../../errors.js";
3
5
  import { publishOwnerObservation } from "../observation-kernel.js";
4
6
  const progressByOwner = new WeakMap();
7
+ export function readProcessToolOutputForWork(owner, sessionId, workId) {
8
+ const current = progressByOwner.get(owner)?.get(sessionId);
9
+ return current?.workId === workId
10
+ ? [...current.slots.values()].filter((value) => value !== null)
11
+ : [];
12
+ }
13
+ const decodeOutputDetails = Schema.decodeUnknownResult(Schema.Struct({
14
+ droppedBytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
15
+ totalBytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
16
+ }), { onExcessProperty: "error" });
17
+ /** Run-scoped, replaceable tool snapshots: never stored in Woodstock. */
18
+ export function openToolProgress(owner, accepted) {
19
+ return Effect.gen(function* () {
20
+ const current = yield* openProcessProgress(owner, accepted);
21
+ const publish = () => {
22
+ publishOwnerObservation(owner, accepted.sessionId, "progress");
23
+ };
24
+ return {
25
+ started: (event) => Effect.sync(() => {
26
+ if (current.slots.size < 4 &&
27
+ !current.slots.has(event.toolCall.value))
28
+ current.slots.set(event.toolCall.value, null);
29
+ }),
30
+ update: (event) => Effect.sync(() => {
31
+ const previous = current.slots.get(event.toolCall.value);
32
+ if (previous === undefined)
33
+ return;
34
+ const details = decodeOutputDetails(event.result.data);
35
+ // Other tool-specific update contracts do not represent output tails.
36
+ if (Result.isFailure(details))
37
+ return;
38
+ const tail = event.result.content
39
+ .filter((part) => part.type === "text")
40
+ .map((part) => part.text)
41
+ .join("");
42
+ if (new TextEncoder().encode(tail).byteLength > 8192 ||
43
+ tail.split("\n").length > 200 ||
44
+ details.success.droppedBytes > details.success.totalBytes)
45
+ return;
46
+ current.slots.set(event.toolCall.value, {
47
+ // Slots follow Mule execution identity; clients join the provider's
48
+ // call ID carried by assistant calls and durable tool results.
49
+ toolCallId: event.call.id,
50
+ revision: (previous?.revision ?? 0) + 1,
51
+ tail,
52
+ ...details.success,
53
+ });
54
+ publish();
55
+ }),
56
+ terminal: (event) => Effect.sync(() => {
57
+ if (current.slots.delete(event.toolCall.value))
58
+ publish();
59
+ }),
60
+ };
61
+ });
62
+ }
5
63
  function progressFor(owner) {
6
64
  const existing = progressByOwner.get(owner);
7
65
  if (existing !== undefined)
@@ -10,6 +68,29 @@ function progressFor(owner) {
10
68
  progressByOwner.set(owner, created);
11
69
  return created;
12
70
  }
71
+ /** One run-scoped owner for message progress and tool tails. */
72
+ function openProcessProgress(owner, accepted) {
73
+ return Effect.gen(function* () {
74
+ const sessions = progressFor(owner);
75
+ const existing = sessions.get(accepted.sessionId);
76
+ if (existing?.workId === accepted.commandId)
77
+ return existing;
78
+ const current = {
79
+ workId: accepted.commandId,
80
+ value: null,
81
+ slots: new Map(),
82
+ };
83
+ sessions.set(accepted.sessionId, current);
84
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
85
+ if (sessions.get(accepted.sessionId) !== current)
86
+ return;
87
+ sessions.delete(accepted.sessionId);
88
+ if (current.value !== null || current.slots.size > 0)
89
+ publishOwnerObservation(owner, accepted.sessionId, "progress");
90
+ }));
91
+ return current;
92
+ });
93
+ }
13
94
  function rejected(message) {
14
95
  return new ShiitakeError("runtime", "rejected", message);
15
96
  }
@@ -41,9 +122,12 @@ function textFor(event) {
41
122
  */
42
123
  export function openProgress(owner, accepted, complete) {
43
124
  return Effect.gen(function* () {
125
+ const current = yield* openProcessProgress(owner, accepted);
44
126
  let started;
45
127
  const clear = Effect.sync(() => {
46
- if (progressFor(owner).delete(accepted.sessionId))
128
+ const hadProgress = current.value !== null;
129
+ current.value = null;
130
+ if (hadProgress)
47
131
  publishOwnerObservation(owner, accepted.sessionId, "progress");
48
132
  started = undefined;
49
133
  });
@@ -51,7 +135,7 @@ export function openProgress(owner, accepted, complete) {
51
135
  return Object.freeze({
52
136
  started: (event) => Effect.sync(() => {
53
137
  started = event;
54
- progressFor(owner).delete(accepted.sessionId);
138
+ current.value = null;
55
139
  }),
56
140
  update: (event) => Effect.suspend(() => {
57
141
  const messageStart = started;
@@ -61,25 +145,19 @@ export function openProgress(owner, accepted, complete) {
61
145
  messageStart.turn.value !== event.turn.value ||
62
146
  messageStart.message.value !== event.message.value)
63
147
  return Effect.fail(rejected("Mule progress identity conflicts"));
64
- const previous = progressFor(owner).get(accepted.sessionId);
148
+ const previous = current.value;
65
149
  const content = textFor(event);
66
- if (previous?.workId === accepted.commandId &&
67
- previous.value.text === content.text &&
68
- previous.value.thinking === content.thinking)
150
+ if (previous?.text === content.text &&
151
+ previous.thinking === content.thinking)
69
152
  return Effect.void;
70
- progressFor(owner).set(accepted.sessionId, Object.freeze({
71
- workId: accepted.commandId,
72
- value: Object.freeze({
73
- messageId: event.message.value,
74
- revision: (previous?.workId === accepted.commandId
75
- ? previous.value.revision
76
- : 0) + 1,
77
- text: content.text,
78
- thinking: content.thinking,
79
- textTruncated: false,
80
- thinkingTruncated: false,
81
- }),
82
- }));
153
+ current.value = Object.freeze({
154
+ messageId: event.message.value,
155
+ revision: (previous?.revision ?? 0) + 1,
156
+ text: content.text,
157
+ thinking: content.thinking,
158
+ textTruncated: false,
159
+ thinkingTruncated: false,
160
+ });
83
161
  publishOwnerObservation(owner, accepted.sessionId, "progress");
84
162
  return Effect.void;
85
163
  }),
@@ -10,7 +10,7 @@ export function recoverAssistantTurn(rows) {
10
10
  // A later request was already dispatched. Resend that request from the
11
11
  // completed transcript, not the previous assistant turn.
12
12
  if (after.some(({ entry }) => entry.kind === "turn_started" ||
13
- (entry.kind === "compaction" && entry.resumesRun === true) ||
13
+ (entry.kind === "compaction" && entry.data.resumesRun) ||
14
14
  (entry.kind === "live_retry" &&
15
15
  entry.event.type === "auto_retry_start")))
16
16
  return undefined;
@@ -28,7 +28,9 @@ export function recoverAssistantTurn(rows) {
28
28
  {
29
29
  call,
30
30
  state: "completed",
31
- result: outcome.result,
31
+ result: outcome.data === undefined
32
+ ? outcome.result
33
+ : { ...outcome.result, data: outcome.data },
32
34
  message: toolResults.find((result) => result.toolCallId === call.id),
33
35
  },
34
36
  ];
@@ -57,7 +59,7 @@ export function contextMessageIds(path, activeWorkId) {
57
59
  if (terminalWork.has(entry.workId) || entry.workId === activeWorkId)
58
60
  messages.add(row.id);
59
61
  }
60
- if ((entry.kind === "compaction" && entry.resumesRun === true) ||
62
+ if ((entry.kind === "compaction" && entry.data.resumesRun) ||
61
63
  (entry.kind === "live_retry" && entry.event.type === "auto_retry_start")) {
62
64
  const failed = lastWorkAssistant.get(entry.workId);
63
65
  if (failed !== undefined)
@@ -74,7 +76,7 @@ export function abnormalTerminal(rows) {
74
76
  return undefined;
75
77
  if (rows.some(({ sequence, entry }) => sequence > terminal.sequence &&
76
78
  (entry.kind === "run_started" ||
77
- (entry.kind === "compaction" && entry.resumesRun === true))))
79
+ (entry.kind === "compaction" && entry.data.resumesRun))))
78
80
  return undefined;
79
81
  if (terminal.entry.outcome === "cancelled")
80
82
  return "cancelled";
@@ -4,7 +4,7 @@ import type { SqliteOwner } from "../contracts.js";
4
4
  export declare function prepareSessionAttention(owner: SqliteOwner): void;
5
5
  /** Read the complete bounded attention projection inside the owner's transaction. */
6
6
  export declare function readSessionAttention(owner: SqliteOwner, sessionId: string): readonly SessionAttention[];
7
- /** Whether any owner still requests attention; no wall-clock expiry heuristic. */
7
+ /** Whether any owner requests inbox attention; drawer-only items stay visible. */
8
8
  export declare function hasSessionAttention(owner: SqliteOwner, sessionId: string): boolean;
9
9
  /** Upsert only the calling extension's item, in the calling transaction. */
10
10
  export declare function setSessionAttention(owner: SqliteOwner, sessionId: string, extensionId: string, item: AttentionItem): void;