@frockbot/kernel-contracts 0.3.1 → 0.3.3

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.
@@ -1,6 +1,6 @@
1
1
  // Generated by scripts/generate-isolate-context-catalog.ts. Do not edit.
2
2
  export const BOT_ISOLATE_CONTEXT_DTS_V1 =
3
- "/**\n * The common exact `ctx` passed to a Bot-authored Package's tool and hook.\n * The model-facing declarations are generated from these interfaces, and the\n * wrapper's implementation is compile- and test-checked against the same\n * keys.\n */\ninterface BotPackageContextV1 {\n readonly tool?: string;\n readonly event?: BotIsolateHookEventNameV1;\n readonly botId: string;\n readonly sessionId: string;\n readonly runId: string;\n readonly turnId: string;\n readonly generationId: string;\n readonly packageId: string;\n readonly deadlineMs: number;\n readonly bindings: string[];\n readonly capabilities: {\n list(): Promise<IsolateCapabilityListOutcomeV1>;\n };\n readonly model: {\n invoke(request: NormalizedModelRequest): Promise<BotPackageModelOutcomeV1>;\n };\n readonly tools: {\n /** Runs through the trusted registry's active-Composition and deny guards. */\n invoke(request: IsolateToolRequestV1): Promise<IsolateToolOutcomeV1>;\n };\n readonly memory: {\n read(request: IsolateMemoryReadRequestV1): Promise<IsolateMemoryOutcomeV1>;\n write(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>;\n forget(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>;\n };\n readonly workspace: {\n read(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>;\n list(request: IsolateWorkspaceListRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n stat(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>;\n write(request: IsolateWorkspaceWriteRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n delete(request: IsolateWorkspaceDeleteRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n };\n connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>;\n notify(request: IsolateNotificationRequestV1): Promise<IsolateNotificationOutcomeV1>;\n schedule(request: IsolateScheduleRequestV1): Promise<IsolateScheduleOutcomeV1>;\n}\n\ninterface BotPackageExecutionContextV1 extends BotPackageContextV1 {\n readonly tool: string;\n readonly event?: never;\n}\n\ninterface BotPackageHookContextV1 extends BotPackageContextV1 {\n readonly tool?: never;\n readonly event: BotIsolateHookEventNameV1;\n}";
3
+ "/**\n * The common exact `ctx` passed to a Bot-authored Package's tool and hook.\n * The model-facing declarations are generated from these interfaces, and the\n * wrapper's implementation is compile- and test-checked against the same\n * keys.\n */\ninterface BotPackageContextV1 {\n readonly tool?: string;\n readonly event?: BotIsolateHookEventNameV1;\n readonly botId: string;\n readonly sessionId: string;\n readonly runId: string;\n readonly turnId: string;\n readonly generationId: string;\n readonly packageId: string;\n readonly deadlineMs: number;\n readonly bindings: string[];\n readonly capabilities: {\n list(): Promise<IsolateCapabilityListOutcomeV1>;\n };\n readonly model: {\n invoke(request: NormalizedModelRequest): Promise<BotPackageModelOutcomeV1>;\n };\n readonly tools: {\n /** Runs through the trusted registry's active-Composition and deny guards. */\n invoke(request: IsolateToolRequestV1): Promise<IsolateToolOutcomeV1>;\n };\n readonly memory: {\n read(request: IsolateMemoryReadRequestV1): Promise<IsolateMemoryOutcomeV1>;\n write(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>;\n forget(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>;\n };\n readonly workspace: {\n read(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>;\n list(request: IsolateWorkspaceListRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n stat(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>;\n write(request: IsolateWorkspaceWriteRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n delete(request: IsolateWorkspaceDeleteRequestV1): Promise<IsolateWorkspaceOutcomeV1>;\n };\n /**\n * The User's Applets (ADR 0022). Account-wide: every Bot of this User sees\n * every Applet. `publish` is a durable effect — it reads the built Applet\n * from the Applets Package's durable root, verifies its manifest against the\n * bytes, writes immutable artifacts, records the generation, mounts it, and\n * proposes a new Composition generation for this Bot.\n */\n readonly applets: {\n list(): Promise<IsolateAppletsOutcomeV1>;\n create(input: {\n displayName: string;\n }): Promise<IsolateAppletsOutcomeV1>;\n publish(input: {\n appletId: string;\n }): Promise<IsolateAppletsOutcomeV1>;\n revert(input: {\n appletId: string;\n generationId: string;\n }): Promise<IsolateAppletsOutcomeV1>;\n delete(input: {\n appletId: string;\n }): Promise<IsolateAppletsOutcomeV1>;\n focus(input: {\n appletId: string | null;\n }): Promise<IsolateAppletsOutcomeV1>;\n generations(input: {\n appletId: string;\n }): Promise<IsolateAppletsOutcomeV1>;\n };\n connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>;\n notify(request: IsolateNotificationRequestV1): Promise<IsolateNotificationOutcomeV1>;\n schedule(request: IsolateScheduleRequestV1): Promise<IsolateScheduleOutcomeV1>;\n}\n\ninterface BotPackageExecutionContextV1 extends BotPackageContextV1 {\n readonly tool: string;\n readonly event?: never;\n}\n\ninterface BotPackageHookContextV1 extends BotPackageContextV1 {\n readonly tool?: never;\n readonly event: BotIsolateHookEventNameV1;\n}";
4
4
  export const BOT_ISOLATE_CONTEXT_KEYS_V1 = [
5
5
  "tool",
6
6
  "event",
@@ -17,9 +17,10 @@ export const BOT_ISOLATE_CONTEXT_KEYS_V1 = [
17
17
  "tools",
18
18
  "memory",
19
19
  "workspace",
20
+ "applets",
20
21
  "connection",
21
22
  "notify",
22
23
  "schedule",
23
24
  ] as const;
24
25
  export const BOT_ISOLATE_CONTEXT_SUMMARY_V1 =
25
- "Package execute and hook ctx share: readonly tool?: string; readonly event?: BotIsolateHookEventNameV1; readonly botId: string; readonly sessionId: string; readonly runId: string; readonly turnId: string; readonly generationId: string; readonly packageId: string; readonly deadlineMs: number; readonly bindings: string[]; readonly capabilities: { list(): Promise<IsolateCapabilityListOutcomeV1>; }; readonly model: { invoke(request: NormalizedModelRequest): Promise<BotPackageModelOutcomeV1>; }; readonly tools: { /** Runs through the trusted registry's active-Composition and deny guards. */ invoke(request: IsolateToolRequestV1): Promise<IsolateToolOutcomeV1>; }; readonly memory: { read(request: IsolateMemoryReadRequestV1): Promise<IsolateMemoryOutcomeV1>; write(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>; forget(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>; }; readonly workspace: { read(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>; list(request: IsolateWorkspaceListRequestV1): Promise<IsolateWorkspaceOutcomeV1>; stat(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>; write(request: IsolateWorkspaceWriteRequestV1): Promise<IsolateWorkspaceOutcomeV1>; delete(request: IsolateWorkspaceDeleteRequestV1): Promise<IsolateWorkspaceOutcomeV1>; }; connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>; notify(request: IsolateNotificationRequestV1): Promise<IsolateNotificationOutcomeV1>; schedule(request: IsolateScheduleRequestV1): Promise<IsolateScheduleOutcomeV1>;";
26
+ "Package execute and hook ctx share: readonly tool?: string; readonly event?: BotIsolateHookEventNameV1; readonly botId: string; readonly sessionId: string; readonly runId: string; readonly turnId: string; readonly generationId: string; readonly packageId: string; readonly deadlineMs: number; readonly bindings: string[]; readonly capabilities: { list(): Promise<IsolateCapabilityListOutcomeV1>; }; readonly model: { invoke(request: NormalizedModelRequest): Promise<BotPackageModelOutcomeV1>; }; readonly tools: { /** Runs through the trusted registry's active-Composition and deny guards. */ invoke(request: IsolateToolRequestV1): Promise<IsolateToolOutcomeV1>; }; readonly memory: { read(request: IsolateMemoryReadRequestV1): Promise<IsolateMemoryOutcomeV1>; write(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>; forget(request: IsolateMemoryWriteRequestV1): Promise<IsolateMemoryOutcomeV1>; }; readonly workspace: { read(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>; list(request: IsolateWorkspaceListRequestV1): Promise<IsolateWorkspaceOutcomeV1>; stat(path: IsolateWorkspacePathV1): Promise<IsolateWorkspaceOutcomeV1>; write(request: IsolateWorkspaceWriteRequestV1): Promise<IsolateWorkspaceOutcomeV1>; delete(request: IsolateWorkspaceDeleteRequestV1): Promise<IsolateWorkspaceOutcomeV1>; }; /** * The User's Applets (ADR 0022). Account-wide: every Bot of this User sees * every Applet. `publish` is a durable effect — it reads the built Applet * from the Applets Package's durable root, verifies its manifest against the * bytes, writes immutable artifacts, records the generation, mounts it, and * proposes a new Composition generation for this Bot. */ readonly applets: { list(): Promise<IsolateAppletsOutcomeV1>; create(input: { displayName: string; }): Promise<IsolateAppletsOutcomeV1>; publish(input: { appletId: string; }): Promise<IsolateAppletsOutcomeV1>; revert(input: { appletId: string; generationId: string; }): Promise<IsolateAppletsOutcomeV1>; delete(input: { appletId: string; }): Promise<IsolateAppletsOutcomeV1>; focus(input: { appletId: string | null; }): Promise<IsolateAppletsOutcomeV1>; generations(input: { appletId: string; }): Promise<IsolateAppletsOutcomeV1>; }; connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>; notify(request: IsolateNotificationRequestV1): Promise<IsolateNotificationOutcomeV1>; schedule(request: IsolateScheduleRequestV1): Promise<IsolateScheduleOutcomeV1>;";
package/src/isolate.ts CHANGED
@@ -230,6 +230,25 @@ export interface IsolateNotificationRequestV1 {
230
230
  export type IsolateNotificationOutcomeV1 =
231
231
  { status: "recorded" } | IsolateCapabilityFailureV1;
232
232
 
233
+ /**
234
+ * The Applet capability a Bot isolate calls (ADR 0022, plan §3).
235
+ *
236
+ * One tagged request rather than seven methods on the stub: the operations
237
+ * share a scope, an authority, and an outcome shape, and the wrapper narrows
238
+ * them back into the seven-verb `ctx.applets` a Bot author writes against.
239
+ */
240
+ export type IsolateAppletsRequestV1 =
241
+ | { op: "list" }
242
+ | { op: "create"; displayName: string }
243
+ | { op: "publish"; appletId: string }
244
+ | { op: "revert"; appletId: string; generationId: string }
245
+ | { op: "delete"; appletId: string }
246
+ | { op: "focus"; appletId: string | null }
247
+ | { op: "generations"; appletId: string };
248
+
249
+ export type IsolateAppletsOutcomeV1 =
250
+ { status: "available"; value: unknown } | IsolateCapabilityFailureV1;
251
+
233
252
  /** A durable Routine operation attributed to one Package call. */
234
253
  export interface IsolateScheduleRequestV1 {
235
254
  callId: string;
@@ -303,6 +322,7 @@ export interface BotCapabilitiesStub {
303
322
  schedule(
304
323
  request: IsolateScheduleRequestV1,
305
324
  ): Promise<IsolateScheduleOutcomeV1>;
325
+ applets(request: IsolateAppletsRequestV1): Promise<IsolateAppletsOutcomeV1>;
306
326
  }
307
327
 
308
328
  /** The model outcome Bot-authored `package.js` receives after wrapper narrowing. */
@@ -363,6 +383,25 @@ export interface BotPackageContextV1 {
363
383
  request: IsolateWorkspaceDeleteRequestV1,
364
384
  ): Promise<IsolateWorkspaceOutcomeV1>;
365
385
  };
386
+ /**
387
+ * The User's Applets (ADR 0022). Account-wide: every Bot of this User sees
388
+ * every Applet. `publish` is a durable effect — it reads the built Applet
389
+ * from the Applets Package's durable root, verifies its manifest against the
390
+ * bytes, writes immutable artifacts, records the generation, mounts it, and
391
+ * proposes a new Composition generation for this Bot.
392
+ */
393
+ readonly applets: {
394
+ list(): Promise<IsolateAppletsOutcomeV1>;
395
+ create(input: { displayName: string }): Promise<IsolateAppletsOutcomeV1>;
396
+ publish(input: { appletId: string }): Promise<IsolateAppletsOutcomeV1>;
397
+ revert(input: {
398
+ appletId: string;
399
+ generationId: string;
400
+ }): Promise<IsolateAppletsOutcomeV1>;
401
+ delete(input: { appletId: string }): Promise<IsolateAppletsOutcomeV1>;
402
+ focus(input: { appletId: string | null }): Promise<IsolateAppletsOutcomeV1>;
403
+ generations(input: { appletId: string }): Promise<IsolateAppletsOutcomeV1>;
404
+ };
366
405
  connection(connectionId: string): Promise<IsolateConnectionOutcomeV1>;
367
406
  notify(
368
407
  request: IsolateNotificationRequestV1,
@@ -999,6 +1038,65 @@ export function decodeIsolateScheduleRequestV1(
999
1038
  };
1000
1039
  }
1001
1040
 
1041
+ const APPLET_CAPABILITY_ID = /^[a-z0-9][a-z0-9-]{0,63}\.[a-z0-9-]{1,64}$/;
1042
+
1043
+ export function decodeIsolateAppletsRequestV1(
1044
+ input: unknown,
1045
+ label = "isolate Applets request",
1046
+ ): IsolateAppletsRequestV1 {
1047
+ const value = record(input, label);
1048
+ const appletId = (): string => {
1049
+ const id = boundedString(value.appletId, `${label}.appletId`, 129);
1050
+ if (!APPLET_CAPABILITY_ID.test(id)) {
1051
+ throw new Error(`${label}.appletId is invalid`);
1052
+ }
1053
+ return id;
1054
+ };
1055
+ switch (value.op) {
1056
+ case "list":
1057
+ exactKeys(value, ["op"], label);
1058
+ return { op: "list" };
1059
+ case "create":
1060
+ exactKeys(value, ["op", "displayName"], label);
1061
+ return {
1062
+ op: "create",
1063
+ displayName: boundedString(
1064
+ value.displayName,
1065
+ `${label}.displayName`,
1066
+ 128,
1067
+ ),
1068
+ };
1069
+ case "publish":
1070
+ exactKeys(value, ["op", "appletId"], label);
1071
+ return { op: "publish", appletId: appletId() };
1072
+ case "revert":
1073
+ exactKeys(value, ["op", "appletId", "generationId"], label);
1074
+ return {
1075
+ op: "revert",
1076
+ appletId: appletId(),
1077
+ generationId: boundedString(
1078
+ value.generationId,
1079
+ `${label}.generationId`,
1080
+ 128,
1081
+ ),
1082
+ };
1083
+ case "delete":
1084
+ exactKeys(value, ["op", "appletId"], label);
1085
+ return { op: "delete", appletId: appletId() };
1086
+ case "focus":
1087
+ exactKeys(value, ["op", "appletId"], label);
1088
+ return {
1089
+ op: "focus",
1090
+ appletId: value.appletId === null ? null : appletId(),
1091
+ };
1092
+ case "generations":
1093
+ exactKeys(value, ["op", "appletId"], label);
1094
+ return { op: "generations", appletId: appletId() };
1095
+ default:
1096
+ throw new Error(`${label}.op is invalid`);
1097
+ }
1098
+ }
1099
+
1002
1100
  const MEMORY_SCOPES = ["bot", "user", "project"] as const;
1003
1101
  const MEMORY_TIERS = ["profile", "log", "note"] as const;
1004
1102
 
@@ -0,0 +1,145 @@
1
+ /**
2
+ * What a Turn the User's next message replaced looks like to the model request
3
+ * that follows it.
4
+ *
5
+ * The whole answer is in the durable log: `deriveMessages` walks the events and
6
+ * reads nothing else, so a superseded Turn contributes exactly what it made
7
+ * durable — what it said, and what its tools actually returned. There is no
8
+ * branch for supersede here, and that is the point: the next request is
9
+ * reconstructed from events, so a Turn that ended early is a Turn with fewer
10
+ * events, not a Turn with a special case.
11
+ */
12
+ import { describe, expect, test } from "bun:test";
13
+ import { Session } from "./session.js";
14
+ import type { SessionEvent } from "./types.js";
15
+
16
+ const timestamp = "2026-09-03T00:00:00.000Z";
17
+
18
+ function session(): Session {
19
+ return new Session("user-1:primary", () => {});
20
+ }
21
+
22
+ /**
23
+ * A Turn that sent a line, completed one tool call, and had a second still in
24
+ * flight when the User's next message arrived.
25
+ */
26
+ function supersededTurn(target: Session): void {
27
+ target.appendBatch([
28
+ { type: "turn/start", turn: 1 },
29
+ { type: "step/start", turn: 1, step: 1 },
30
+ { type: "user/message", turn: 1, step: 1, messageId: "m-1", text: "go" },
31
+ {
32
+ type: "assistant/message",
33
+ turn: 1,
34
+ step: 1,
35
+ requestId: "request-1",
36
+ text: "starting",
37
+ toolCalls: [
38
+ { id: "call-a", name: "read", input: {} },
39
+ { id: "call-b", name: "write", input: {} },
40
+ ],
41
+ },
42
+ {
43
+ type: "tool/call",
44
+ turn: 1,
45
+ step: 1,
46
+ occurrenceId: "tool:1:1:0",
47
+ name: "read",
48
+ input: {},
49
+ },
50
+ {
51
+ type: "tool/result",
52
+ turn: 1,
53
+ step: 1,
54
+ occurrenceId: "tool:1:1:0",
55
+ name: "read",
56
+ content: "the notes say hello",
57
+ isError: false,
58
+ status: "completed",
59
+ },
60
+ {
61
+ type: "tool/call",
62
+ turn: 1,
63
+ step: 1,
64
+ occurrenceId: "tool:1:1:1",
65
+ name: "write",
66
+ input: {},
67
+ },
68
+ ]);
69
+ }
70
+
71
+ describe("the next Turn's request derives from what the superseded Turn recorded", () => {
72
+ test("its sends and completed tool results are visible; the in-flight one is not claimed", () => {
73
+ const target = session();
74
+ supersededTurn(target);
75
+ // The interrupt: every unresolved effect is closed as interrupted, and the
76
+ // Turn is ended. This is the same repair a Stop makes.
77
+ target.reconcileInterrupted();
78
+
79
+ const messages = target.deriveMessages();
80
+
81
+ expect(messages).toEqual([
82
+ { role: "user", content: "go" },
83
+ {
84
+ role: "assistant",
85
+ content: "starting",
86
+ toolCalls: [
87
+ { id: "call-a", name: "read", input: {} },
88
+ { id: "call-b", name: "write", input: {} },
89
+ ],
90
+ },
91
+ {
92
+ role: "tool",
93
+ callId: "call-a",
94
+ name: "read",
95
+ content: "the notes say hello",
96
+ isError: false,
97
+ },
98
+ {
99
+ role: "tool",
100
+ callId: "call-b",
101
+ name: "write",
102
+ content: "Interrupted before a durable result was recorded.",
103
+ isError: true,
104
+ },
105
+ ]);
106
+ // The Turn ended, and the log says so. Nothing was invented for the effect
107
+ // that never returned: it is present, and it is an error.
108
+ expect(target.events.at(-1)).toMatchObject({
109
+ type: "turn/end",
110
+ outcome: "interrupted",
111
+ });
112
+ });
113
+
114
+ test("the new Turn's own message is simply the next one in the log", () => {
115
+ const target = session();
116
+ supersededTurn(target);
117
+ target.reconcileInterrupted();
118
+ target.appendBatch([
119
+ { type: "turn/start", turn: 2 },
120
+ { type: "step/start", turn: 2, step: 1 },
121
+ {
122
+ type: "user/message",
123
+ turn: 2,
124
+ step: 1,
125
+ messageId: "m-2",
126
+ text: "actually, do this instead",
127
+ },
128
+ ]);
129
+
130
+ const messages = target.deriveMessages();
131
+
132
+ expect(messages.at(-1)).toEqual({
133
+ role: "user",
134
+ content: "actually, do this instead",
135
+ });
136
+ // Deterministic: the same events derive the same request every time.
137
+ expect(target.deriveMessages()).toEqual(messages);
138
+ const replayed = new Session(
139
+ "user-1:primary",
140
+ () => {},
141
+ target.events as readonly SessionEvent[],
142
+ );
143
+ expect(replayed.deriveMessages()).toEqual(messages);
144
+ });
145
+ });
@@ -125,6 +125,12 @@ export type ToolEffectReconciliation =
125
125
  | { status: "unavailable"; reason: string };
126
126
 
127
127
  export interface ToolDefinition extends ToolSchema {
128
+ /**
129
+ * A dynamic Tool Namespace. Absent means the tool is native and its schema
130
+ * is offered to the model directly; present means the schema is disclosed
131
+ * and the tool is invoked only through the registry's two meta-tools.
132
+ */
133
+ namespace?: string;
128
134
  idempotent?: boolean;
129
135
  /** The turn types this tool is offered on. Absent means all of them. */
130
136
  admission?: TurnAdmissionV1;
@@ -185,10 +191,24 @@ export interface ToolRegistrationOptions {
185
191
  subagentRoleCeiling?: readonly string[];
186
192
  }
187
193
 
194
+ export interface ToolNamespaceRegistration {
195
+ name: string;
196
+ description?: string;
197
+ status?: "ready" | "needsAuth" | "error";
198
+ /** External namespaces require call metadata before an effect is admitted. */
199
+ external?: boolean;
200
+ /** Namespace-level instructions rendered in the dynamic catalog prompt. */
201
+ useInstructions?: string;
202
+ }
203
+
188
204
  /** Contributing Packages register tool definitions through this surface. */
189
205
  export interface ToolRegistration {
190
- /** Every registered name, before turn/role admission trims the catalog. */
206
+ /**
207
+ * Every registered identity, before turn/role admission trims the catalog.
208
+ * Native tools use their bare name; dynamic tools use `namespace/name`.
209
+ */
191
210
  registeredNames?(): string[];
211
+ registerNamespace(namespace: ToolNamespaceRegistration): () => void;
192
212
  register(
193
213
  definition: ToolDefinition,
194
214
  options?: ToolRegistrationOptions,
package/src/types.ts CHANGED
@@ -611,7 +611,10 @@ export interface SessionEventMap {
611
611
  *
612
612
  * `reason` is why the sync ran: `open` before the Turn's first Computer tool
613
613
  * call, `signal` when the on-Computer watcher reported a change mid-Turn,
614
- * `turn-end` after a Turn that used the Computer.
614
+ * `turn-end` after a Turn that used the Computer, and `publish` for the one
615
+ * sanctioned reconciliation outside that policy — a single declared root
616
+ * pulled and pushed so an Applet publish can read the artifact `applet
617
+ * build` left on the Computer (ADR 0022 decision 7).
615
618
  */
616
619
  /**
617
620
  * A background process on the Computer changed hands: it was launched,
@@ -640,7 +643,7 @@ export interface SessionEventMap {
640
643
  };
641
644
  "computer/sync": {
642
645
  turn: number;
643
- reason: "open" | "signal" | "turn-end";
646
+ reason: "open" | "signal" | "turn-end" | "publish";
644
647
  status: "ok" | "unavailable" | "refused" | "skipped";
645
648
  detail: string;
646
649
  pulled: number;
@@ -1875,7 +1878,11 @@ export function decodeSessionEvent(input: unknown): SessionEvent {
1875
1878
  "session event",
1876
1879
  );
1877
1880
  turn();
1878
- if (!["open", "signal", "turn-end"].includes(event.reason as string)) {
1881
+ if (
1882
+ !["open", "signal", "turn-end", "publish"].includes(
1883
+ event.reason as string,
1884
+ )
1885
+ ) {
1879
1886
  throw new Error("session event.reason is invalid");
1880
1887
  }
1881
1888
  if (