pi-roundtable 0.7.19 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -15
  3. package/README.zh-TW.md +65 -13
  4. package/docs/migrating-0.8.md +132 -0
  5. package/docs/migration-0.9.md +247 -0
  6. package/docs/plugins.md +853 -81
  7. package/examples/channel-context.test.ts +88 -0
  8. package/examples/channel-context.ts +62 -0
  9. package/examples/discord-adapter.test.ts +42 -0
  10. package/examples/discord-adapter.ts +26 -0
  11. package/examples/echo-runtime.test.ts +1 -0
  12. package/examples/echo-runtime.ts +1 -1
  13. package/examples/fake-surface.test.ts +69 -6
  14. package/examples/fake-surface.ts +73 -7
  15. package/examples/headless.test.ts +72 -0
  16. package/examples/headless.ts +35 -0
  17. package/examples/http.test.ts +31 -1
  18. package/examples/http.ts +39 -0
  19. package/examples/prechecks.test.ts +1 -0
  20. package/examples/principals.test.ts +60 -0
  21. package/examples/principals.ts +48 -0
  22. package/examples/prompt.test.ts +1 -1
  23. package/examples/study-room.test.ts +29 -1
  24. package/examples/study-room.ts +16 -19
  25. package/examples/support-desk.test.ts +3 -1
  26. package/examples/tools.test.ts +6 -1
  27. package/package.json +7 -3
  28. package/src/cli/checks/database.ts +11 -0
  29. package/src/cli/checks/identity.ts +225 -0
  30. package/src/cli/checks/model.ts +21 -3
  31. package/src/cli/cli.ts +130 -5
  32. package/src/cli/config-edit.ts +83 -13
  33. package/src/cli/doctor.ts +28 -5
  34. package/src/cli/init.ts +9 -1
  35. package/src/cli/line-diff.ts +72 -0
  36. package/src/cli/principal.ts +412 -0
  37. package/src/cli/project.ts +33 -2
  38. package/src/cli/report.ts +12 -3
  39. package/src/cli/templates.ts +46 -14
  40. package/src/cli/upgrade-rewrite.ts +102 -0
  41. package/src/cli/upgrade-source.ts +430 -0
  42. package/src/cli/upgrade.ts +176 -0
  43. package/src/core/agents/agent-claim.ts +56 -34
  44. package/src/core/agents/agent-team-fixture.ts +6 -0
  45. package/src/core/agents/team-turns.ts +10 -1
  46. package/src/core/builtin/agent-server.ts +38 -101
  47. package/src/core/builtin/discord-admin.ts +4 -1
  48. package/src/core/builtin/discord.ts +71 -13
  49. package/src/core/builtin/modules.ts +197 -43
  50. package/src/core/builtin/stores.ts +40 -21
  51. package/src/core/config/access.ts +229 -0
  52. package/src/core/config/adapters.ts +120 -0
  53. package/src/core/config/config.ts +228 -94
  54. package/src/core/contract/channel-context.ts +53 -0
  55. package/src/core/contract/channels.ts +81 -9
  56. package/src/core/contract/providers.ts +3 -2
  57. package/src/core/contract/runtime.ts +31 -11
  58. package/src/core/contract/surface.ts +35 -6
  59. package/src/core/conversations/conversation-registry.ts +56 -0
  60. package/src/core/conversations/conversation-store.ts +126 -0
  61. package/src/core/conversations/conversations-plugin.ts +29 -0
  62. package/src/core/define-roundtable.ts +157 -57
  63. package/src/core/discord/agent-panel.ts +2 -2
  64. package/src/core/discord/card-audience.ts +148 -0
  65. package/src/core/discord/channel-context.ts +348 -0
  66. package/src/core/discord/connection.ts +15 -3
  67. package/src/core/discord/direct-channel.ts +54 -0
  68. package/src/core/discord/discord-adapter.ts +9 -0
  69. package/src/core/discord/discord-owners.ts +133 -0
  70. package/src/core/discord/discord-surface.ts +69 -14
  71. package/src/core/discord/fresh-marker.ts +24 -0
  72. package/src/core/discord/inbound-message.ts +21 -7
  73. package/src/core/discord/interaction-module.ts +11 -1
  74. package/src/core/discord/owner-cards-fixture.ts +80 -0
  75. package/src/core/discord/owner-cards.ts +58 -86
  76. package/src/core/discord/owner-command.ts +27 -13
  77. package/src/core/discord/stop-button.ts +4 -4
  78. package/src/core/domain/conversation.ts +10 -0
  79. package/src/core/domain/errors.ts +5 -0
  80. package/src/core/domain/ports.ts +33 -2
  81. package/src/core/domain/progress.ts +10 -0
  82. package/src/core/events.ts +1 -0
  83. package/src/core/host.ts +32 -10
  84. package/src/core/http/listeners.ts +174 -21
  85. package/src/core/http/websocket.ts +316 -0
  86. package/src/core/i18n/cards.ts +11 -9
  87. package/src/core/identity/access-policy.ts +227 -0
  88. package/src/core/identity/actor-facts.ts +65 -0
  89. package/src/core/identity/caching-principal-store.ts +111 -0
  90. package/src/core/identity/config-owner.ts +145 -0
  91. package/src/core/identity/contact.ts +27 -0
  92. package/src/core/identity/identity-plugin.ts +56 -0
  93. package/src/core/identity/identity-schema.ts +282 -0
  94. package/src/core/identity/identity-service.ts +489 -0
  95. package/src/core/identity/identity-view.ts +77 -0
  96. package/src/core/identity/plugin-identities.ts +109 -0
  97. package/src/core/identity/principal-reads.ts +38 -0
  98. package/src/core/identity/principal-store.ts +478 -0
  99. package/src/core/identity/principal-tables.ts +53 -0
  100. package/src/core/identity/seen-throttle.ts +57 -0
  101. package/src/core/identity/ulid.ts +36 -0
  102. package/src/core/identity.ts +34 -5
  103. package/src/core/interactions/prompts.ts +127 -0
  104. package/src/core/modules/background/background-turns.ts +56 -17
  105. package/src/core/modules/background/personal-target.ts +50 -0
  106. package/src/core/modules/delegation/delegate.ts +28 -14
  107. package/src/core/modules/delegation/delegator.ts +23 -7
  108. package/src/core/modules/memory/owner-memory.ts +123 -50
  109. package/src/core/modules/notify/notify.ts +53 -13
  110. package/src/core/modules/notify/session-notify.ts +110 -0
  111. package/src/core/modules/schedules/prechecks.ts +8 -2
  112. package/src/core/modules/schedules/schedule-prechecks.ts +4 -1
  113. package/src/core/modules/schedules/schedule-store.ts +51 -20
  114. package/src/core/modules/schedules/schedule-tools.ts +122 -12
  115. package/src/core/modules/schedules/scheduler.ts +43 -2
  116. package/src/core/modules/schedules/schedules.ts +43 -14
  117. package/src/core/ops/error-reporter.ts +24 -12
  118. package/src/core/plugin.ts +62 -0
  119. package/src/core/presence/direct-channels.ts +104 -0
  120. package/src/core/registry/contributions.ts +63 -3
  121. package/src/core/registry/providers.ts +1 -1
  122. package/src/core/routing/channel-router.ts +226 -17
  123. package/src/core/routing/conversation-turns.ts +85 -1
  124. package/src/core/routing/surface-port.ts +35 -2
  125. package/src/core/routing/system-turns.ts +29 -0
  126. package/src/core/runtime/bridge-guard.ts +225 -0
  127. package/src/core/runtime/compaction-tiers.ts +5 -1
  128. package/src/core/runtime/conversation-sessions.ts +124 -12
  129. package/src/core/runtime/extensions/ask-user.ts +1 -1
  130. package/src/core/runtime/extensions/confirmation-gate.ts +32 -10
  131. package/src/core/runtime/extensions/private-memory.ts +447 -0
  132. package/src/core/runtime/extensions/self-compact-guard.ts +11 -0
  133. package/src/core/runtime/pending-confirmation-store.ts +67 -4
  134. package/src/core/runtime/pi-agent-runtime.ts +143 -135
  135. package/src/core/runtime/preflight-tools.ts +47 -0
  136. package/src/core/runtime/prompt-slot.ts +6 -6
  137. package/src/core/runtime/reader-history.ts +129 -0
  138. package/src/core/runtime/runtime-plugin.ts +205 -0
  139. package/src/core/runtime/runtime-types.ts +40 -12
  140. package/src/core/runtime/session-conversation.ts +143 -0
  141. package/src/core/runtime/session-factory.ts +115 -15
  142. package/src/core/runtime/session-scope.ts +78 -0
  143. package/src/core/runtime/steerable-run.ts +9 -0
  144. package/src/core/runtime/tool-call-scope.ts +43 -0
  145. package/src/core/runtime/turn-answer.ts +30 -1
  146. package/src/core/runtime/turn-progress.ts +116 -0
  147. package/src/core/services.ts +42 -2
  148. package/src/core/sessions.ts +30 -0
  149. package/src/core/shared/unix-server.ts +35 -5
  150. package/src/core/speakers.ts +21 -19
  151. package/src/core/testing/background.ts +31 -0
  152. package/src/core/testing/fixture-database.ts +55 -0
  153. package/src/core/testing/map-identity.ts +76 -0
  154. package/src/core/testing/module-sessions.ts +134 -0
  155. package/src/core/testing/modules.ts +149 -38
  156. package/src/core/testing/owner.ts +17 -0
  157. package/src/core/testing/surface-contract.ts +460 -0
  158. package/src/core/testing/test-host.ts +100 -19
  159. package/src/core/testing/tool-set.ts +32 -0
  160. package/src/core/tool-set.snapshot.json +4 -4
  161. package/src/core/tool-tiers.ts +57 -3
  162. package/src/discord/index.ts +10 -0
  163. package/src/index.ts +65 -8
  164. package/src/kit/compaction.ts +13 -0
  165. package/src/kit/index.ts +10 -0
  166. package/src/kit/support.ts +1 -1
  167. package/src/testing.ts +96 -21
  168. package/templates/.env.example +1 -1
  169. package/templates/adapters/web/.env.example +24 -0
  170. package/templates/adapters/web/README.md +22 -0
  171. package/templates/adapters/web/package.json.tmpl +26 -0
  172. package/templates/adapters/web/persona/assistant.md +2 -0
  173. package/templates/adapters/web/roundtable.config.ts +59 -0
  174. package/templates/package.json.tmpl +1 -0
  175. package/templates/plugins/self-compact.ts +11 -0
  176. package/templates/roundtable.config.ts +14 -2
  177. package/src/core/domain/owner-prompts.ts +0 -45
