@fungi.computer/shiitake 0.0.0-stage → 0.0.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 (164) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -2
  3. package/dist/agar-extension.d.ts +9 -0
  4. package/dist/agar-extension.js +38 -0
  5. package/dist/agent-control.d.ts +72 -0
  6. package/dist/agent-control.js +431 -0
  7. package/dist/client-entry.d.ts +211 -0
  8. package/dist/client-entry.js +101 -0
  9. package/dist/client-wire.d.ts +199 -0
  10. package/dist/client-wire.js +271 -0
  11. package/dist/client.d.ts +439 -0
  12. package/dist/client.js +4 -0
  13. package/dist/cloudflare.d.ts +220 -0
  14. package/dist/cloudflare.js +531 -0
  15. package/dist/effect.d.ts +42 -0
  16. package/dist/effect.js +20 -0
  17. package/dist/errors.d.ts +50 -0
  18. package/dist/errors.js +81 -0
  19. package/dist/extension-client.d.ts +38 -0
  20. package/dist/extension-client.js +56 -0
  21. package/dist/extension-contract.d.ts +73 -0
  22. package/dist/extension-contract.js +11 -0
  23. package/dist/extensions.d.ts +589 -0
  24. package/dist/extensions.js +263 -0
  25. package/dist/fleet.d.ts +297 -0
  26. package/dist/fleet.js +515 -0
  27. package/dist/hosted.d.ts +133 -0
  28. package/dist/hosted.js +265 -0
  29. package/dist/http-routes.d.ts +772 -0
  30. package/dist/http-routes.js +242 -0
  31. package/dist/http.d.ts +1 -0
  32. package/dist/http.js +1 -0
  33. package/dist/index.d.ts +35 -0
  34. package/dist/index.js +13 -0
  35. package/dist/internal/accepted-run-executor.d.ts +23 -0
  36. package/dist/internal/accepted-run-executor.js +129 -0
  37. package/dist/internal/admission-door.d.ts +75 -0
  38. package/dist/internal/admission-door.js +105 -0
  39. package/dist/internal/agent-close.d.ts +4 -0
  40. package/dist/internal/agent-close.js +42 -0
  41. package/dist/internal/browser-sessions.d.ts +31 -0
  42. package/dist/internal/browser-sessions.js +173 -0
  43. package/dist/internal/client-boundary.d.ts +262 -0
  44. package/dist/internal/client-boundary.js +202 -0
  45. package/dist/internal/compaction/compaction.d.ts +54 -0
  46. package/dist/internal/compaction/compaction.js +292 -0
  47. package/dist/internal/compaction/session-compaction.d.ts +69 -0
  48. package/dist/internal/compaction/session-compaction.js +330 -0
  49. package/dist/internal/compaction/settings.d.ts +11 -0
  50. package/dist/internal/compaction/settings.js +40 -0
  51. package/dist/internal/compaction/utils.d.ts +45 -0
  52. package/dist/internal/compaction/utils.js +319 -0
  53. package/dist/internal/contracts.d.ts +235 -0
  54. package/dist/internal/contracts.js +1 -0
  55. package/dist/internal/coordinator.d.ts +18 -0
  56. package/dist/internal/coordinator.js +362 -0
  57. package/dist/internal/document-codec.d.ts +11 -0
  58. package/dist/internal/document-codec.js +39 -0
  59. package/dist/internal/effect-client.d.ts +13 -0
  60. package/dist/internal/effect-client.js +102 -0
  61. package/dist/internal/effect-contracts.d.ts +236 -0
  62. package/dist/internal/effect-contracts.js +2 -0
  63. package/dist/internal/extension-command-registry.d.ts +11 -0
  64. package/dist/internal/extension-command-registry.js +29 -0
  65. package/dist/internal/extension-command-tools.d.ts +17 -0
  66. package/dist/internal/extension-command-tools.js +50 -0
  67. package/dist/internal/extension-context.d.ts +25 -0
  68. package/dist/internal/extension-context.js +35 -0
  69. package/dist/internal/extension-definitions.d.ts +5 -0
  70. package/dist/internal/extension-definitions.js +6 -0
  71. package/dist/internal/extension-hook.d.ts +25 -0
  72. package/dist/internal/extension-hook.js +46 -0
  73. package/dist/internal/extension-roster.d.ts +23 -0
  74. package/dist/internal/extension-roster.js +228 -0
  75. package/dist/internal/extension-runtime.d.ts +63 -0
  76. package/dist/internal/extension-runtime.js +1004 -0
  77. package/dist/internal/failure.d.ts +2 -0
  78. package/dist/internal/failure.js +9 -0
  79. package/dist/internal/host-meta.d.ts +41 -0
  80. package/dist/internal/host-meta.js +39 -0
  81. package/dist/internal/json-value.d.ts +4 -0
  82. package/dist/internal/json-value.js +3 -0
  83. package/dist/internal/model-acquisition.d.ts +21 -0
  84. package/dist/internal/model-acquisition.js +98 -0
  85. package/dist/internal/model-preference.d.ts +21 -0
  86. package/dist/internal/model-preference.js +20 -0
  87. package/dist/internal/observation-kernel.d.ts +28 -0
  88. package/dist/internal/observation-kernel.js +169 -0
  89. package/dist/internal/promise-facade.d.ts +18 -0
  90. package/dist/internal/promise-facade.js +172 -0
  91. package/dist/internal/run-scope.d.ts +27 -0
  92. package/dist/internal/run-scope.js +214 -0
  93. package/dist/internal/runtime-coordinator.d.ts +34 -0
  94. package/dist/internal/runtime-coordinator.js +233 -0
  95. package/dist/internal/runtime-core.d.ts +19 -0
  96. package/dist/internal/runtime-core.js +39 -0
  97. package/dist/internal/runtime-reconciliation.d.ts +18 -0
  98. package/dist/internal/runtime-reconciliation.js +162 -0
  99. package/dist/internal/session-context.d.ts +11 -0
  100. package/dist/internal/session-context.js +67 -0
  101. package/dist/internal/session-status.d.ts +10 -0
  102. package/dist/internal/session-status.js +52 -0
  103. package/dist/internal/woodstock/agent-reply.d.ts +25 -0
  104. package/dist/internal/woodstock/agent-reply.js +67 -0
  105. package/dist/internal/woodstock/client-projection.d.ts +29 -0
  106. package/dist/internal/woodstock/client-projection.js +512 -0
  107. package/dist/internal/woodstock/compaction-activity.d.ts +17 -0
  108. package/dist/internal/woodstock/compaction-activity.js +32 -0
  109. package/dist/internal/woodstock/cursor.d.ts +9 -0
  110. package/dist/internal/woodstock/cursor.js +44 -0
  111. package/dist/internal/woodstock/extension-store.d.ts +72 -0
  112. package/dist/internal/woodstock/extension-store.js +262 -0
  113. package/dist/internal/woodstock/fleet-store.d.ts +32 -0
  114. package/dist/internal/woodstock/fleet-store.js +93 -0
  115. package/dist/internal/woodstock/identities.d.ts +6 -0
  116. package/dist/internal/woodstock/identities.js +11 -0
  117. package/dist/internal/woodstock/index.d.ts +170 -0
  118. package/dist/internal/woodstock/index.js +546 -0
  119. package/dist/internal/woodstock/progress.d.ts +32 -0
  120. package/dist/internal/woodstock/progress.js +89 -0
  121. package/dist/internal/woodstock/run-recovery.d.ts +8 -0
  122. package/dist/internal/woodstock/run-recovery.js +95 -0
  123. package/dist/internal/woodstock/session-attention.d.ts +16 -0
  124. package/dist/internal/woodstock/session-attention.js +88 -0
  125. package/dist/internal/woodstock/session-documents.d.ts +18 -0
  126. package/dist/internal/woodstock/session-documents.js +51 -0
  127. package/dist/internal/woodstock/session-entry.d.ts +478 -0
  128. package/dist/internal/woodstock/session-entry.js +484 -0
  129. package/dist/internal/woodstock/session-grants.d.ts +24 -0
  130. package/dist/internal/woodstock/session-grants.js +84 -0
  131. package/dist/internal/woodstock/session-inbox.d.ts +56 -0
  132. package/dist/internal/woodstock/session-inbox.js +307 -0
  133. package/dist/internal/woodstock/session-store.d.ts +432 -0
  134. package/dist/internal/woodstock/session-store.js +2006 -0
  135. package/dist/internal/woodstock/storage-admission.d.ts +14 -0
  136. package/dist/internal/woodstock/storage-admission.js +44 -0
  137. package/dist/model-preference.d.ts +20 -0
  138. package/dist/model-preference.js +30 -0
  139. package/dist/node.d.ts +64 -0
  140. package/dist/node.js +256 -0
  141. package/dist/remote-client.d.ts +11 -0
  142. package/dist/remote-client.js +298 -0
  143. package/dist/session-attention.d.ts +35 -0
  144. package/dist/session-attention.js +22 -0
  145. package/dist/session-documents.d.ts +28 -0
  146. package/dist/session-documents.js +1 -0
  147. package/dist/testing.d.ts +54 -0
  148. package/dist/testing.js +215 -0
  149. package/docs/api/README.md +26 -0
  150. package/docs/api/agar-extension.md +40 -0
  151. package/docs/api/agent-control.md +151 -0
  152. package/docs/api/client-entry.md +344 -0
  153. package/docs/api/client.md +1112 -0
  154. package/docs/api/cloudflare.md +500 -0
  155. package/docs/api/effect.md +904 -0
  156. package/docs/api/extensions.md +2003 -0
  157. package/docs/api/fleet.md +706 -0
  158. package/docs/api/hosted.md +382 -0
  159. package/docs/api/http.md +111 -0
  160. package/docs/api/index.md +1116 -0
  161. package/docs/api/model-preference.md +151 -0
  162. package/docs/api/node.md +142 -0
  163. package/docs/api/testing.md +250 -0
  164. package/package.json +130 -3
