pi-roundtable 0.15.3 → 0.16.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/CHANGELOG.md CHANGED
@@ -5,6 +5,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.16.0] - 2026-10-16
9
+
10
+ ### Changed
11
+
12
+ - Release in lockstep with pi-roundtable-line 0.16.0, which adds a webhook screen, LINE's API through the plugin and `senderNameProblem`; the core has no behavior change. Every package's core peer range moves to `>=0.16.0 <0.17.0`.
13
+
14
+ ## [0.15.4] - 2026-10-15
15
+
16
+ ### Changed
17
+
18
+ - With `discord.agentSpeakers: "owners"`, a schedule whose turn would run below the owner tier no longer starts its precheck or its container: the scheduler asks the channel's claim before the precheck, and the run is recorded `skipped:` with the reason the turn would have been skipped with (`the agent server answers owners only (speakers: "owners")`). A new optional `ChannelClaim.backgroundRefusal(turn)` answers that for a claim, with the turn and the speaker the router checked; `ConversationPort.runsAs` and `ChannelRouter.runsAs` now include it, and the agent server's claim uses it for its own `background` skips (the owner target only, no groups, owners only). A claim without it is unaffected.
19
+
8
20
  ## [0.15.3] - 2026-10-15
9
21
 
10
22
  ### Added
package/docs/plugins.md CHANGED
@@ -1238,7 +1238,7 @@ pi-roundtable-sandbox's `precheckScriptRunner` runs each script in a sealed cont
1238
1238
  - `schedule_create` and `schedule_update` take `precheck_script`, a JavaScript module of at most `PRECHECK_SCRIPT_CHARS` (8,000) characters with a default export; it is parsed, never run, when it is set. A schedule has a `precheck` or a `precheck_script`; setting one removes the other, and `null` removes either.
1239
1239
  - The runner's `run(script, context)` gets the `PrecheckScriptContext`: the schedule, `firedAt`, `tier`, `signal`, the host's `timeZone`, and `today`, the date there. `tier` is the tier the run is for: the schedule's, capped at what its creator holds now, as its turn would run. Its answer is checked like a named precheck's, and its finding is named `script`.
1240
1240
  - `describe({ channel, target, tier })` (a `PrecheckScope`) tells the model how to write one and what it may call there; `schedule_list` shows it, waiting at most 10 seconds. `tier` is the asker's there and the run's `tier` when it runs, so a runner may grant lower tiers less.
1241
- - Before a schedule's precheck, named or a script, the scheduler checks who its turn would run as, as `ConversationPort.runsAs` does: a creator who may not run it now, such as one disabled, runs no precheck, and the run is recorded `skipped:` with the reason.
1241
+ - Before a schedule's precheck, named or a script, the scheduler checks who its turn would run as, as `ConversationPort.runsAs` does: a creator who may not run it now, such as one disabled, runs no precheck, and the run is recorded `skipped:` with the reason. The claim that owns the channel is asked too (`ChannelClaim.backgroundRefusal`): with `discord.agentSpeakers: "owners"`, a schedule whose turn would run below the owner tier starts no precheck and no container, and is recorded with the same reason its turn would have been skipped with.
1242
1242
  - When the host stops, the scheduler aborts running scripts and waits up to 15 seconds for the runner to clean up; a precheck that ends then starts no turn.
1243
1243
  - Without a runner, the tools neither take nor mention `precheck_script`, a script is refused with the registered names, and a schedule that already has one wakes with `### Precheck failed: script`, never a silent skip.
1244
1244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable",
3
- "version": "0.15.3",
3
+ "version": "0.16.0",
4
4
  "description": "A plugin-driven Pi agent server for one owner, reached through Discord or the web",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -65,7 +65,7 @@
65
65
  "canvas": "3.2.3",
66
66
  "discord.js": "14.27.0",
67
67
  "pi-jev-compaction": "1.0.0",
68
- "pi-mcp-adapter": "5.0.0",
68
+ "pi-mcp-adapter": "5.2.0",
69
69
  "pino": "10.4.0",
70
70
  "typebox": "1.3.34",
71
71
  "unpdf": "1.8.1"
@@ -78,6 +78,6 @@
78
78
  "@biomejs/biome": "2.5.15",
79
79
  "@types/bun": "1.4.2",
80
80
  "pi-self-compact": "0.2.0",
81
- "pi-web-access": "0.35.0"
81
+ "pi-web-access": "0.39.0"
82
82
  }
83
83
  }
@@ -7,6 +7,7 @@ import {
7
7
  import type {
8
8
  Admission,
9
9
  BackgroundTarget,
10
+ BackgroundTurn,
10
11
  ChannelClaim,
11
12
  InboundMessage,
12
13
  } from "../contract/channels.ts";
@@ -19,7 +20,7 @@ import { PERSONAL_TARGET } from "../modules/background/personal-target.ts";
19
20
  import { withReference } from "../routing/message-text.ts";
20
21
  import { outcome } from "../routing/settle-turn.ts";
21
22
  import { systemTurn } from "../routing/system-turns.ts";
22
- import { addressee, attributed } from "../speakers.ts";
23
+ import { addressee, attributed, type Speaker } from "../speakers.ts";
23
24
  import type { DiscordAgentTeam } from "./agent-team.ts";
24
25
  import { discordIdOf } from "./team-keys.ts";
25
26
 
@@ -104,6 +105,19 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
104
105
  }
105
106
  };
106
107
 