@@ -1,18 +1,30 @@
1
1
  import { mkdirSync } from "node:fs";
2
- import { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
3
3
  import { ownerAttachmentDir } from "../attachments/attachment-dir.ts";
4
4
  import type {
5
5
  ChannelKey,
6
6
  PendingConfirmation,
7
7
  } from "../domain/conversation.ts";
8
- import type { TurnRequest } from "../domain/ports.ts";
8
+ import type { TurnConversation, TurnRequest } from "../domain/ports.ts";
9
9
  import type { ThinkingLevel } from "../models.ts";
10
10
  import type { ToolTiers } from "../tool-tiers.ts";
11
- import { ConfirmationGate } from "./extensions/confirmation-gate.ts";
11
+ import { ConfirmationGate, unexpired } from "./extensions/confirmation-gate.ts";
12
12
  import { PromptSlot } from "./prompt-slot.ts";
13
13
  import type { ChannelSession, PiAgentRuntimeOptions } from "./runtime-types.ts";
14
14
  import { revisionsKey, skillsKey } from "./runtime-types.ts";
15
+ import {
16
+ conversationChanged,
17
+ namedConversation,
18
+ sessionConversation,
19
+ } from "./session-conversation.ts";
15
20
  import type { SessionFactory } from "./session-factory.ts";
21
+ import {
22
+ historyMessages,
23
+ historyScope,
24
+ scopedHistory,
25
+ } from "./session-scope.ts";
26
+
27
+ type AgentMessage = AgentSession["messages"][number];
16
28
 
17
29
  /** The open sessions of a runtime's conversations and what is held for each of them. */
18
30
  export class ConversationSessions {
@@ -21,6 +33,11 @@ export class ConversationSessions {
21
33
  readonly #tiers: ToolTiers;
22
34
  readonly #sessions = new Map<ChannelKey, Promise<ChannelSession>>();
23
35
  readonly #gates = new Map<ChannelKey, ConfirmationGate>();
36
+ /**
37
+ * What the store held for a conversation that has no gate yet, once read: a host asks what is
38
+ * pending before the conversation's first turn after a restart, which builds its gate.
39
+ */
40
+ readonly #restored = new Map<ChannelKey, PendingConfirmation | undefined>();
24
41
  /** Each conversation's way to ask the owner during its current turn. */
25
42
  readonly #slots = new Map<ChannelKey, PromptSlot>();
26
43
  /** Each conversation's level the judge picked for its last turn, kept while the judge is unsure. */
@@ -41,9 +58,24 @@ export class ConversationSessions {
41
58
  this.#tiers = tiers;
42
59
  }
43
60
 
44
- /** The held actions of a conversation whose gate is open. */
61
+ /** The held actions of a conversation whose gate is open, or that `held` has restored from the store. */
45
62
  pending(channel: ChannelKey): PendingConfirmation | undefined {
46
- return this.#gates.get(channel)?.pending();
63
+ const gate = this.#gates.get(channel);
64
+ return gate ? gate.pending() : unexpired(this.#restored.get(channel));
65
+ }
66
+
67
+ /**
68
+ * The conversation's held actions, restored from the store the first time they are asked for
69
+ * after a restart. Builds neither a gate nor a session: a gate's flavour follows the turn that
70
+ * first needs it, an agent's or not, and only the turn knows which.
71
+ */
72
+ async held(key: ChannelKey): Promise<PendingConfirmation | undefined> {
73
+ if (!this.#gates.has(key) && !this.#restored.has(key)) {
74
+ const stored = await this.#options.confirmations.load(key);
75
+ // A turn may have built the gate while the store was read; its held actions are the later ones.
76
+ if (!this.#gates.has(key)) this.#restored.set(key, stored);
77
+ }
78
+ return this.pending(key);
47
79
  }
48
80
 
49
81
  /** Disposes every open session. */
@@ -77,6 +109,7 @@ export class ConversationSessions {
77
109
  this.#tiers,
78
110
  );
79
111
  this.#gates.set(key, gate);
112
+ this.#restored.delete(key);
80
113
  }
81
114
  return gate;
82
115
  }
@@ -96,7 +129,7 @@ export class ConversationSessions {
96
129
  */
97
130
  session(
98
131
  key: ChannelKey,
99
- request?: Pick<TurnRequest, "channel" | "agent" | "kind">,
132
+ request?: Pick<TurnRequest, "channel" | "agent" | "kind" | "conversation">,
100
133
  ): Promise<ChannelSession> {
101
134
  let pending = this.#sessions.get(key);
102
135
  if (!pending) {
@@ -104,28 +137,87 @@ export class ConversationSessions {
104
137
  mkdirSync(sessionDir, { recursive: true });
105
138
  const channel = request?.channel ?? key;
106
139
  const agent = request?.agent;
107
- pending = (async () =>
108
- this.#factory.create(
140
+ pending = (async () => {
141
+ const conversation = await sessionConversation(
142
+ key,
143
+ request,
144
+ this.#options,
145
+ );
146
+ // A history of another scope is archived, never replayed, and what it held goes with it,
147
+ // after a restart too: no one approves, or reads, an action held for someone else.
148
+ const { history, archived } = scopedHistory(
149
+ sessionDir,
150
+ this.#factory.cwd(agent),
151
+ conversation,
152
+ );
153
+ if (archived > 0) {
154
+ this.#options.logger.info(
155
+ { channel: key, archived },
156
+ "the conversation serves someone else now; its earlier history was archived",
157
+ );
158
+ this.#gates.delete(key);
159
+ this.#restored.delete(key);
160
+ await this.#options.confirmations.save(key, undefined);
161
+ }
162
+ return this.#factory.create(
109
163
  agent?.home ?? channel,
110
- SessionManager.continueRecent(this.#factory.cwd(agent), sessionDir),
164
+ history,
111
165
  await this.gate(key, agent !== undefined),
112
166
  this.slot(key),
113
167
  ownerAttachmentDir(this.#options.dataDir, channel),
114
168
  agent,
115
169
  request?.kind ?? "owner",
116
- ))();
170
+ conversation,
171
+ );
172
+ })();
117
173
  pending.catch(() => this.#sessions.delete(key));
118
174
  this.#sessions.set(key, pending);
119
175
  }
120
176
  return pending;
121
177
  }
122
178
 
123
- /** The conversation's session, rebuilt with its history when its session tools or skills changed. */
179
+ /**
180
+ * Whom the conversation serves at this turn: as the turn names it, else as the host records it
181
+ * now, read at every turn so a record changed while the session is open counts; else as its open
182
+ * session was made for, or its history records, so a conversation nothing names carries on as it
183
+ * was, after a restart too; else shared.
184
+ */
185
+ async conversation(
186
+ key: ChannelKey,
187
+ request: Pick<TurnRequest, "agent" | "conversation">,
188
+ ): Promise<TurnConversation> {
189
+ const named = await namedConversation(key, request, this.#options);
190
+ if (named) return named;
191
+ const open = this.#sessions.get(key);
192
+ if (open) return (await open).conversation;
193
+ return (
194
+ historyScope(
195
+ this.#factory.sessionDir(key),
196
+ this.#factory.cwd(request.agent),
197
+ ) ?? { visibility: "shared" }
198
+ );
199
+ }
200
+
201
+ /**
202
+ * The conversation's session for the turn: rebuilt with its history when its session tools or
203
+ * skills changed, and made anew, with nothing held for it, when the conversation serves someone
204
+ * else than it was made for, its history archived rather than replayed.
205
+ */
124
206
  async freshSession(
125
207
  key: ChannelKey,
126
208
  request: TurnRequest,
127
209
  ): Promise<ChannelSession> {
128
210
  const channelSession = await this.session(key, request);
211
+ if (
212
+ conversationChanged(channelSession.conversation, request.conversation)
213
+ ) {
214
+ this.#options.logger.info(
215
+ { channel: key },
216
+ "the conversation's scope changed; starting an isolated session",
217
+ );
218
+ await this.forget(key);
219
+ return this.session(key, request);
220
+ }
129
221
  const stale = this.#staleReason(channelSession, request);
130
222
  if (!stale) return channelSession;
131
223
  this.#options.logger.info(
@@ -137,7 +229,7 @@ export class ConversationSessions {
137
229
  return this.session(key, request);
138
230
  }
139
231
 
140
- /** Why a cached session must be rebuilt before this turn, or undefined when it is current. */
232
+ /** Why a cached session must be rebuilt, keeping its history, before this turn; undefined when it is current. */
141
233
  #staleReason(
142
234
  channelSession: ChannelSession,
143
235
  request: TurnRequest,
@@ -151,6 +243,25 @@ export class ConversationSessions {
151
243
  return undefined;
152
244
  }
153
245
 
246
+ /**
247
+ * The messages of a conversation's history: its open session's, or else read from its files
248
+ * without opening one, so reading them fixes no scope; none when they were recorded, or its open
249
+ * session was made, for someone else than the conversation serves now.
250
+ */
251
+ async messages(key: ChannelKey): Promise<readonly AgentMessage[]> {
252
+ const now = await this.conversation(key, {});
253
+ const open = this.#sessions.get(key);
254
+ if (open) {
255
+ const { conversation, session } = await open;
256
+ return conversationChanged(conversation, now) ? [] : session.messages;
257
+ }
258
+ return historyMessages(
259
+ this.#factory.sessionDir(key),
260
+ this.#factory.cwd(undefined),
261
+ now,
262
+ );
263
+ }
264
+
154
265
  /** Drops the open session and everything held for the conversation, leaving its files. */
155
266
  // pi-lens-ignore: mixed-async-styles — disposal failures of a dropped session are swallowed in a chain on purpose
156
267
  async forget(channel: ChannelKey): Promise<void> {
@@ -160,6 +271,7 @@ export class ConversationSessions {
160
271
  ?.then(({ session }) => session.dispose())
161
272
  .catch(() => undefined);
162
273
  this.#gates.delete(channel);
274
+ this.#restored.delete(channel);
163
275
  this.#slots.delete(channel);
164
276
  this.usage.delete(channel);
165
277
  this.judged.delete(channel);
@@ -1,8 +1,8 @@
1
1
  import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import type { OwnerAnswer, OwnerQuestion } from "../../domain/owner-prompts.ts";
4
3
  import { messages } from "../../i18n/index.ts";
5
4
  import { type OwnerIdentity, ownerWords } from "../../identity.ts";
5
+ import type { OwnerAnswer, OwnerQuestion } from "../../interactions/prompts.ts";
6
6
  import { toolText } from "../../shared/tool-result.ts";
7
7
  import type { PromptSlot } from "../prompt-slot.ts";
8
8
 
@@ -3,10 +3,11 @@ import type {
3
3
  HeldCall,
4
4
  PendingConfirmation,
5
5
  } from "../../domain/conversation.ts";
6
- import type { OwnerPrompts } from "../../domain/owner-prompts.ts";
7
6
  import { type HoldCheck, type HoldContext, higherTier } from "../../holds.ts";
8
7
  import { messages } from "../../i18n/index.ts";
9
8
  import { type OwnerIdentity, ownerWords } from "../../identity.ts";
9
+ import type { Prompts } from "../../interactions/prompts.ts";
10
+ import type { Speaker } from "../../speakers.ts";
10
11
  import type { ToolTiers } from "../../tool-tiers.ts";
11
12
  import type { PromptSlot } from "../prompt-slot.ts";
12
13
 
@@ -16,6 +17,17 @@ const CARD_INPUT_CHARS = 1_500;
16
17
  /** Held actions older than this cannot be approved any more. */
17
18
  export const CONFIRMATION_TTL_MS = 24 * 3_600_000;
18
19
 
20
+ /** The held actions when they can still be approved at `now`; expired ones count as none. */
21
+ export function unexpired(
22
+ pending: PendingConfirmation | undefined,
23
+ now = new Date(),
24
+ ): PendingConfirmation | undefined {
25
+ if (!pending) return undefined;
26
+ return now.getTime() - pending.heldAt.getTime() > CONFIRMATION_TTL_MS
27
+ ? undefined
28
+ : pending;
29
+ }
30
+
19
31
  /** JSON with object keys sorted at every level, so equal inputs compare equal. */
20
32
  // pi-lens-ignore: no-unknown-parameters — tool input is untyped model JSON; this is where it is read
21
33
  export function canonicalJson(value: unknown): string {
@@ -54,6 +66,8 @@ function byCodeUnit(a: string, b: string): number {
54
66
  // pi-lens-ignore: large-class — one gate per conversation; its state is the held calls and their approvals
55
67
  export class ConfirmationGate {
56
68
  #selectionId: string | undefined;
69
+ /** The running turn's speaker, whom the actions it holds belong to. */
70
+ #speaker: Pick<Speaker, "id" | "principalId"> | undefined;
57
71
  #pending: PendingConfirmation | undefined;
58
72
  #approved: HeldCall[] = [];
59
73
  readonly #holds: HoldCheck;
@@ -84,20 +98,22 @@ export class ConfirmationGate {
84
98
 
85
99
  /** Held actions awaiting the owner's answer; expired ones count as none. */
86
100
  pending(now = new Date()): PendingConfirmation | undefined {
87
- const pending = this.#pending;
88
- if (!pending) return undefined;
89
- return now.getTime() - pending.heldAt.getTime() > CONFIRMATION_TTL_MS
90
- ? undefined
91
- : pending;
101
+ return unexpired(this.#pending, now);
92
102
  }
93
103
 
94
- /** `addressee` is who the turn is for; without one, the owner. */
104
+ /**
105
+ * `addressee` is who the turn is for; without one, the owner. `speaker` is who spoke the turn,
106
+ * recorded with what it holds by their id and principal; without one, only the owners may
107
+ * approve them.
108
+ */
95
109
  beginTurn(
96
110
  selectionId: string,
97
111
  confirmed: boolean,
98
112
  addressee?: OwnerIdentity,
113
+ speaker?: Pick<Speaker, "id" | "principalId">,
99
114
  ): void {
100
115
  this.#selectionId = selectionId;
116
+ this.#speaker = speaker;
101
117
  this.#addressee = addressee ?? this.#owner;
102
118
  this.#approved = confirmed ? [...(this.pending()?.calls ?? [])] : [];
103
119
  this.#pending = undefined;
@@ -120,7 +136,7 @@ export class ConfirmationGate {
120
136
  async review(
121
137
  tool: string,
122
138
  input: Record<string, unknown>,
123
- ask?: { prompts: OwnerPrompts; asker: string; signal?: AbortSignal },
139
+ ask?: { prompts: Prompts; asker: string; signal?: AbortSignal },
124
140
  ): Promise<string | undefined> {
125
141
  return (await this.decide(tool, input, ask)).reason;
126
142
  }
@@ -132,7 +148,7 @@ export class ConfirmationGate {
132
148
  async decide(
133
149
  tool: string,
134
150
  input: Record<string, unknown>,
135
- ask?: { prompts: OwnerPrompts; asker: string; signal?: AbortSignal },
151
+ ask?: { prompts: Prompts; asker: string; signal?: AbortSignal },
136
152
  ): Promise<{ reason?: string; approvedOnCard?: true }> {
137
153
  const call = this.#needing(tool, input);
138
154
  if (!call) return {};
@@ -180,10 +196,16 @@ export class ConfirmationGate {
180
196
  const selectionId = this.#selectionId;
181
197
  if (selectionId === undefined)
182
198
  throw new Error("a call was held outside a turn");
183
- const pending = this.#pending ?? {
199
+ const pending: PendingConfirmation = this.#pending ?? {
184
200
  selectionId,
185
201
  heldAt: new Date(),
186
202
  calls: [],
203
+ ...(this.#speaker
204
+ ? {
205
+ speakerId: this.#speaker.id,
206
+ principalId: this.#speaker.principalId,
207
+ }
208
+ : {}),
187
209
  };
188
210
  if (
189
211
  !pending.calls.some((c) => c.tool === call.tool && c.input === call.input)