@@ -0,0 +1,1004 @@
1
+ import { AttentionItemSchema, maximumExtensionAttention, } from "../session-attention.js";
2
+ import { encodeDocument } from "./document-codec.js";
3
+ import * as Cause from "effect/Cause";
4
+ import * as Result from "effect/Result";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Exit from "effect/Exit";
7
+ import * as Option from "effect/Option";
8
+ import * as Redacted from "effect/Redacted";
9
+ import * as Schema from "effect/Schema";
10
+ import * as Scope from "effect/Scope";
11
+ import { ShiitakeError } from "../errors.js";
12
+ import { AdmissionError, ExtensionCommandError, ExtensionRead, ExtensionStateError, ExtensionStop, ExtensionTx, } from "../extensions.js";
13
+ import { decodeClientSessionId } from "./client-boundary.js";
14
+ import { AsyncExtensionTransactionError, invokeExtensionHook, invokeSyncHandler, } from "./extension-hook.js";
15
+ import { setSessionAttention, clearSessionAttention, projectSessionAttention, readAttentionSessionPage, replaceAttentionGrants, hasStoredSession, hasPendingSessionSteer, hasSuccessfulToolResult, advanceCursor, declareSessionDocument, readSessionDocument, writeSessionDocument, deleteAllRecords, deleteRecord, deleteTimer, readAllRecords, readCursor, readDueTimers, readRecord, readRecordPage, readStateLedger, readStatus, readTimer, readTimerWake, writeRecord, writeStateLedger, writeStatus, writeTimer, } from "./woodstock/index.js";
16
+ import { JsonValueSchema } from "./json-value.js";
17
+ import { publishOwnerObservation } from "./observation-kernel.js";
18
+ /** The settled consumer name core itself uses for mail and control repair. */
19
+ export const coreConsumer = "core";
20
+ const maximumKeyLength = 256;
21
+ const maximumRecordBytes = 1024 * 1024;
22
+ const maximumReasonLength = 512;
23
+ const maximumPage = 64;
24
+ const encoder = new TextEncoder();
25
+ const decodePromptSections = Schema.decodeUnknownResult(Schema.Array(Schema.Struct({
26
+ name: Schema.String.pipe(Schema.check(Schema.isPattern(/^[a-z][A-Za-z0-9_-]{0,63}$/u))),
27
+ text: Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.makeFilter((text) => encoder.encode(text).byteLength <= 8192))),
28
+ })).pipe(Schema.check(Schema.isMaxLength(16))));
29
+ /** A missing or failed pure migration cannot safely decode the new state. */
30
+ class IncompatibleStateMigrationError extends Error {
31
+ }
32
+ function boundReason(reason) {
33
+ return reason.length <= maximumReasonLength
34
+ ? reason
35
+ : `${reason.slice(0, maximumReasonLength - 1)}…`;
36
+ }
37
+ const decodeStoredJson = Schema.decodeSync(Schema.fromJsonString(JsonValueSchema));
38
+ function failureText(value) {
39
+ return value instanceof ExtensionStop
40
+ ? value.reason
41
+ : `${value._tag}: ${value.message}`;
42
+ }
43
+ function isExtensionFailure(value) {
44
+ return (value instanceof ExtensionStop ||
45
+ value instanceof ExtensionStateError ||
46
+ value instanceof AdmissionError ||
47
+ value instanceof ExtensionCommandError);
48
+ }
49
+ /** A bounded, caller-safe reason for one stopping cause. */
50
+ function stopReason(hook, cause) {
51
+ const failure = Cause.findErrorOption(cause);
52
+ if (Option.isSome(failure)) {
53
+ const value = failure.value;
54
+ if (value instanceof ExtensionStop)
55
+ return boundReason(value.reason);
56
+ return boundReason(isExtensionFailure(value)
57
+ ? `${hook}: ${failureText(value)}`
58
+ : `${hook} failed`);
59
+ }
60
+ const defect = Result.getSuccess(Cause.findDefect(cause));
61
+ if (Option.isSome(defect)) {
62
+ if (defect.value instanceof Error)
63
+ return boundReason(defect.value.name === "AsyncFiberError"
64
+ ? `${hook} crossed an asynchronous boundary inside a core transaction`
65
+ : `${hook} defect: ${defect.value.message}`);
66
+ return boundReason(`${hook} defect`);
67
+ }
68
+ return boundReason(`${hook} was interrupted`);
69
+ }
70
+ function shouldStop(cause) {
71
+ const failure = Cause.findErrorOption(cause);
72
+ return Option.isNone(failure) || failure.value instanceof ExtensionStop;
73
+ }
74
+ function stateError(reason, message) {
75
+ return new ExtensionStateError({ reason, message });
76
+ }
77
+ function boundedKey(key, what) {
78
+ return key.length >= 1 && key.length <= maximumKeyLength
79
+ ? Effect.succeed(key)
80
+ : Effect.fail(stateError("bound", `${what} must be 1..256 characters`));
81
+ }
82
+ /** Map one typed extension command failure to Shiitake's public error. */
83
+ function commandFailure(failure) {
84
+ const message = boundReason(failure.message);
85
+ if (failure instanceof ExtensionCommandError)
86
+ switch (failure.reason) {
87
+ case "invalid":
88
+ return new ShiitakeError("contract", "invalid", message);
89
+ case "conflict":
90
+ return new ShiitakeError("store", "conflict", message);
91
+ case "not_found":
92
+ return new ShiitakeError("store", "rejected", message);
93
+ case "rejected":
94
+ return new ShiitakeError("extensions", "rejected", message);
95
+ }
96
+ if (failure instanceof AdmissionError)
97
+ switch (failure.reason) {
98
+ case "invalid":
99
+ return new ShiitakeError("contract", "invalid", message);
100
+ case "conflict":
101
+ return new ShiitakeError("contract", "conflict", message);
102
+ case "closed":
103
+ return new ShiitakeError("runtime", "closed", message);
104
+ }
105
+ return failure.reason === "bound"
106
+ ? new ShiitakeError("contract", "invalid", message)
107
+ : new ShiitakeError("store", "unavailable", message);
108
+ }
109
+ /** Activate the admitted roster and bind it to core's doors and tables. */
110
+ export function openExtensionRuntime(input) {
111
+ const { owner } = input;
112
+ const frames = new WeakMap();
113
+ const members = new Map();
114
+ const incompatibleMigrations = new Set();
115
+ let projecting;
116
+ const updating = new Set();
117
+ const transacting = new Set();
118
+ let documentFrame;
119
+ const readServiceFor = (id) => Object.freeze({ extensionId: id });
120
+ const requireRead = (id) => Effect.flatMap(ExtensionRead, (service) => service.extensionId === id
121
+ ? Effect.void
122
+ : Effect.die(new Error("Extension read used another extension's context")));
123
+ const requireFrame = (id) => Effect.flatMap(ExtensionTx, (service) => {
124
+ const frame = frames.get(service);
125
+ return frame?.extensionId === id
126
+ ? Effect.succeed(frame)
127
+ : Effect.die(new Error("Extension write used outside its own core transaction"));
128
+ });
129
+ const storeStep = (run) => Effect.try({
130
+ try: run,
131
+ catch: () => stateError("unavailable", "Extension state is unavailable"),
132
+ });
133
+ const grantGuard = (member, capability) => member.admitted.grants.has(capability)
134
+ ? Effect.void
135
+ : Effect.fail(stateError("unavailable", `Extension is not granted ${capability}`));
136
+ function collectionHandle(member, name, spec) {
137
+ const id = member.admitted.id;
138
+ const decode = Schema.decodeResult(Schema.fromJsonString(spec.schema));
139
+ const encode = Schema.encodeResult(spec.schema);
140
+ const decodeJson = (json) => {
141
+ const decoded = decode(json);
142
+ return decoded._tag === "Failure"
143
+ ? Effect.fail(stateError("decode", `Stored ${name} record is invalid`))
144
+ : Effect.succeed(decoded.success);
145
+ };
146
+ return Object.freeze({
147
+ peek: (key) => {
148
+ if (projecting !== id || !member.admitted.grants.has("state"))
149
+ throw new Error("Extension read used outside its own projection");
150
+ const json = readRecord(owner, id, name, key);
151
+ if (json === undefined)
152
+ return Option.none();
153
+ const decoded = decode(json);
154
+ if (decoded._tag === "Failure")
155
+ throw stateError("decode", `Stored ${name} record is invalid`);
156
+ return Option.some(decoded.success);
157
+ },
158
+ peekPage: (query) => {
159
+ if (projecting !== id || !member.admitted.grants.has("state"))
160
+ throw new Error("Extension read used outside its own projection");
161
+ if (!Number.isSafeInteger(query.limit) ||
162
+ query.limit < 1 ||
163
+ query.limit > maximumPage)
164
+ throw stateError("bound", "Page limit must be 1..64");
165
+ return readRecordPage(owner, id, name, query).map((json) => {
166
+ const decoded = decode(json);
167
+ if (decoded._tag === "Failure")
168
+ throw stateError("decode", `Stored ${name} record is invalid`);
169
+ return decoded.success;
170
+ });
171
+ },
172
+ get: (key) => requireRead(id).pipe(Effect.andThen(grantGuard(member, "state")), Effect.andThen(storeStep(() => readRecord(owner, id, name, key))), Effect.flatMap((json) => json === undefined
173
+ ? Effect.succeed(Option.none())
174
+ : decodeJson(json).pipe(Effect.map(Option.some)))),
175
+ page: (query) => requireRead(id).pipe(Effect.andThen(grantGuard(member, "state")), Effect.andThen(Number.isSafeInteger(query.limit) &&
176
+ query.limit >= 1 &&
177
+ query.limit <= maximumPage
178
+ ? Effect.void
179
+ : Effect.fail(stateError("bound", "Page limit must be 1..64"))), Effect.andThen(storeStep(() => readRecordPage(owner, id, name, query))), Effect.flatMap((rows) => Effect.forEach(rows, decodeJson))),
180
+ put: (value) => requireFrame(id).pipe(Effect.andThen(grantGuard(member, "state")), Effect.andThen(Effect.suspend(() => {
181
+ const encoded = encode(value);
182
+ if (encoded._tag === "Failure")
183
+ return Effect.fail(stateError("encode", `The ${name} record does not match its schema`));
184
+ const json = JSON.stringify(encoded.success);
185
+ if (encoder.encode(json).byteLength > maximumRecordBytes)
186
+ return Effect.fail(stateError("bound", `The ${name} record exceeds 1 MiB`));
187
+ const index = spec.index?.(value) ?? null;
188
+ return Effect.all([
189
+ boundedKey(spec.key(value), "A record key"),
190
+ index === null
191
+ ? Effect.succeed(null)
192
+ : boundedKey(index, "A record index"),
193
+ ]).pipe(Effect.flatMap(([key, boundedIndex]) => storeStep(() => {
194
+ writeRecord(owner, id, name, key, boundedIndex, json);
195
+ })));
196
+ }))),
197
+ remove: (key) => requireFrame(id).pipe(Effect.andThen(grantGuard(member, "state")), Effect.andThen(storeStep(() => {
198
+ deleteRecord(owner, id, name, key);
199
+ }))),
200
+ });
201
+ }
202
+ /** Synchronous collection operations exist only inside state.update. */
203
+ function updateCollection(member, name, spec) {
204
+ const id = member.admitted.id;
205
+ const decode = Schema.decodeResult(Schema.fromJsonString(spec.schema));
206
+ const encode = Schema.encodeResult(spec.schema);
207
+ const guard = () => {
208
+ if (!updating.has(id) || !member.admitted.grants.has("state"))
209
+ throw stateError("unavailable", "Extension state update is unavailable");
210
+ };
211
+ const key = (value) => {
212
+ if (value.length < 1 || value.length > maximumKeyLength)
213
+ throw stateError("bound", "A record key must be 1..256 characters");
214
+ return value;
215
+ };
216
+ const decoded = (json) => {
217
+ const result = decode(json);
218
+ if (result._tag === "Failure")
219
+ throw stateError("decode", `Stored ${name} record is invalid`);
220
+ return result.success;
221
+ };
222
+ return Object.freeze({
223
+ get: (recordKey) => {
224
+ guard();
225
+ const json = readRecord(owner, id, name, key(recordKey));
226
+ return json === undefined ? Option.none() : Option.some(decoded(json));
227
+ },
228
+ page: (query) => {
229
+ guard();
230
+ if (!Number.isSafeInteger(query.limit) ||
231
+ query.limit < 1 ||
232
+ query.limit > maximumPage)
233
+ throw stateError("bound", "Page limit must be 1..64");
234
+ return readRecordPage(owner, id, name, query).map(decoded);
235
+ },
236
+ put: (value) => {
237
+ guard();
238
+ const result = encode(value);
239
+ if (result._tag === "Failure")
240
+ throw stateError("encode", `The ${name} record does not match its schema`);
241
+ const json = JSON.stringify(result.success);
242
+ if (encoder.encode(json).byteLength > maximumRecordBytes)
243
+ throw stateError("bound", `The ${name} record exceeds 1 MiB`);
244
+ const index = spec.index?.(value) ?? null;
245
+ if (index !== null)
246
+ key(index);
247
+ writeRecord(owner, id, name, key(spec.key(value)), index, json);
248
+ },
249
+ remove: (recordKey) => {
250
+ guard();
251
+ deleteRecord(owner, id, name, key(recordKey));
252
+ },
253
+ });
254
+ }
255
+ function timersFor(member) {
256
+ const id = member.admitted.id;
257
+ return Object.freeze({
258
+ set: (key, dueAt) => requireFrame(id).pipe(Effect.tap(() => grantGuard(member, "timers")), Effect.tap(() => boundedKey(key, "A timer key")), Effect.tap(() => Number.isSafeInteger(dueAt) && dueAt >= 0
259
+ ? Effect.void
260
+ : Effect.fail(stateError("bound", "A timer instant is invalid"))), Effect.flatMap((frame) => storeStep(() => {
261
+ writeTimer(owner, id, key, dueAt);
262
+ frame.timersChanged = true;
263
+ }))),
264
+ cancel: (key) => requireFrame(id).pipe(Effect.tap(() => grantGuard(member, "timers")), Effect.flatMap((frame) => storeStep(() => {
265
+ deleteTimer(owner, id, key);
266
+ frame.timersChanged = true;
267
+ }))),
268
+ read: (key) => requireRead(id).pipe(Effect.andThen(grantGuard(member, "timers")), Effect.andThen(storeStep(() => readTimer(owner, id, key))), Effect.map(Option.fromNullishOr)),
269
+ });
270
+ }
271
+ function attentionFor(member) {
272
+ const id = member.admitted.id;
273
+ const mutate = (sessionId, write) => requireFrame(id).pipe(Effect.tap(() => grantGuard(member, "attention")), Effect.flatMap((frame) => Effect.try({
274
+ try: () => {
275
+ decodeClientSessionId(sessionId);
276
+ write();
277
+ projectSessionAttention(owner, sessionId, input.readQueueHead(sessionId));
278
+ frame.observed.add(sessionId);
279
+ },
280
+ catch: (error) => error instanceof ShiitakeError && error.cause === "invalid"
281
+ ? stateError("bound", error.message)
282
+ : stateError("unavailable", "Attention update is unavailable"),
283
+ })));
284
+ return {
285
+ set: (sessionId, item) => {
286
+ const decoded = Schema.decodeUnknownResult(AttentionItemSchema, {
287
+ onExcessProperty: "error",
288
+ })(item);
289
+ return decoded._tag === "Failure"
290
+ ? Effect.fail(stateError("bound", "Attention item is invalid"))
291
+ : mutate(sessionId, () => {
292
+ if (!hasStoredSession(owner, sessionId))
293
+ throw new Error("Attention Session is unavailable");
294
+ setSessionAttention(owner, sessionId, id, decoded.success);
295
+ });
296
+ },
297
+ clear: (sessionId, itemId) => boundedKey(itemId, "An attention id").pipe(Effect.andThen(mutate(sessionId, () => {
298
+ clearSessionAttention(owner, sessionId, id, itemId);
299
+ }))),
300
+ };
301
+ }
302
+ function sendFor(member) {
303
+ const id = member.admitted.id;
304
+ return (request) => requireFrame(id).pipe(Effect.flatMap((frame) => member.admitted.grants.has("send")
305
+ ? Effect.try({
306
+ try: () => {
307
+ const { requireExistingSession = false } = request;
308
+ const result = frame.admit({
309
+ sessionId: request.sessionId,
310
+ requireExistingSession,
311
+ command: {
312
+ type: "prompt",
313
+ requestId: request.prompt.requestId,
314
+ delivery: request.prompt.delivery,
315
+ text: request.prompt.text,
316
+ },
317
+ when: request.when,
318
+ origin: {
319
+ kind: "extension",
320
+ extensionId: id,
321
+ correlation: request.correlation ?? null,
322
+ },
323
+ });
324
+ return result.status === "accepted"
325
+ ? {
326
+ _tag: "Accepted",
327
+ commandId: result.commandId,
328
+ delivery: result.delivery,
329
+ }
330
+ : { _tag: "NotReady" };
331
+ },
332
+ catch: (error) => new AdmissionError({
333
+ reason: error instanceof ShiitakeError
334
+ ? error.cause === "conflict"
335
+ ? "conflict"
336
+ : error.cause === "closed"
337
+ ? "closed"
338
+ : "invalid"
339
+ : "invalid",
340
+ message: error instanceof ShiitakeError
341
+ ? error.message
342
+ : "Session command is invalid",
343
+ }),
344
+ })
345
+ : Effect.fail(new AdmissionError({
346
+ reason: "closed",
347
+ message: "Extension is not granted send",
348
+ }))));
349
+ }
350
+ /**
351
+ * Run one extension body in its own owner transaction. A failed body rolls
352
+ * back only this transaction; its Exit is returned for the caller's policy.
353
+ */
354
+ function runTransaction(member, body, boundary) {
355
+ return Effect.suspend(() => {
356
+ let bodyExit;
357
+ let frame;
358
+ const rollback = new Error("Extension transaction rolled back");
359
+ return input.door
360
+ .transact((admit) => {
361
+ const service = readServiceFor(member.admitted.id);
362
+ frame = {
363
+ extensionId: member.admitted.id,
364
+ admit,
365
+ observed: new Set(),
366
+ timersChanged: false,
367
+ };
368
+ frames.set(service, frame);
369
+ transacting.add(member.admitted.id);
370
+ const previousDocumentFrame = documentFrame;
371
+ documentFrame = frame;
372
+ try {
373
+ bodyExit = Effect.runSyncExit(body.pipe(Effect.provideService(ExtensionTx, service), Effect.provideService(ExtensionRead, service)));
374
+ }
375
+ finally {
376
+ documentFrame = previousDocumentFrame;
377
+ frames.delete(service);
378
+ transacting.delete(member.admitted.id);
379
+ }
380
+ if (Exit.isFailure(bodyExit))
381
+ throw rollback;
382
+ return bodyExit.value;
383
+ }, boundary)
384
+ .pipe(Effect.map(() => {
385
+ if (bodyExit === undefined)
386
+ throw new Error("Extension transaction returned no exit");
387
+ return bodyExit;
388
+ }), Effect.catch((error) => bodyExit !== undefined && Exit.isFailure(bodyExit)
389
+ ? Effect.succeed(bodyExit)
390
+ : Effect.fail(error)), Effect.tap((exit) => Exit.isSuccess(exit) && frame !== undefined
391
+ ? Effect.gen(function* () {
392
+ for (const sessionId of frame?.observed ?? [])
393
+ publishOwnerObservation(owner, sessionId);
394
+ if (boundary === "request" && frame?.timersChanged === true)
395
+ yield* input.requestWake;
396
+ })
397
+ : Effect.void));
398
+ });
399
+ }
400
+ /** Refresh at most the extension's outstanding resource ceiling, in bounded pages. */
401
+ function refreshAttentionOwner(extensionId) {
402
+ let after = "";
403
+ for (let count = 0; count < maximumExtensionAttention; count += maximumPage) {
404
+ const sessions = readAttentionSessionPage(owner, extensionId, after);
405
+ for (const sessionId of sessions) {
406
+ projectSessionAttention(owner, sessionId, input.readQueueHead(sessionId));
407
+ // Only a latency hint: readers re-read after the synchronous transaction.
408
+ queueMicrotask(() => {
409
+ publishOwnerObservation(owner, sessionId);
410
+ });
411
+ }
412
+ const last = sessions.at(-1);
413
+ if (sessions.length < maximumPage || last === undefined)
414
+ return;
415
+ after = last;
416
+ }
417
+ }
418
+ /**
419
+ * Record a stop and log the full cause. The in-memory stop applies
420
+ * immediately either way, so the extension stays isolated until the next
421
+ * open even when nothing durable landed. A failed durable write is never
422
+ * swallowed: it fails this Effect with a `store`/`unavailable`
423
+ * `ShiitakeError` so the operation that triggered the stop learns the
424
+ * record did not actually land.
425
+ */
426
+ function recordStop(member, reason, cause, within) {
427
+ const stoppedAt = Date.now();
428
+ member.status = "stopped";
429
+ member.reason = reason;
430
+ member.stoppedAt = stoppedAt;
431
+ const write = Effect.try({
432
+ try: () => {
433
+ const run = () => {
434
+ writeStatus(owner, member.admitted.id, {
435
+ status: "stopped",
436
+ reason,
437
+ stoppedAt,
438
+ revision: member.admitted.revision,
439
+ });
440
+ refreshAttentionOwner(member.admitted.id);
441
+ };
442
+ if (within === "own-transaction")
443
+ owner.transactionSync(run);
444
+ else
445
+ run();
446
+ },
447
+ catch: (error) => new ShiitakeError("store", "unavailable", `Extension ${member.admitted.id} stop status is unavailable`, undefined, error),
448
+ });
449
+ return Effect.gen(function* () {
450
+ const exit = yield* Effect.exit(write);
451
+ yield* Effect.logError("shiitake_extension_stopped").pipe(Effect.annotateLogs({
452
+ extensionId: member.admitted.id,
453
+ reason,
454
+ cause: cause === undefined ? null : Redacted.make(cause),
455
+ durable: Exit.isSuccess(exit),
456
+ }));
457
+ return yield* exit;
458
+ });
459
+ }
460
+ /** Persist a synchronous stop, in or outside an enclosing owner transaction. */
461
+ function recordStopSynchronously(member, reason, within) {
462
+ member.status = "stopped";
463
+ member.reason = reason;
464
+ member.stoppedAt = Date.now();
465
+ const run = () => {
466
+ writeStatus(owner, member.admitted.id, {
467
+ status: "stopped",
468
+ reason,
469
+ stoppedAt: member.stoppedAt,
470
+ revision: member.admitted.revision,
471
+ });
472
+ refreshAttentionOwner(member.admitted.id);
473
+ };
474
+ try {
475
+ if (within === "own-transaction")
476
+ owner.transactionSync(run);
477
+ else
478
+ run();
479
+ }
480
+ catch (error) {
481
+ throw new ShiitakeError("store", "unavailable", `Extension ${member.admitted.id} stop status is unavailable`, undefined, error);
482
+ }
483
+ }
484
+ function documentHandle(member, name, spec) {
485
+ const check = (sessionId) => {
486
+ if (documentFrame?.extensionId !== member.admitted.id ||
487
+ !member.admitted.grants.has("state"))
488
+ throw stateError("unavailable", "Session documents require the extension's state transaction");
489
+ decodeClientSessionId(sessionId);
490
+ return documentFrame;
491
+ };
492
+ const read = (sessionId) => {
493
+ check(sessionId);
494
+ const json = readSessionDocument(owner, sessionId, member.admitted.id, name) ??
495
+ encodeDocument(spec, spec.initial);
496
+ if (encoder.encode(json).byteLength > spec.maxBytes)
497
+ throw stateError("bound", "Session document exceeds maxBytes");
498
+ const decoded = Schema.decodeUnknownResult(Schema.fromJsonString(spec.schema))(json);
499
+ if (decoded._tag === "Failure")
500
+ throw stateError("decode", "Stored session document does not match its schema");
501
+ const value = decoded.success;
502
+ return value;
503
+ };
504
+ const handle = {
505
+ read,
506
+ update: (sessionId, change) => {
507
+ const frame = check(sessionId);
508
+ const value = change(read(sessionId));
509
+ writeSessionDocument(owner, sessionId, member.admitted.id, name, encodeDocument(spec, value));
510
+ frame.observed.add(sessionId);
511
+ },
512
+ };
513
+ return Object.freeze(handle);
514
+ }
515
+ function contextFor(member) {
516
+ const observed = new Set();
517
+ const update = (body) => {
518
+ const id = member.admitted.id;
519
+ if (member.status !== "running" ||
520
+ !member.admitted.grants.has("state") ||
521
+ updating.has(id) ||
522
+ transacting.has(id) ||
523
+ projecting === id)
524
+ throw stateError("unavailable", "Extension state update is unavailable");
525
+ try {
526
+ const result = owner.transactionSync(() => {
527
+ updating.add(id);
528
+ try {
529
+ return invokeSyncHandler(body, undefined);
530
+ }
531
+ finally {
532
+ updating.delete(id);
533
+ }
534
+ });
535
+ for (const sessionId of observed)
536
+ publishOwnerObservation(owner, sessionId);
537
+ return result;
538
+ }
539
+ catch (error) {
540
+ if (!(error instanceof AsyncExtensionTransactionError))
541
+ throw error;
542
+ const reason = error.message;
543
+ recordStopSynchronously(member, reason, "own-transaction");
544
+ throw new ExtensionStop({ reason });
545
+ }
546
+ finally {
547
+ observed.clear();
548
+ }
549
+ };
550
+ return Object.freeze({
551
+ collection: (name, spec) => collectionHandle(member, name, spec),
552
+ updateCollection: (name, spec) => updateCollection(member, name, spec),
553
+ document: (name, spec) => documentHandle(member, name, spec),
554
+ update,
555
+ observeUpdate: (sessionId) => {
556
+ if (!updating.has(member.admitted.id))
557
+ throw stateError("unavailable", "Extension observation is unavailable");
558
+ observed.add(decodeClientSessionId(sessionId));
559
+ },
560
+ context: Object.freeze({
561
+ manifest: member.admitted.manifest,
562
+ revision: member.admitted.revision,
563
+ grants: Object.freeze([...member.admitted.grants]),
564
+ send: sendFor(member),
565
+ timers: timersFor(member),
566
+ attention: attentionFor(member),
567
+ hasPendingSteer: (run) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasPendingSessionSteer(owner, decodeClientSessionId(run.sessionId), run.workId)))),
568
+ hasSuccessfulToolResult: (run, toolCallId) => requireRead(member.admitted.id).pipe(Effect.andThen(grantGuard(member, "run_tools")), Effect.andThen(storeStep(() => hasSuccessfulToolResult(owner, decodeClientSessionId(run.sessionId), run.workId, toolCallId)))),
569
+ observe: (sessionId) => requireFrame(member.admitted.id).pipe(Effect.map((frame) => {
570
+ frame.observed.add(sessionId);
571
+ })),
572
+ transact: (body) => {
573
+ const run = member.status === "running"
574
+ ? runTransaction(member, body, "request").pipe(Effect.orDie)
575
+ : Effect.fail(new ExtensionStop({
576
+ reason: member.reason ?? "Extension is stopped",
577
+ }));
578
+ return run.pipe(Effect.flatMap((exit) => {
579
+ if (Exit.isSuccess(exit))
580
+ return Effect.succeed(exit.value);
581
+ if (!shouldStop(exit.cause))
582
+ return Effect.failCause(exit.cause);
583
+ const reason = stopReason("transact", exit.cause);
584
+ return recordStop(member, reason, exit.cause, "own-transaction").pipe(Effect.flatMap(() => Effect.fail(new ExtensionStop({ reason }))));
585
+ }));
586
+ },
587
+ }),
588
+ });
589
+ }
590
+ /** Pure per-version migrations, applied and re-decoded in one transaction. */
591
+ function migrate(member) {
592
+ const { id, definition, revision } = member.admitted;
593
+ const declared = definition.state.version;
594
+ owner.transactionSync(() => {
595
+ const ledger = readStateLedger(owner, id);
596
+ if (ledger !== undefined && ledger.version > declared)
597
+ throw new ShiitakeError("extensions", "conflict", `Stored ${id} state version ${String(ledger.version)} is newer than ${String(declared)}`);
598
+ if (ledger !== undefined && ledger.version < declared) {
599
+ let rows = readAllRecords(owner, id).map((row) => ({
600
+ collection: row.collection,
601
+ key: row.key,
602
+ value: decodeStoredJson(row.valueJson),
603
+ }));
604
+ for (let version = ledger.version; version < declared; version += 1) {
605
+ const step = definition.state.migrations[version];
606
+ if (step === undefined)
607
+ throw new IncompatibleStateMigrationError(`Missing ${id} state migration from ${String(version)}`);
608
+ try {
609
+ rows = rows.flatMap((row) => step(row));
610
+ }
611
+ catch (cause) {
612
+ throw new IncompatibleStateMigrationError(`Failed ${id} state migration from ${String(version)}`, { cause });
613
+ }
614
+ }
615
+ deleteAllRecords(owner, id);
616
+ for (const row of rows) {
617
+ const declaredSpec = definition.state.collections[row.collection];
618
+ if (declaredSpec === undefined)
619
+ throw new IncompatibleStateMigrationError(`Migrated ${id} record names an undeclared collection`);
620
+ const decoded = declaredSpec.indexFromEncoded(row.value);
621
+ if (decoded._tag === "Failure")
622
+ throw new IncompatibleStateMigrationError(`Migrated ${id} ${row.collection} record is invalid`);
623
+ writeRecord(owner, id, row.collection, row.key, decoded.success, JSON.stringify(row.value));
624
+ }
625
+ }
626
+ writeStateLedger(owner, id, declared, revision);
627
+ });
628
+ }
629
+ function activateMember(member, scope) {
630
+ const context = contextFor(member);
631
+ return Effect.suspend(() => member.admitted.definition.activate(context)).pipe(Effect.provideService(Scope.Scope, scope), Effect.exit, Effect.flatMap((exit) => {
632
+ if (Exit.isSuccess(exit)) {
633
+ member.hooks = exit.value;
634
+ return Effect.void;
635
+ }
636
+ return recordStop(member, stopReason("activation", exit.cause), exit.cause, "own-transaction");
637
+ }));
638
+ }
639
+ const running = (capability) => [...members.values()].filter((member) => member.status === "running" &&
640
+ member.hooks !== undefined &&
641
+ member.admitted.grants.has(capability));
642
+ function statusOf(member) {
643
+ return Object.freeze({
644
+ id: member.admitted.id,
645
+ version: member.admitted.manifest.version,
646
+ revision: member.admitted.revision,
647
+ status: member.status,
648
+ reason: member.reason,
649
+ stoppedAt: member.stoppedAt,
650
+ });
651
+ }
652
+ function isRunParticipant(member) {
653
+ return (member.status === "running" &&
654
+ member.hooks !== undefined &&
655
+ (member.admitted.grants.has("run_tools") ||
656
+ member.admitted.grants.has("files") ||
657
+ member.admitted.grants.has("prompt") ||
658
+ member.admitted.grants.has("compaction_handoff")));
659
+ }
660
+ /** Run a read-only network or tool hook outside the write transaction. */
661
+ function runRead(member, hook, effect) {
662
+ return Effect.exit(effect.pipe(Effect.provideService(ExtensionRead, readServiceFor(member.admitted.id)))).pipe(Effect.flatMap((exit) => Exit.isSuccess(exit)
663
+ ? Effect.succeed(Option.some(exit.value))
664
+ : recordStop(member, stopReason(hook, exit.cause), exit.cause, "own-transaction").pipe(Effect.as(Option.none()))));
665
+ }
666
+ const stopFromExit = (member, hook, exit) => Exit.isFailure(exit)
667
+ ? recordStop(member, stopReason(hook, exit.cause), exit.cause, "own-transaction")
668
+ : Effect.void;
669
+ /**
670
+ * Run pending state migrations. A stored version newer than the declared one
671
+ * fails construction; any other failure stops only this extension.
672
+ */
673
+ function prepare(member) {
674
+ return Effect.try({
675
+ try: () => {
676
+ migrate(member);
677
+ owner.transactionSync(() => {
678
+ for (const [name, spec] of Object.entries(member.admitted.documents))
679
+ declareSessionDocument(owner, {
680
+ extensionId: member.admitted.id,
681
+ name,
682
+ initialJson: encodeDocument(spec, spec.initial),
683
+ fork: spec.fork,
684
+ maxBytes: spec.maxBytes,
685
+ });
686
+ });
687
+ },
688
+ catch: (error) => error,
689
+ }).pipe(Effect.as(true), Effect.catch((error) => {
690
+ if (error instanceof ShiitakeError)
691
+ return Effect.fail(error);
692
+ if (error instanceof IncompatibleStateMigrationError &&
693
+ (member.admitted.grants.has("run_tools") ||
694
+ member.admitted.grants.has("files") ||
695
+ member.admitted.grants.has("prompt") ||
696
+ member.admitted.grants.has("compaction_handoff")))
697
+ incompatibleMigrations.add(member.admitted.id);
698
+ return recordStop(member, boundReason(error instanceof Error
699
+ ? `migration: ${error.message}`
700
+ : "migration failed"), Cause.die(error), "own-transaction").pipe(Effect.as(false));
701
+ }));
702
+ }
703
+ return Effect.gen(function* () {
704
+ const scope = yield* Scope.make();
705
+ for (const admitted of input.admitted) {
706
+ const stored = readStatus(owner, admitted.id);
707
+ const keepStopped = stored?.status === "stopped" && stored.revision === admitted.revision;
708
+ const member = {
709
+ admitted,
710
+ hooks: undefined,
711
+ status: keepStopped ? "stopped" : "running",
712
+ reason: keepStopped ? stored.reason : null,
713
+ stoppedAt: keepStopped ? stored.stoppedAt : null,
714
+ };
715
+ members.set(admitted.id, member);
716
+ // Every settled consumer has a cursor from its first roster on, so a
717
+ // later resume replays only what it missed.
718
+ yield* Effect.try({
719
+ try: () => {
720
+ if (admitted.grants.has("settled"))
721
+ owner.transactionSync(() => {
722
+ if (readCursor(owner, admitted.id) === undefined)
723
+ advanceCursor(owner, admitted.id, readCursor(owner, coreConsumer) ?? 0);
724
+ });
725
+ },
726
+ catch: () => new ShiitakeError("store", "unavailable", "Settled cursor is unavailable"),
727
+ });
728
+ const prepared = yield* prepare(member).pipe(Effect.onError(() => Scope.close(scope, Exit.void)));
729
+ if (!prepared)
730
+ continue;
731
+ if (keepStopped)
732
+ continue;
733
+ yield* Effect.try({
734
+ try: () => {
735
+ owner.transactionSync(() => {
736
+ writeStatus(owner, admitted.id, {
737
+ status: "running",
738
+ reason: null,
739
+ stoppedAt: null,
740
+ revision: admitted.revision,
741
+ });
742
+ refreshAttentionOwner(admitted.id);
743
+ });
744
+ },
745
+ catch: () => new ShiitakeError("store", "unavailable", "Extension status is unavailable"),
746
+ });
747
+ yield* activateMember(member, scope);
748
+ }
749
+ yield* Effect.try({
750
+ try: () => {
751
+ owner.transactionSync(() => {
752
+ const changed = replaceAttentionGrants(owner, input.admitted
753
+ .filter((member) => member.grants.has("attention"))
754
+ .map((member) => member.id));
755
+ for (const id of changed)
756
+ refreshAttentionOwner(id);
757
+ });
758
+ },
759
+ catch: () => new ShiitakeError("store", "unavailable", "Attention grants are unavailable"),
760
+ });
761
+ const runtime = {
762
+ runParticipants: () => [...members.values()]
763
+ .filter(isRunParticipant)
764
+ .map((member) => [member.admitted.id, member.admitted.fingerprint]),
765
+ incompatibleMigrations: () => [...incompatibleMigrations],
766
+ modulesForRun: (run, captured) => Effect.forEach(running("run_tools"), (member) => {
767
+ const hook = member.hooks?.moduleForRun;
768
+ return !captured.has(member.admitted.id) || hook === undefined
769
+ ? Effect.succeed(Option.none())
770
+ : runRead(member, "moduleForRun", Effect.suspend(() => hook(run))).pipe(Effect.map(Option.flatten));
771
+ }).pipe(Effect.map((modules) => modules.flatMap(Option.toArray))),
772
+ filesForRun: (run, captured) => Effect.gen(function* () {
773
+ const member = running("files").find((entry) => captured.has(entry.admitted.id));
774
+ if (member === undefined)
775
+ return [];
776
+ const hook = member.hooks?.filesForRun;
777
+ if (hook === undefined) {
778
+ yield* recordStop(member, "files: granted capability has no handler", undefined, "own-transaction");
779
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", "Extension Files are unavailable"));
780
+ }
781
+ const result = yield* runRead(member, "filesForRun", Effect.suspend(() => hook(run)));
782
+ if (Option.isNone(result))
783
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", "Extension Files capture failed"));
784
+ return Object.freeze([...Option.getOrElse(result.value, () => [])]);
785
+ }),
786
+ promptSections: (run, captured) => Effect.gen(function* () {
787
+ const sections = [];
788
+ let totalBytes = 0;
789
+ for (const member of running("prompt")) {
790
+ if (!captured.has(member.admitted.id))
791
+ continue;
792
+ const hook = member.hooks?.prompt;
793
+ if (hook === undefined) {
794
+ yield* recordStop(member, "prompt: granted capability has no handler", undefined, "own-transaction");
795
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", "Extension prompt is unavailable"));
796
+ }
797
+ const result = yield* Effect.exit(invokeExtensionHook(() => hook(run)).pipe(Effect.provideService(ExtensionRead, readServiceFor(member.admitted.id))));
798
+ if (Exit.isFailure(result)) {
799
+ yield* recordStop(member, stopReason("prompt", result.cause), result.cause, "own-transaction");
800
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", "Extension prompt failed"));
801
+ }
802
+ const decoded = decodePromptSections(result.value);
803
+ const values = decoded._tag === "Success" ? decoded.success : [];
804
+ totalBytes += values.reduce((size, section) => size + encoder.encode(section.text).byteLength, 0);
805
+ if (decoded._tag === "Failure" ||
806
+ new Set(values.map(({ name }) => name)).size !== values.length ||
807
+ totalBytes > 65536) {
808
+ yield* recordStop(member, "prompt: section is invalid or exceeds its bound", undefined, "own-transaction");
809
+ return yield* Effect.fail(new ShiitakeError("extensions", "rejected", "Extension prompt is invalid"));
810
+ }
811
+ sections.push(...[...values]
812
+ .sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
813
+ .map((section) => ({
814
+ name: `${member.admitted.id}.${section.name}`,
815
+ text: section.text,
816
+ })));
817
+ }
818
+ return sections;
819
+ }),
820
+ compactionHandoff: (captured) => running("compaction_handoff").find(({ admitted }) => captured.has(admitted.id))?.hooks?.compactionHandoff,
821
+ settledConsumers: () => running("settled").map((member) => ({
822
+ id: member.admitted.id,
823
+ cursor: readCursor(owner, member.admitted.id) ?? 0,
824
+ })),
825
+ deliverSettled: (runs, throughSequence) => Effect.forEach(running("settled"), (member) => Effect.suspend(() => {
826
+ const cursor = readCursor(owner, member.admitted.id) ?? 0;
827
+ if (throughSequence <= cursor)
828
+ return Effect.void;
829
+ const slice = runs.filter(({ sequence }) => sequence > cursor);
830
+ const hook = member.hooks?.onSettled;
831
+ const body = (hook === undefined || slice.length === 0
832
+ ? Effect.void
833
+ : hook(slice)).pipe(Effect.andThen(Effect.sync(() => {
834
+ advanceCursor(owner, member.admitted.id, throughSequence);
835
+ })));
836
+ return runTransaction(member, body, "reconcile").pipe(Effect.flatMap((exit) => stopFromExit(member, "onSettled", exit)));
837
+ }), { discard: true }),
838
+ fireDueTimers: (now) => Effect.gen(function* () {
839
+ const ids = running("timers").map(({ admitted }) => admitted.id);
840
+ const due = readDueTimers(owner, ids, now, maximumPage);
841
+ for (const timer of due) {
842
+ const member = members.get(timer.extensionId);
843
+ if (member?.status !== "running")
844
+ continue;
845
+ const hook = member.hooks?.onTimer;
846
+ const body = Effect.suspend(() => {
847
+ if (readTimer(owner, timer.extensionId, timer.key) !== timer.dueAt)
848
+ return Effect.void;
849
+ deleteTimer(owner, timer.extensionId, timer.key);
850
+ return hook === undefined
851
+ ? Effect.void
852
+ : hook({ key: timer.key, dueAt: timer.dueAt, firedAt: now });
853
+ });
854
+ const exit = yield* runTransaction(member, body, "reconcile");
855
+ yield* stopFromExit(member, "onTimer", exit);
856
+ }
857
+ return due.length === maximumPage;
858
+ }),
859
+ timerWake: (now) => readTimerWake(owner, running("timers").map(({ admitted }) => admitted.id), now),
860
+ project: (sessionId) => {
861
+ const slots = {};
862
+ for (const member of members.values()) {
863
+ const projects = member.admitted.grants.has("projection");
864
+ let catalog = member.admitted.grants.has("commands")
865
+ ? member.admitted.definition.catalog
866
+ : undefined;
867
+ if (!projects && catalog === undefined)
868
+ continue;
869
+ const revision = member.admitted.revision;
870
+ const projection = member.admitted.definition.contract.projection;
871
+ const hook = member.hooks?.project;
872
+ if (member.status === "running" && member.hooks !== undefined) {
873
+ const value = !projects || hook === undefined || projection === null
874
+ ? Option.some(null)
875
+ : (() => {
876
+ let output = null;
877
+ let failure;
878
+ projecting = member.admitted.id;
879
+ try {
880
+ const projected = invokeSyncHandler(() => hook(sessionId), undefined);
881
+ if (Option.isSome(projected)) {
882
+ const encoded = Schema.encodeResult(projection.schema)(projected.value);
883
+ if (encoded._tag === "Failure")
884
+ throw new Error("Extension projection is invalid");
885
+ const json = Schema.decodeUnknownResult(JsonValueSchema)(encoded.success);
886
+ if (json._tag === "Failure")
887
+ throw new Error("Extension projection is not JSON");
888
+ output = json.success;
889
+ const projectCatalog = member.hooks.projectCatalog;
890
+ if (catalog !== undefined &&
891
+ projectCatalog !== undefined)
892
+ catalog = Schema.decodeUnknownSync(JsonValueSchema)(invokeSyncHandler(() => projectCatalog(json.success), undefined));
893
+ }
894
+ }
895
+ catch (error) {
896
+ failure = error;
897
+ }
898
+ finally {
899
+ projecting = undefined;
900
+ }
901
+ if (failure !== undefined) {
902
+ recordStopSynchronously(member, boundReason(failure instanceof Error
903
+ ? `project defect: ${failure.message}`
904
+ : "project defect"), "enclosing-transaction");
905
+ return Option.none();
906
+ }
907
+ return Option.some(output);
908
+ })();
909
+ if (Option.isSome(value)) {
910
+ slots[member.admitted.id] = Object.freeze(catalog === undefined
911
+ ? { status: "running", revision, value: value.value }
912
+ : {
913
+ status: "running",
914
+ revision,
915
+ value: value.value,
916
+ catalog,
917
+ });
918
+ continue;
919
+ }
920
+ }
921
+ slots[member.admitted.id] = Object.freeze({
922
+ status: "stopped",
923
+ revision,
924
+ reason: member.reason ?? "Extension did not activate",
925
+ });
926
+ }
927
+ return Object.freeze(slots);
928
+ },
929
+ call: (target, extensionId, command, raw) => Effect.gen(function* () {
930
+ const member = members.get(extensionId);
931
+ if (member?.admitted.grants.has("commands") !== true)
932
+ return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is unavailable"));
933
+ if (member.status !== "running" || member.hooks === undefined)
934
+ return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", `Extension ${extensionId} is stopped: ${member.reason ?? "not activated"}`));
935
+ const commands = member.admitted.definition.contract.commands;
936
+ const spec = Object.hasOwn(commands, command)
937
+ ? commands[command]
938
+ : undefined;
939
+ if (spec?.scope !== target.scope)
940
+ return yield* Effect.fail(new ShiitakeError("contract", "invalid", "Extension command is unknown"));
941
+ const sessionId = target.scope === "session"
942
+ ? yield* Effect.try({
943
+ try: () => decodeClientSessionId(target.sessionId),
944
+ catch: () => new ShiitakeError("contract", "invalid", "Session id is invalid"),
945
+ })
946
+ : null;
947
+ const handler = member.hooks.commands.get(command);
948
+ if (handler === undefined) {
949
+ const decoded = Schema.decodeUnknownResult(spec.input, {
950
+ onExcessProperty: "error",
951
+ })(raw);
952
+ if (decoded._tag === "Failure")
953
+ return yield* Effect.fail(new ShiitakeError("contract", "invalid", "Extension command input is invalid"));
954
+ yield* recordStop(member, `command ${command} has no handler`, undefined, "own-transaction");
955
+ return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is stopped"));
956
+ }
957
+ const prepared = handler({ sessionId, input: raw });
958
+ if (prepared._tag === "Failure")
959
+ return yield* Effect.fail(new ShiitakeError("contract", "invalid", "Extension command input is invalid"));
960
+ const body = prepared.success();
961
+ const exit = yield* runTransaction(member, body, "request");
962
+ if (Exit.isSuccess(exit))
963
+ return exit.value;
964
+ const failure = Cause.findErrorOption(exit.cause);
965
+ if (Option.isSome(failure) && !shouldStop(exit.cause))
966
+ return yield* Effect.fail(commandFailure(failure.value));
967
+ const reason = stopReason(`command ${command}`, exit.cause);
968
+ yield* recordStop(member, reason, exit.cause, "own-transaction");
969
+ return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", `Extension ${extensionId} is stopped: ${reason}`));
970
+ }),
971
+ list: () => [...members.values()].map(statusOf),
972
+ resume: (id) => Effect.gen(function* () {
973
+ const member = members.get(id);
974
+ if (member === undefined)
975
+ return yield* Effect.fail(new ShiitakeError("extensions", "unavailable", "Extension is unavailable"));
976
+ if (member.status === "stopped") {
977
+ yield* Effect.try({
978
+ try: () => {
979
+ owner.transactionSync(() => {
980
+ writeStatus(owner, member.admitted.id, {
981
+ status: "running",
982
+ reason: null,
983
+ stoppedAt: null,
984
+ revision: member.admitted.revision,
985
+ });
986
+ refreshAttentionOwner(member.admitted.id);
987
+ });
988
+ },
989
+ catch: () => new ShiitakeError("store", "unavailable", "Extension status is unavailable"),
990
+ });
991
+ member.status = "running";
992
+ member.reason = null;
993
+ member.stoppedAt = null;
994
+ if (member.hooks === undefined && (yield* prepare(member)))
995
+ yield* activateMember(member, scope);
996
+ yield* input.requestWake;
997
+ }
998
+ return statusOf(member);
999
+ }),
1000
+ close: Scope.close(scope, Exit.void),
1001
+ };
1002
+ return Object.freeze(runtime);
1003
+ });
1004
+ }