pi-roundtable 0.15.3 → 0.15.4
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 +6 -0
- package/docs/plugins.md +1 -1
- package/package.json +3 -3
- package/src/core/agents/agent-claim.ts +18 -15
- package/src/core/contract/channels.ts +11 -1
- package/src/core/routing/channel-router.ts +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.15.4] - 2026-10-15
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- 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.
|
|
13
|
+
|
|
8
14
|
## [0.15.3] - 2026-10-15
|
|
9
15
|
|
|
10
16
|
### 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
|
+
"version": "0.15.4",
|
|
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.
|
|
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.
|
|
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
|
-
|
|
223
|
-
|
|
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,
|
|
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
|
|
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 {
|