108
+ /** Why the agent server would skip a background turn for its checked speaker; the turn's own skips. */
109
+ const backgroundRefusal = (
110
+ turn: BackgroundTurn & { speaker: Speaker },
111
+ ): string | undefined => {
112
+ // Fail closed: a turn for any target but the owner's never reaches the owner's tools.
113
+ if (turn.target !== PERSONAL_TARGET.name)
114
+ return `the agent server answers only "${PERSONAL_TARGET.name}" background turns`;
115
+ if (team.owns(turn.channel) === "group") return "groups have no schedules";
116
+ if (options.speakers === "owners" && turn.speaker.tier !== "owner")
117
+ return 'the agent server answers owners only (speakers: "owners")';
118
+ return undefined;
119
+ };
120
+
107
121
  const webhookReport = (message: InboundMessage): Admission | undefined => {
108
122
  const text = message.text.trim();
109
123
  if (!text) return undefined;
@@ -201,16 +215,8 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
201
215
  failure: "agent message handling failed",
202
216
  };
203
217
  },
218
+ backgroundRefusal,
204
219
  background: async (turn) => {
205
- // Fail closed: a turn for any target but the owner's never reaches the owner's tools.
206
- if (turn.target !== PERSONAL_TARGET.name)
207
- return {
208
- status: "skipped",
209
- reason: `the agent server answers only "${PERSONAL_TARGET.name}" background turns`,
210
- };
211
- const owned = team.owns(turn.channel);
212
- if (owned === "group")
213
- return { status: "skipped", reason: "groups have no schedules" };
214
220
  // Who the turn runs as, checked by the router: its author's principal, at a tier they hold.
215
221
  const { speaker } = turn;
216
222
  if (!speaker)
@@ -219,11 +225,8 @@ export function agentClaim(options: AgentClaimOptions): ChannelClaim {
219
225
  reason:
220
226
  "a background turn reaches the agent server through the router, which says whom it runs as",
221
227
  };
222
- if (options.speakers === "owners" && speaker.tier !== "owner")
223
- return {
224
- status: "skipped",
225
- reason: 'the agent server answers owners only (speakers: "owners")',
226
- };
228
+ const reason = backgroundRefusal({ ...turn, speaker });
229
+ if (reason !== undefined) return { status: "skipped", reason };
227
230
  return outcome(
228
231
  await team.answerBackground(
229
232
  turn.channel,
@@ -197,6 +197,15 @@ export interface ChannelClaim {
197
197
  admit(message: InboundMessage): Admission | undefined;
198
198
  /** A background turn in an owned channel; a claim without one skips them. */
199
199
  background?(turn: BackgroundTurn): Promise<ScheduledOutcome>;
200
+ /**
201
+ * Why `background` would skip the turn for its checked speaker, without running it; undefined
202
+ * when it would not. The router asks it in `runsAs`, so work that must not happen for a turn
203
+ * the claim will skip, such as a schedule's precheck, does not start. It decides from the turn
204
+ * and its speaker alone, and its reasons are the ones `background` skips with.
205
+ */
206
+ backgroundRefusal?(
207
+ turn: BackgroundTurn & { speaker: Speaker },
208
+ ): string | undefined;
200
209
  /** Starts the channel's conversation over, saying whose it was. */
201
210
  startFresh(channel: ChannelKey): Promise<string>;
202
211
  /**
@@ -230,7 +239,8 @@ export interface ConversationPort {
230
239
  background(turn: BackgroundTurn): Promise<ScheduledOutcome>;
231
240
  /**
232
241
  * Who a background turn would run as now, checked as `background` checks it, without running
233
- * it: for work that must not happen for an author who may not run the turn, such as a
242
+ * it: for work that must not happen for an author who may not run the turn, or one the channel's
243
+ * claim would skip for that speaker (an agent server that answers owners only), such as a
234
244
  * schedule's precheck. Throws when the identity service cannot tell.
235
245
  */
236
246
  runsAs(turn: BackgroundTurn): Promise<BackgroundRunsAs>;
@@ -336,10 +336,23 @@ export class ChannelRouter implements ConversationPort {
336
336
  /**
337
337
  * Who a background turn runs as: the system principal for the host's own turn, at its tier;
338
338
  * otherwise its author's principal, which must exist, be enabled, and hold a tier now, at the
339
- * lower of the turn's tier and theirs. Why not, when it may not run; throws when the identity
339
+ * lower of the turn's tier and theirs; and the claim that owns the channel must not refuse it
340
+ * for that speaker (`backgroundRefusal`). Why not, when it may not run; throws when the identity
340
341
  * service cannot tell.
341
342
  */
342
343
  async runsAs(turn: BackgroundTurn): Promise<BackgroundRunsAs> {
344
+ const runs = await this.#speakerOf(turn);
345
+ if ("skipped" in runs) return runs;
346
+ // The claim that owns the channel has the last word, so work that must not start for a turn it
347
+ // would skip, such as a schedule's precheck, does not.
348
+ const refusal = this.#owner(turn.channel)?.backgroundRefusal?.({
349
+ ...turn,
350
+ speaker: runs.speaker,
351
+ });
352
+ return refusal === undefined ? runs : { skipped: refusal };
353
+ }
354
+
355
+ async #speakerOf(turn: BackgroundTurn): Promise<BackgroundRunsAs> {
343
356
  const { author, tier } = turn;
344
357
  if (!author?.principalId || !tier)
345
358
  return {