@fungi.computer/shiitake 0.3.1 → 0.4.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.
- package/README.md +18 -8
- package/dist/agent-control.d.ts +19 -15
- package/dist/agent-control.js +66 -126
- package/dist/client-wire.d.ts +12 -3
- package/dist/client-wire.js +33 -8
- package/dist/client.d.ts +34 -3
- package/dist/client.js +1 -1
- package/dist/cloudflare.d.ts +3 -1
- package/dist/cloudflare.js +2 -2
- package/dist/delegation.d.ts +145 -13
- package/dist/delegation.js +109 -37
- package/dist/effect.d.ts +1 -1
- package/dist/fleet.d.ts +30 -8
- package/dist/fleet.js +142 -38
- package/dist/http-routes.d.ts +84 -12
- package/dist/http-routes.js +15 -1
- package/dist/index.d.ts +2 -2
- package/dist/internal/admission-door.d.ts +2 -0
- package/dist/internal/admission-door.js +2 -0
- package/dist/internal/client-boundary.d.ts +10 -0
- package/dist/internal/client-boundary.js +10 -0
- package/dist/internal/compaction/compaction.js +1 -1
- package/dist/internal/contracts.d.ts +2 -0
- package/dist/internal/coordinator.d.ts +3 -1
- package/dist/internal/coordinator.js +98 -13
- package/dist/internal/effect-client.d.ts +3 -1
- package/dist/internal/effect-client.js +3 -2
- package/dist/internal/effect-contracts.d.ts +6 -2
- package/dist/internal/model-acquisition.js +2 -0
- package/dist/internal/observation-kernel.d.ts +8 -4
- package/dist/internal/observation-kernel.js +44 -9
- package/dist/internal/promise-facade.d.ts +1 -1
- package/dist/internal/promise-facade.js +1 -1
- package/dist/internal/run-scope.js +37 -8
- package/dist/internal/runtime-coordinator.d.ts +5 -0
- package/dist/internal/runtime-coordinator.js +8 -4
- package/dist/internal/runtime-core.d.ts +2 -1
- package/dist/internal/runtime-core.js +6 -1
- package/dist/internal/session-context.d.ts +1 -1
- package/dist/internal/session-context.js +84 -13
- package/dist/internal/woodstock/agent-reply.d.ts +7 -2
- package/dist/internal/woodstock/agent-reply.js +22 -13
- package/dist/internal/woodstock/fleet-store.d.ts +3 -2
- package/dist/internal/woodstock/fleet-store.js +43 -6
- package/dist/internal/woodstock/index.d.ts +2 -0
- package/dist/internal/woodstock/index.js +11 -5
- package/dist/internal/woodstock/run-recovery.js +4 -1
- package/dist/internal/woodstock/session-entry.d.ts +13 -3
- package/dist/internal/woodstock/session-entry.js +1 -0
- package/dist/internal/woodstock/session-store.d.ts +4 -0
- package/dist/internal/woodstock/session-store.js +26 -19
- package/dist/remote-client.js +5 -1
- package/dist/testing.d.ts +5 -2
- package/dist/testing.js +4 -2
- package/docs/api/agent-control.md +18 -12
- package/docs/api/client.md +78 -0
- package/docs/api/delegation.md +72 -55
- package/docs/api/effect.md +5 -0
- package/docs/api/fleet.md +23 -23
- package/docs/api/http.md +2 -1
- package/docs/api/index.md +13 -0
- package/docs/api/testing.md +3 -3
- package/package.json +9 -8
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { nanoid } from "@fungi.computer/cairn";
|
|
2
2
|
import { parseModelPreference } from "./model-preference.js";
|
|
3
|
+
import { acceptedNotice } from "@fungi.computer/bird-dog/effect";
|
|
3
4
|
import * as Effect from "effect/Effect";
|
|
4
5
|
import * as Option from "effect/Option";
|
|
5
6
|
import * as Stream from "effect/Stream";
|
|
6
7
|
import { ShiitakeError } from "../errors.js";
|
|
7
|
-
import { decodeClientReadOptions, decodeClientSessionCommand, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
|
|
8
|
+
import { decodeClientReadOptions, decodeClientSettlementRequest, requireDecoded, decodeClientSessionCommand, decodeClientSessionId, decodeClientSubscribeOptions, } from "./client-boundary.js";
|
|
8
9
|
import { mergeObservationSignals, openObservationKernel, publishOwnerObservation, } from "./observation-kernel.js";
|
|
9
10
|
import { checkHostMeta } from "./host-meta.js";
|
|
10
11
|
import { readSessionActivity } from "./session-status.js";
|
|
11
12
|
import { readSessionContext } from "./session-context.js";
|
|
12
|
-
import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
|
|
13
|
+
import { abortSession, cancelSession, branchSession, recordOperatorLine, forkSession, readSession, readCommandSettlement, readSessionModelPreference, readSessionReplayPage, readSessionInbox, readLatestPendingPrompt, createSession, deleteSession, deliverSessionGrants, readSessionGrants, setSessionModelPreference, setSessionTitle, readAdmissionIdentity, assertAdmissionIdentity, } from "./woodstock/index.js";
|
|
13
14
|
/** Generic Agent-scope extension controls over the runtime's one call door. */
|
|
14
15
|
function agentExtensions(runtime) {
|
|
15
16
|
return Object.freeze({
|
|
@@ -33,7 +34,7 @@ function withContext(snapshot, context) {
|
|
|
33
34
|
}
|
|
34
35
|
/** Read the baseline and its context use for one Session. */
|
|
35
36
|
function readSessionWithContext(options, runtime, sessionId, input) {
|
|
36
|
-
return currentSessionActivity(options, runtime, sessionId).pipe(Effect.flatMap((activity) => readSession(options.store, sessionId, activity, input, () => runtime.extensions.project(sessionId))), Effect.flatMap((snapshot) => readSessionContext(options, sessionId).pipe(Effect.map((context) => withContext(snapshot, context)))));
|
|
37
|
+
return currentSessionActivity(options, runtime, sessionId).pipe(Effect.flatMap((activity) => readSession(options.store, sessionId, activity, input, () => runtime.extensions.project(sessionId))), Effect.flatMap((snapshot) => readSessionContext(options, sessionId, snapshot.cursor, input?.before === undefined).pipe(Effect.map((context) => withContext(snapshot, context)))));
|
|
37
38
|
}
|
|
38
39
|
function sessionOwnerOperation(runtime, sessionId, operation) {
|
|
39
40
|
return runtime.assertOpen.pipe(Effect.andThen(Effect.try({
|
|
@@ -106,6 +107,12 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
|
|
|
106
107
|
if (signal === "reset")
|
|
107
108
|
return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
|
|
108
109
|
return runtime.assertOpen.pipe(Effect.andThen(currentSessionActivity(options, runtime, sessionId)), Effect.flatMap((activity) => {
|
|
110
|
+
registration.setQueued(activity.facts.queue.state === "queued");
|
|
111
|
+
if (signal === "activity" && state.delivery === "live")
|
|
112
|
+
return Effect.succeed([
|
|
113
|
+
[{ kind: "activity", activity: activity.facts }],
|
|
114
|
+
Option.some({ ...state, awaitSignal: true }),
|
|
115
|
+
]);
|
|
109
116
|
if (signal === "progress" && state.delivery === "live") {
|
|
110
117
|
return Effect.succeed([
|
|
111
118
|
[
|
|
@@ -132,6 +139,7 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
|
|
|
132
139
|
return resetFrame(options, runtime, sessionId, includeSandboxFailures).pipe(Effect.map(continueFromReset));
|
|
133
140
|
const latestCursor = page.frames
|
|
134
141
|
.flatMap((frame) => frame.kind === "progress" ||
|
|
142
|
+
frame.kind === "activity" ||
|
|
135
143
|
frame.kind === "tool_output"
|
|
136
144
|
? []
|
|
137
145
|
: [frame.cursor])
|
|
@@ -141,13 +149,18 @@ function subscriptionFrames(options, runtime, registration, sessionId, after, de
|
|
|
141
149
|
delivery: page.hasMore ? state.delivery : "live",
|
|
142
150
|
awaitSignal: !page.hasMore,
|
|
143
151
|
};
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
// Only a final update that moves the cursor gets a
|
|
153
|
+
// figure. A wake that moved nothing never re-reads the
|
|
154
|
+
// whole kept window.
|
|
155
|
+
const movedHistory = page.frames.some((frame) => frame.kind === "update" &&
|
|
156
|
+
frame.cursor !== state.after);
|
|
157
|
+
return (movedHistory && !page.hasMore
|
|
158
|
+
? readSessionContext(options, sessionId, latestCursor ?? null)
|
|
147
159
|
: Effect.succeed(undefined)).pipe(Effect.map((context) => [
|
|
148
160
|
context === undefined
|
|
149
161
|
? page.frames
|
|
150
|
-
: page.frames.map((frame) => frame.kind === "update"
|
|
162
|
+
: page.frames.map((frame) => frame.kind === "update" &&
|
|
163
|
+
frame.cursor === latestCursor
|
|
151
164
|
? Object.freeze({ ...frame, context })
|
|
152
165
|
: frame),
|
|
153
166
|
Option.some(next),
|
|
@@ -173,11 +186,14 @@ function decodeWatchInput(sessionId, input) {
|
|
|
173
186
|
function watchSession(options, runtime, observation, sessionId, input) {
|
|
174
187
|
return runtime.assertOpen.pipe(Effect.andThen(decodeWatchInput(sessionId, input)), Effect.flatMap((decoded) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
175
188
|
const registration = yield* observation.register(decoded.sessionId);
|
|
176
|
-
return yield* restore(runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, decoded.sessionId, decoded.options)), Effect.map((snapshot) =>
|
|
177
|
-
snapshot
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
return yield* restore(runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, decoded.sessionId, decoded.options)), Effect.map((snapshot) => {
|
|
190
|
+
registration.setQueued(snapshot.activity.queue.state === "queued");
|
|
191
|
+
return Object.freeze({
|
|
192
|
+
snapshot,
|
|
193
|
+
changes: subscriptionFrames(options, runtime, registration, decoded.sessionId, snapshot.cursor ?? undefined, "live", decoded.options.includeSandboxFailures === true).pipe(Stream.ensuring(Effect.sync(registration.close))),
|
|
194
|
+
close: Effect.sync(registration.close),
|
|
195
|
+
});
|
|
196
|
+
}))).pipe(Effect.onError(() => Effect.sync(registration.close)));
|
|
181
197
|
}))));
|
|
182
198
|
}
|
|
183
199
|
/**
|
|
@@ -335,6 +351,35 @@ export function openCoordinator(options, runtime) {
|
|
|
335
351
|
call: (command, input) => runtime.assertOpen.pipe(Effect.andThen(runtime.extensions.call({ scope: "session", sessionId }, extensionId, command, input))),
|
|
336
352
|
}),
|
|
337
353
|
read: (input) => runtime.assertOpen.pipe(Effect.andThen(readSessionWithContext(options, runtime, sessionId, input))),
|
|
354
|
+
readSettlement: (commandId, timeoutMs = 0) => Effect.scoped(Effect.gen(function* () {
|
|
355
|
+
yield* runtime.assertOpen;
|
|
356
|
+
const decoded = yield* Effect.try({
|
|
357
|
+
try: () => ({
|
|
358
|
+
sessionId: decodeClientSessionId(sessionId),
|
|
359
|
+
...requireDecoded(decodeClientSettlementRequest({ commandId, timeoutMs }), "Settlement request is invalid"),
|
|
360
|
+
}),
|
|
361
|
+
catch: (cause) => cause instanceof ShiitakeError
|
|
362
|
+
? cause
|
|
363
|
+
: new ShiitakeError("contract", "invalid", "Settlement request is invalid"),
|
|
364
|
+
});
|
|
365
|
+
// Subscribe before reading: a commit racing the first read cannot
|
|
366
|
+
// be missed. Only settlement hints reach this registration.
|
|
367
|
+
const registration = yield* Effect.acquireRelease(observation.register(decoded.sessionId, "settlement"), (value) => Effect.sync(value.close));
|
|
368
|
+
const read = readCommandSettlement(options.store, decoded.sessionId, decoded.commandId);
|
|
369
|
+
const initial = yield* read;
|
|
370
|
+
if (initial !== undefined || decoded.timeoutMs === 0)
|
|
371
|
+
return initial;
|
|
372
|
+
return yield* Effect.gen(function* () {
|
|
373
|
+
for (;;) {
|
|
374
|
+
const hint = yield* registration.awaitSignal;
|
|
375
|
+
if (hint === "closed")
|
|
376
|
+
return yield* Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"));
|
|
377
|
+
const settled = yield* read;
|
|
378
|
+
if (settled !== undefined)
|
|
379
|
+
return settled;
|
|
380
|
+
}
|
|
381
|
+
}).pipe(Effect.timeoutOption(decoded.timeoutMs), Effect.map(Option.getOrUndefined));
|
|
382
|
+
})),
|
|
338
383
|
subscribe: (input) => subscribeSession(options, runtime, observation, sessionId, input),
|
|
339
384
|
watch: (input) => watchSession(options, runtime, observation, sessionId, input),
|
|
340
385
|
});
|
|
@@ -370,7 +415,47 @@ export function openCoordinator(options, runtime) {
|
|
|
370
415
|
* is the host's authentication of a delegated Agent's reply; only then does
|
|
371
416
|
* the model see the line inside the verified-reply tag.
|
|
372
417
|
*/
|
|
373
|
-
export function acceptAddressedMail(runtime, mail, meta, reply) {
|
|
418
|
+
export function acceptAddressedMail(runtime, mail, meta, reply, stageMail, sessionGrantDelivery) {
|
|
419
|
+
if (mail.to.sessionId === undefined) {
|
|
420
|
+
if (reply !== undefined || mail.replyTo.length === 0)
|
|
421
|
+
throw new Error("Only delegated work can create a recipient Session");
|
|
422
|
+
return runtime.door.transact((admit) => {
|
|
423
|
+
const sessionId = runtime.createSessionWithinTransaction(mail.key);
|
|
424
|
+
const grants = sessionGrantDelivery?.(sessionId);
|
|
425
|
+
if (grants !== undefined)
|
|
426
|
+
runtime.stageSessionGrantsWithinTransaction(sessionId, grants);
|
|
427
|
+
const addressed = {
|
|
428
|
+
...mail,
|
|
429
|
+
to: { ...mail.to, sessionId },
|
|
430
|
+
};
|
|
431
|
+
const request = {
|
|
432
|
+
sessionId,
|
|
433
|
+
requireExistingSession: true,
|
|
434
|
+
command: {
|
|
435
|
+
requestId: mail.key,
|
|
436
|
+
delivery: "followUp",
|
|
437
|
+
type: "prompt",
|
|
438
|
+
text: mail.content,
|
|
439
|
+
},
|
|
440
|
+
when: "now",
|
|
441
|
+
origin: { kind: "user" },
|
|
442
|
+
mail: addressed,
|
|
443
|
+
onAccepted: (_acceptedSessionId, commandId) => {
|
|
444
|
+
if (stageMail === undefined)
|
|
445
|
+
throw new Error("Recipient Agent mail outbox is unavailable");
|
|
446
|
+
stageMail(acceptedNotice(mail, sessionId, commandId, Date.now()));
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
const result = admit(meta === undefined ? request : { ...request, meta });
|
|
450
|
+
if (result.status !== "accepted")
|
|
451
|
+
throw new Error("Sessionless addressed mail was not admitted");
|
|
452
|
+
return {
|
|
453
|
+
status: "accepted",
|
|
454
|
+
commandId: result.commandId,
|
|
455
|
+
sessionId,
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
}
|
|
374
459
|
return admitUserWork(runtime, mail.to.sessionId, {
|
|
375
460
|
requestId: mail.key,
|
|
376
461
|
delivery: reply === undefined ? "followUp" : "steer",
|
|
@@ -5,7 +5,9 @@ import type { ShiitakeOwnerAgent, ShiitakeOptions } from "./contracts.js";
|
|
|
5
5
|
/** Adapt a Promise tool configuration for both initial open and reload. */
|
|
6
6
|
export declare function liftTools(toolSource: NonNullable<ShiitakeOptions["tools"]>): NonNullable<ShiitakeEffectOptions["tools"]>;
|
|
7
7
|
/** Run one Shiitake Effect through its sole Promise error adaptation. */
|
|
8
|
-
export declare function runShiitakePromise<A, E>(effect: Effect.Effect<A, E
|
|
8
|
+
export declare function runShiitakePromise<A, E>(effect: Effect.Effect<A, E>, options?: Readonly<{
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
}>): Promise<A>;
|
|
9
11
|
/** Adapt one canonical Effect agent to Shiitake's ordinary web interface. */
|
|
10
12
|
export declare function toPromiseAgent(agent: ShiitakeEffectAgent, suppliedAddress?: string): ShiitakeAgent;
|
|
11
13
|
/**
|
|
@@ -19,8 +19,8 @@ export function liftTools(toolSource) {
|
|
|
19
19
|
: { ...tools, execution: toolSource.execution };
|
|
20
20
|
}
|
|
21
21
|
/** Run one Shiitake Effect through its sole Promise error adaptation. */
|
|
22
|
-
export function runShiitakePromise(effect) {
|
|
23
|
-
return Effect.runPromiseExit(effect).then((exit) => {
|
|
22
|
+
export function runShiitakePromise(effect, options) {
|
|
23
|
+
return Effect.runPromiseExit(effect, options).then((exit) => {
|
|
24
24
|
if (Exit.isSuccess(exit))
|
|
25
25
|
return exit.value;
|
|
26
26
|
const failure = Option.getOrUndefined(Cause.findErrorOption(exit.cause));
|
|
@@ -76,6 +76,7 @@ function sessionFacade(address, session) {
|
|
|
76
76
|
});
|
|
77
77
|
},
|
|
78
78
|
read: (input) => runShiitakePromise(session.read(input)),
|
|
79
|
+
readSettlement: (commandId, options) => runShiitakePromise(session.readSettlement(commandId, options?.timeoutMs), options),
|
|
79
80
|
subscribe: (input) => observationAsyncIterable(session.subscribe(input)),
|
|
80
81
|
watch: (input) => runShiitakePromise(session.watch(input)).then((watch) => Object.freeze({
|
|
81
82
|
snapshot: watch.snapshot,
|
|
@@ -2,7 +2,7 @@ import type { HostMetaOptions } from "./host-meta.js";
|
|
|
2
2
|
import type { ExtensionRosterV0 } from "../extensions.js";
|
|
3
3
|
import type { Api, AssistantMessage, Model, MuleStreamFunction, RetryPolicy } from "@fungi.computer/mule/effect";
|
|
4
4
|
import type { CapabilitySource, ExecutionOptions, ModuleRegistration } from "@fungi.computer/mycelium";
|
|
5
|
-
import type { GrantsDelivery, GrantsDeliveryResult, ToolAccess } from "./contracts.js";
|
|
5
|
+
import type { CommandSettlement, GrantsDelivery, GrantsDeliveryResult, ToolAccess } from "./contracts.js";
|
|
6
6
|
import type { Sandbox } from "@fungi.computer/mycelium/effect";
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Stream from "effect/Stream";
|
|
@@ -26,6 +26,8 @@ export type ModelAcquisitionEffect = Readonly<{
|
|
|
26
26
|
retryable: (message: AssistantMessage) => boolean;
|
|
27
27
|
/** Owner-authored reason for this door's failure message, never SDK prose. */
|
|
28
28
|
failureReason?: (message: AssistantMessage) => string | undefined;
|
|
29
|
+
/** Provider prose for this door's exact failed turn; never a diagnostic. */
|
|
30
|
+
providerError?: (message: AssistantMessage) => string | undefined;
|
|
29
31
|
/** Effect finalizer for acquired model resources. */
|
|
30
32
|
release: Effect.Effect<void, ShiitakeError>;
|
|
31
33
|
}>;
|
|
@@ -128,7 +130,7 @@ export type ShiitakeEffectOptions = Readonly<{
|
|
|
128
130
|
retry?: RetryPolicy;
|
|
129
131
|
/**
|
|
130
132
|
* Maximum silence between live provider stream events, in milliseconds.
|
|
131
|
-
* Omitted
|
|
133
|
+
* Omitted uses Mule's model stream limits.
|
|
132
134
|
*/
|
|
133
135
|
modelStreamIdleTimeoutMs?: number;
|
|
134
136
|
}>;
|
|
@@ -189,6 +191,8 @@ export type ShiitakeEffectSession = Readonly<{
|
|
|
189
191
|
}>;
|
|
190
192
|
/** Read a bounded durable baseline. */
|
|
191
193
|
read: (input?: ReadOptions) => Effect.Effect<SessionSnapshot, ShiitakeError>;
|
|
194
|
+
/** Exact command facts; timeout releases observation without cancelling work. */
|
|
195
|
+
readSettlement: (commandId: string, timeoutMs?: number) => Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
|
|
192
196
|
/** Subscribe to strict-after replay and live frames. */
|
|
193
197
|
subscribe: (input: SubscribeOptions) => Stream.Stream<SessionFrame, ShiitakeError>;
|
|
194
198
|
/** Open a race-safe baseline plus live changes resource. */
|
|
@@ -21,6 +21,8 @@ function asEffectAcquisition(acquisition) {
|
|
|
21
21
|
};
|
|
22
22
|
if (acquisition.failureReason !== undefined)
|
|
23
23
|
shared = { ...shared, failureReason: acquisition.failureReason };
|
|
24
|
+
if (acquisition.providerError !== undefined)
|
|
25
|
+
shared = { ...shared, providerError: acquisition.providerError };
|
|
24
26
|
if (acquisition.stream === undefined)
|
|
25
27
|
return acquisition.retryAfterMs === undefined
|
|
26
28
|
? Object.freeze(shared)
|
|
@@ -2,8 +2,8 @@ import * as Effect from "effect/Effect";
|
|
|
2
2
|
import { ShiitakeError } from "../errors.js";
|
|
3
3
|
import type { SqliteOwner } from "./contracts.js";
|
|
4
4
|
/** Finite process-local signal; durable owner state remains authoritative. */
|
|
5
|
-
export type ObservationSignal = "closed" | "reset" | "durable" | "progress";
|
|
6
|
-
export type ObservationReason = "durable" | "progress";
|
|
5
|
+
export type ObservationSignal = "closed" | "reset" | "durable" | "activity" | "progress";
|
|
6
|
+
export type ObservationReason = "durable" | "activity" | "progress" | "settlement";
|
|
7
7
|
/** One scoped process-local observation registration. */
|
|
8
8
|
export type ObservationRegistration = Readonly<{
|
|
9
9
|
/** Inspect and consume one pending finite signal without waiting. */
|
|
@@ -12,17 +12,21 @@ export type ObservationRegistration = Readonly<{
|
|
|
12
12
|
awaitSignal: Effect.Effect<ObservationSignal>;
|
|
13
13
|
/** Release this registration without affecting durable work. */
|
|
14
14
|
close: () => void;
|
|
15
|
+
/** Track whether this Session's current queue head is waiting on the Agent. */
|
|
16
|
+
setQueued: (queued: boolean) => void;
|
|
15
17
|
}>;
|
|
16
18
|
/** Process-local wake coordination; durable facts remain authoritative. */
|
|
17
19
|
export type ObservationKernel = Readonly<{
|
|
18
20
|
/** Register before the caller's first durable read. */
|
|
19
|
-
register: (sessionId: string) => Effect.Effect<ObservationRegistration, ShiitakeError>;
|
|
21
|
+
register: (sessionId: string, interest?: "settlement") => Effect.Effect<ObservationRegistration, ShiitakeError>;
|
|
20
22
|
/** Close every registration and release this agent's owner publisher. */
|
|
21
23
|
close: Effect.Effect<void>;
|
|
22
24
|
}>;
|
|
23
25
|
/** Combine coalesced reasons without allowing a progress hint to hide a commit. */
|
|
24
26
|
export declare function mergeObservationSignals(first: ObservationSignal, second: ObservationSignal): ObservationSignal;
|
|
25
|
-
/** Notify
|
|
27
|
+
/** Notify observers of one Session's durable or process-local change. */
|
|
26
28
|
export declare function publishOwnerObservation(store: SqliteOwner, sessionId: string | null, reason?: ObservationReason): void;
|
|
29
|
+
/** Wake queued Sessions when this Agent's execution slot changes. */
|
|
30
|
+
export declare function publishAgentExecutionChange(store: SqliteOwner): void;
|
|
27
31
|
/** Open one coalescing process-local observation kernel per agent. */
|
|
28
32
|
export declare function openObservationKernel(store: SqliteOwner): ObservationKernel;
|
|
@@ -12,6 +12,8 @@ export function mergeObservationSignals(first, second) {
|
|
|
12
12
|
return "reset";
|
|
13
13
|
if (first === "durable" || second === "durable")
|
|
14
14
|
return "durable";
|
|
15
|
+
if (first === "activity" || second === "activity")
|
|
16
|
+
return "activity";
|
|
15
17
|
return "progress";
|
|
16
18
|
}
|
|
17
19
|
function queueObservation(listener, reason) {
|
|
@@ -25,7 +27,9 @@ function queueObservation(listener, reason) {
|
|
|
25
27
|
listener.pendingReason =
|
|
26
28
|
listener.pendingReason === "durable" || reason === "durable"
|
|
27
29
|
? "durable"
|
|
28
|
-
: "
|
|
30
|
+
: listener.pendingReason === "activity" || reason === "activity"
|
|
31
|
+
? "activity"
|
|
32
|
+
: "progress";
|
|
29
33
|
}
|
|
30
34
|
function deliverObservation(listener, reason) {
|
|
31
35
|
if (listener.closed || listener.reset)
|
|
@@ -46,14 +50,14 @@ function deliverObservation(listener, reason) {
|
|
|
46
50
|
listener.reset = true;
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
|
-
/** Notify
|
|
53
|
+
/** Notify observers of one Session's durable or process-local change. */
|
|
50
54
|
export function publishOwnerObservation(store, sessionId, reason = "durable") {
|
|
51
55
|
const publishers = ownerPublishers.get(store);
|
|
52
56
|
if (publishers === undefined)
|
|
53
57
|
return;
|
|
54
58
|
for (const publish of publishers) {
|
|
55
59
|
try {
|
|
56
|
-
publish(sessionId, reason);
|
|
60
|
+
publish({ kind: "session", sessionId, reason });
|
|
57
61
|
}
|
|
58
62
|
catch (cause) {
|
|
59
63
|
reportCatchFailure("owner-observation-publish", cause);
|
|
@@ -61,6 +65,20 @@ export function publishOwnerObservation(store, sessionId, reason = "durable") {
|
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
}
|
|
68
|
+
/** Wake queued Sessions when this Agent's execution slot changes. */
|
|
69
|
+
export function publishAgentExecutionChange(store) {
|
|
70
|
+
const publishers = ownerPublishers.get(store);
|
|
71
|
+
if (publishers === undefined)
|
|
72
|
+
return;
|
|
73
|
+
for (const publish of publishers) {
|
|
74
|
+
try {
|
|
75
|
+
publish({ kind: "agent-execution" });
|
|
76
|
+
}
|
|
77
|
+
catch (cause) {
|
|
78
|
+
reportCatchFailure("owner-execution-change-publish", cause);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
64
82
|
/** Open one coalescing process-local observation kernel per agent. */
|
|
65
83
|
export function openObservationKernel(store) {
|
|
66
84
|
const sessions = new Map();
|
|
@@ -79,7 +97,7 @@ export function openObservationKernel(store) {
|
|
|
79
97
|
listener.pendingCount = 0;
|
|
80
98
|
const reason = listener.pendingReason;
|
|
81
99
|
listener.pendingReason = undefined;
|
|
82
|
-
return reason
|
|
100
|
+
return reason ?? "progress";
|
|
83
101
|
};
|
|
84
102
|
const closeListener = (listener) => {
|
|
85
103
|
if (listener.closed)
|
|
@@ -104,26 +122,39 @@ export function openObservationKernel(store) {
|
|
|
104
122
|
}
|
|
105
123
|
}
|
|
106
124
|
};
|
|
107
|
-
const publish = (
|
|
108
|
-
|
|
125
|
+
const publish = (publication) => {
|
|
126
|
+
if (publication.kind === "agent-execution") {
|
|
127
|
+
for (const listener of active)
|
|
128
|
+
if (listener.queued && !listener.settlementOnly)
|
|
129
|
+
deliverObservation(listener, "activity");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const listeners = publication.sessionId === null
|
|
133
|
+
? active
|
|
134
|
+
: sessions.get(publication.sessionId);
|
|
109
135
|
if (listeners === undefined)
|
|
110
136
|
return;
|
|
111
|
-
for (const listener of listeners)
|
|
112
|
-
|
|
137
|
+
for (const listener of listeners) {
|
|
138
|
+
if (listener.settlementOnly && publication.reason !== "settlement")
|
|
139
|
+
continue;
|
|
140
|
+
deliverObservation(listener, publication.reason === "settlement" ? "durable" : publication.reason);
|
|
141
|
+
}
|
|
113
142
|
};
|
|
114
143
|
publishers.add(publish);
|
|
115
144
|
ownerPublishers.set(store, publishers);
|
|
116
145
|
return Object.freeze({
|
|
117
|
-
register: (sessionId) => Effect.suspend(() => {
|
|
146
|
+
register: (sessionId, interest) => Effect.suspend(() => {
|
|
118
147
|
if (released)
|
|
119
148
|
return Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"));
|
|
120
149
|
if (active.size >= MAX_OBSERVATIONS)
|
|
121
150
|
return Effect.fail(new ShiitakeError("runtime", "rejected", "Observation capacity is exhausted"));
|
|
122
151
|
const listener = {
|
|
123
152
|
sessionId,
|
|
153
|
+
settlementOnly: interest === "settlement",
|
|
124
154
|
closed: false,
|
|
125
155
|
pendingCount: 0,
|
|
126
156
|
pendingReason: undefined,
|
|
157
|
+
queued: true,
|
|
127
158
|
reset: false,
|
|
128
159
|
wake: undefined,
|
|
129
160
|
};
|
|
@@ -153,6 +184,10 @@ export function openObservationKernel(store) {
|
|
|
153
184
|
close: () => {
|
|
154
185
|
closeListener(listener);
|
|
155
186
|
},
|
|
187
|
+
setQueued: (queued) => {
|
|
188
|
+
if (!listener.closed)
|
|
189
|
+
listener.queued = queued;
|
|
190
|
+
},
|
|
156
191
|
}));
|
|
157
192
|
}),
|
|
158
193
|
close: Effect.sync(() => {
|
|
@@ -14,5 +14,5 @@ export declare function openPromiseAgent(options: ShiitakeOptions, make: (option
|
|
|
14
14
|
export declare function openPromiseHost(options: ShiitakeOptions, mailRoom: BirdDogEffect, openHost: (options: ShiitakeEffectOptions, mailRoom: BirdDogEffect) => Effect.Effect<ShiitakeEffectHost, ShiitakeError>): Promise<Readonly<{
|
|
15
15
|
agent: ShiitakeOwnerAgent;
|
|
16
16
|
readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
|
|
17
|
-
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply) => Promise<Accepted>;
|
|
17
|
+
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply, stageAcceptedNotice?: (mail: BirdDogMail) => void) => Promise<Accepted>;
|
|
18
18
|
}>>;
|
|
@@ -155,6 +155,6 @@ export function openPromiseHost(options, mailRoom, openHost) {
|
|
|
155
155
|
return runShiitakePromise(liftRuntime(options).pipe(Effect.flatMap((runtime) => openHost(liftOptions(options, runtime), mailRoom)))).then(({ agent, acceptMail, readSettlement }) => Object.freeze({
|
|
156
156
|
agent: toPromiseOwner(agent),
|
|
157
157
|
readSettlement: (sessionId, commandId) => runShiitakePromise(readSettlement(sessionId, commandId)),
|
|
158
|
-
acceptMail: (mail, meta, reply) => runShiitakePromise(acceptMail(mail, meta, reply)),
|
|
158
|
+
acceptMail: (mail, meta, reply, stageAcceptedNotice) => runShiitakePromise(acceptMail(mail, meta, reply, stageAcceptedNotice)),
|
|
159
159
|
}));
|
|
160
160
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { muleEffect, piCodingAgentRetryDefaults,
|
|
1
|
+
import { muleEffect, piCodingAgentRetryDefaults, } from "@fungi.computer/mule/effect";
|
|
2
2
|
import { make as makeMycelium } from "@fungi.computer/mycelium/effect";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
4
|
import * as Cause from "effect/Cause";
|
|
@@ -33,13 +33,21 @@ export class ToolSetupError extends ShiitakeError {
|
|
|
33
33
|
* Map Mule's retry transitions onto Woodstock's retained `live_retry` facts.
|
|
34
34
|
* A started attempt ends the backoff countdown before streaming resumes.
|
|
35
35
|
*/
|
|
36
|
-
function toLiveRetryEvent(event) {
|
|
36
|
+
function toLiveRetryEvent(event, model) {
|
|
37
37
|
if (event.type === "retry_attempt_started")
|
|
38
38
|
return {
|
|
39
39
|
type: "auto_retry_attempt",
|
|
40
40
|
run: event.run,
|
|
41
41
|
attempt: event.attempt,
|
|
42
42
|
};
|
|
43
|
+
// Mule's retry text is Pi's private error body. Woodstock's retry journal
|
|
44
|
+
// feeds status and recovery, not the owner's failed-turn conversation.
|
|
45
|
+
const safeReason = ("Model request failed. Retry the request. " +
|
|
46
|
+
unavailableRunReason({
|
|
47
|
+
runId: event.run.value,
|
|
48
|
+
provider: model.provider,
|
|
49
|
+
model: model.id,
|
|
50
|
+
})).slice(0, 200);
|
|
43
51
|
return event.type === "retry_scheduled"
|
|
44
52
|
? {
|
|
45
53
|
type: "auto_retry_start",
|
|
@@ -47,7 +55,7 @@ function toLiveRetryEvent(event) {
|
|
|
47
55
|
attempt: event.attempt,
|
|
48
56
|
maxAttempts: event.maxAttempts,
|
|
49
57
|
delayMs: event.delayMs,
|
|
50
|
-
errorMessage:
|
|
58
|
+
errorMessage: safeReason,
|
|
51
59
|
}
|
|
52
60
|
: event.finalError === undefined
|
|
53
61
|
? {
|
|
@@ -61,7 +69,7 @@ function toLiveRetryEvent(event) {
|
|
|
61
69
|
run: event.run,
|
|
62
70
|
attempt: event.attempt,
|
|
63
71
|
success: event.success,
|
|
64
|
-
finalError:
|
|
72
|
+
finalError: safeReason,
|
|
65
73
|
};
|
|
66
74
|
}
|
|
67
75
|
/** Acquire the run's tools for exactly one Session, so hosts can project per Session. */
|
|
@@ -139,6 +147,7 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
139
147
|
let resume = prepared.resume;
|
|
140
148
|
const settings = openCompactionSettings(options.compaction)(acquired.model);
|
|
141
149
|
let failureReason;
|
|
150
|
+
let failedModelAttempts = 0;
|
|
142
151
|
const run = yield* runWithCompaction({
|
|
143
152
|
store: options.store,
|
|
144
153
|
sessionId: prepared.command.sessionId,
|
|
@@ -151,6 +160,7 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
151
160
|
messages: prepared.messages,
|
|
152
161
|
resuming: resume !== undefined,
|
|
153
162
|
run: (workingMessages, betweenTurns) => {
|
|
163
|
+
failedModelAttempts = 0;
|
|
154
164
|
const continuation = resume;
|
|
155
165
|
resume = undefined;
|
|
156
166
|
// A failed lifecycle notification precedes Mule's returned outcome
|
|
@@ -162,11 +172,21 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
162
172
|
failedEnd = event;
|
|
163
173
|
return Effect.void;
|
|
164
174
|
}
|
|
175
|
+
if (event.type === "message_end" &&
|
|
176
|
+
event.value.role === "assistant" &&
|
|
177
|
+
event.value.stopReason === "error") {
|
|
178
|
+
const providerError = acquired.providerError?.(event.value);
|
|
179
|
+
if (providerError !== undefined)
|
|
180
|
+
return recorder({
|
|
181
|
+
...event,
|
|
182
|
+
value: { ...event.value, providerError },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
165
185
|
return recorder(event);
|
|
166
186
|
};
|
|
167
187
|
const input = {
|
|
168
188
|
model: acquired.model,
|
|
169
|
-
modelStreamIdleTimeoutMs: options.modelStreamIdleTimeoutMs
|
|
189
|
+
modelStreamIdleTimeoutMs: options.modelStreamIdleTimeoutMs,
|
|
170
190
|
retry: options.retry ?? piCodingAgentRetryDefaults,
|
|
171
191
|
retryAfterMs: acquired.retryAfterMs,
|
|
172
192
|
retryable: acquired.retryable,
|
|
@@ -191,7 +211,11 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
191
211
|
phase: "control",
|
|
192
212
|
message: error.message,
|
|
193
213
|
}))),
|
|
194
|
-
onRetry: (event) =>
|
|
214
|
+
onRetry: (event) => {
|
|
215
|
+
if (event.type === "retry_finished" && !event.success)
|
|
216
|
+
failedModelAttempts = event.attempt + 1;
|
|
217
|
+
return recordLiveRetryEvent(options.store, accepted, toLiveRetryEvent(event, acquired.model), event.turn);
|
|
218
|
+
},
|
|
195
219
|
};
|
|
196
220
|
return muleEffect
|
|
197
221
|
.run(acquired.stream === undefined
|
|
@@ -201,8 +225,13 @@ export function runAcceptedCommand(options, prepared, executionContract, extensi
|
|
|
201
225
|
if (failedEnd === undefined)
|
|
202
226
|
return Effect.void;
|
|
203
227
|
if (Exit.isSuccess(exit)) {
|
|
204
|
-
|
|
205
|
-
|
|
228
|
+
const cause = exit.value.modelLimitReason ??
|
|
229
|
+
acquired.failureReason?.(exit.value.message) ??
|
|
230
|
+
returnedFailureReason(exit.value);
|
|
231
|
+
failureReason = (failedModelAttempts > 1 &&
|
|
232
|
+
exit.value.modelLimitReason === undefined
|
|
233
|
+
? `The model failed ${String(failedModelAttempts)} times on this conversation: ${cause}`
|
|
234
|
+
: cause).slice(0, 200);
|
|
206
235
|
return recorder(failedEnd, failureReason);
|
|
207
236
|
}
|
|
208
237
|
const error = Cause.findErrorOption(exit.cause);
|
|
@@ -2,6 +2,7 @@ import { type BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
|
2
2
|
import { type WatchdogQueueHead } from "@fungi.computer/watchdog/effect";
|
|
3
3
|
import * as Effect from "effect/Effect";
|
|
4
4
|
import type { AgentRunningWork } from "../client.js";
|
|
5
|
+
import type { GrantsDelivery } from "./contracts.js";
|
|
5
6
|
import { ShiitakeError } from "../errors.js";
|
|
6
7
|
import { type AdmissionDoor } from "./admission-door.js";
|
|
7
8
|
import type { ShiitakeEffectOptions } from "./effect-contracts.js";
|
|
@@ -19,6 +20,10 @@ export type RuntimeCoordinator = Readonly<{
|
|
|
19
20
|
beginClose: Effect.Effect<void>;
|
|
20
21
|
/** The one accepted-command door every prompt producer admits through. */
|
|
21
22
|
door: AdmissionDoor;
|
|
23
|
+
/** Reserve a Session within the admission door's enclosing transaction. */
|
|
24
|
+
createSessionWithinTransaction: (requestId: string) => string;
|
|
25
|
+
/** Write new-Session grants through the enclosing admission transaction. */
|
|
26
|
+
stageSessionGrantsWithinTransaction: (sessionId: string, delivery: GrantsDelivery) => void;
|
|
22
27
|
/** Storage-pressure check for new Sessions and new prompts. */
|
|
23
28
|
storage: StorageAdmission;
|
|
24
29
|
recordAcceptedCancellation: RecordAcceptedCancellation;
|
|
@@ -3,7 +3,7 @@ import { WatchdogError } from "@fungi.computer/watchdog";
|
|
|
3
3
|
import { makeRuntime as makeWatchdogRuntime, } from "@fungi.computer/watchdog/effect";
|
|
4
4
|
import * as Effect from "effect/Effect";
|
|
5
5
|
import * as Latch from "effect/Latch";
|
|
6
|
-
import {
|
|
6
|
+
import { publishAgentExecutionChange } from "./observation-kernel.js";
|
|
7
7
|
import { ShiitakeError } from "../errors.js";
|
|
8
8
|
import { agentTurnRecovery, executeAcceptedRun, } from "./accepted-run-executor.js";
|
|
9
9
|
import { openAdmissionDoor } from "./admission-door.js";
|
|
@@ -11,7 +11,7 @@ import { openCompactionSettings } from "./compaction/settings.js";
|
|
|
11
11
|
import { admitExtensionRosterV0, } from "./extension-roster.js";
|
|
12
12
|
import { coreConsumer, openExtensionRuntime, } from "./extension-runtime.js";
|
|
13
13
|
import { reconcileRuntime } from "./runtime-reconciliation.js";
|
|
14
|
-
import { advanceCursor, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
|
|
14
|
+
import { advanceCursor, createSessionWithinTransaction, deliverSessionGrants, prepareExtensionStore, projectSessionRun, openStorageAdmission, readCursor, } from "./woodstock/index.js";
|
|
15
15
|
function runtimeFailure(message) {
|
|
16
16
|
return new ShiitakeError("runtime", "rejected", message);
|
|
17
17
|
}
|
|
@@ -64,7 +64,7 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
64
64
|
if (runningWork.kind === "session")
|
|
65
65
|
activeSessions.add(job.queue);
|
|
66
66
|
// A different Session's wait changes when this executor changes.
|
|
67
|
-
|
|
67
|
+
publishAgentExecutionChange(options.store);
|
|
68
68
|
}), () => job.lane === options.hostJobs?.lane
|
|
69
69
|
? (hostExecutor.current?.execute(job) ??
|
|
70
70
|
Effect.fail(new WatchdogError("execution_failed")))
|
|
@@ -82,7 +82,7 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
82
82
|
if (job.lane !== options.hostJobs?.lane)
|
|
83
83
|
activeSessions.delete(job.queue);
|
|
84
84
|
runningSlots.delete(job.lane === options.hostJobs?.lane ? "host" : "serial");
|
|
85
|
-
|
|
85
|
+
publishAgentExecutionChange(options.store);
|
|
86
86
|
})),
|
|
87
87
|
},
|
|
88
88
|
wake: {
|
|
@@ -253,6 +253,10 @@ export function openRuntimeCoordinator(options, coreDigest, admitted, mailRoom)
|
|
|
253
253
|
: Effect.fail(new ShiitakeError("runtime", "closed", "Shiitake is closed"))),
|
|
254
254
|
beginClose,
|
|
255
255
|
door,
|
|
256
|
+
createSessionWithinTransaction: (requestId) => createSessionWithinTransaction(options.store, requestId, storage.admitNew),
|
|
257
|
+
stageSessionGrantsWithinTransaction: (sessionId, delivery) => {
|
|
258
|
+
deliverSessionGrants(options.store, sessionId, delivery);
|
|
259
|
+
},
|
|
256
260
|
storage,
|
|
257
261
|
recordAcceptedCancellation: ({ jobId, cancellationId, onlyIfPending }) => watchdog.transactional.recordCancellationIntent({
|
|
258
262
|
jobId,
|
|
@@ -5,6 +5,7 @@ import type { VerifiedAgentReply } from "./woodstock/index.js";
|
|
|
5
5
|
import type { BirdDogEffect } from "@fungi.computer/bird-dog/effect";
|
|
6
6
|
import * as Effect from "effect/Effect";
|
|
7
7
|
import type { Accepted } from "../client.js";
|
|
8
|
+
import type { GrantsDelivery } from "./contracts.js";
|
|
8
9
|
import { ShiitakeError } from "../errors.js";
|
|
9
10
|
import type { ShiitakeEffectOptions, ShiitakeEffectOwner } from "./effect-contracts.js";
|
|
10
11
|
/** Private host composition with the addressed-mail ingress kept off Session. */
|
|
@@ -13,7 +14,7 @@ export type RuntimeCore = Readonly<{
|
|
|
13
14
|
readSettlement: (sessionId: string, commandId: string) => Effect.Effect<CommandSettlement | undefined, ShiitakeError>;
|
|
14
15
|
/** Owner-local execution projection; never served by the remote client. */
|
|
15
16
|
activeSessions: Effect.Effect<readonly string[]>;
|
|
16
|
-
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply) => Effect.Effect<Accepted, ShiitakeError>;
|
|
17
|
+
acceptMail: (mail: BirdDogMail, meta?: Json, reply?: VerifiedAgentReply, stageAcceptedNotice?: (mail: BirdDogMail) => void, sessionGrantDelivery?: (sessionId: string) => GrantsDelivery) => Effect.Effect<Accepted, ShiitakeError>;
|
|
17
18
|
}>;
|
|
18
19
|
/** Open Runtime Core, optionally joining its owner-local BirdDog mail room. */
|
|
19
20
|
export declare function openRuntimeCore(options: ShiitakeEffectOptions, mailRoom?: BirdDogEffect): Effect.Effect<RuntimeCore, ShiitakeError>;
|
|
@@ -34,7 +34,12 @@ export function openRuntimeCore(options, mailRoom) {
|
|
|
34
34
|
agent: owner,
|
|
35
35
|
readSettlement: (sessionId, commandId) => runtime.assertOpen.pipe(Effect.andThen(readCommandSettlement(options.store, sessionId, commandId))),
|
|
36
36
|
activeSessions: runtime.activeSessions,
|
|
37
|
-
acceptMail: (mail, meta, reply) => acceptAddressedMail(runtime, mail, meta, reply
|
|
37
|
+
acceptMail: (mail, meta, reply, stageAcceptedNotice, sessionGrantDelivery) => acceptAddressedMail(runtime, mail, meta, reply, stageAcceptedNotice ??
|
|
38
|
+
((notice) => {
|
|
39
|
+
if (mailRoom?.postMail === undefined)
|
|
40
|
+
throw new Error("Recipient Agent mail outbox is unavailable");
|
|
41
|
+
Effect.runSync(mailRoom.postMail(notice));
|
|
42
|
+
}), sessionGrantDelivery),
|
|
38
43
|
});
|
|
39
44
|
});
|
|
40
45
|
}
|