pi-roundtable 0.9.5 → 0.9.6

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,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.9.6] - 2026-10-10
9
+
10
+ ### Fixed
11
+
12
+ - The compaction tiers measure the context a compaction left as a request sends it, system prompt and tool definitions included: the first request after the compaction reports it, and until then the conversation's estimate gets the fixed part the last request before it carried (what it sent less the estimate of its messages). They measured the conversation alone, so a session with a large fixed part (over 100k of tools) that compacted to about 270k kept its next compaction at 300k and compacted again every few tool results. `LatestCompaction` adds `measured`, and the `conversation compacted` log adds `contextAfter`, the measure the tiers use, beside `tokensAfter`.
13
+
8
14
  ## [0.9.5] - 2026-10-10
9
15
 
10
16
  0.9.4 was tagged but never published (its release checks failed on the recorded prompt texts); everything listed under it is released in 0.9.5.
package/docs/plugins.md CHANGED
@@ -68,7 +68,7 @@ Use the kit's building blocks for a plugin that runs Pi itself, such as a coding
68
68
  - Tools: `textToolsExtension`, `requiredString`, `stringList` (with `toolText` and `toolError`) for tools that return text.
69
69
  - Mirroring a built-in tool in a worker that cannot reach the host: `SCHEDULE_TOOLS`, `scheduleToolSpecs({ locale, timeZone })`, `isScheduleTool`, `callScheduleTool`, `DELEGATE_TOOL` and `DELEGATE_TOOL_SPEC`.
70
70
  The specs take the locale and time zone for their descriptions, so the worker needs no process-wide setting.
71
- - Compaction: `CompactionTiers` gives a session the core's compaction tiers (`settings()` for Pi's `SettingsManager`, `wrapCompactor(factory, onBypass, prepare?)` to hold a compaction extension back past the ceiling and change what it is given first, `latest()`), with `SOFT_COMPACT_TOKENS` (300,000), `HARD_COMPACT_TOKENS` (500,000), `COMPACT_HEADROOM_TOKENS` (50,000), `compactionEngine(details, engine)` and the types `CompactionEngine`, `CompactionHistory` and `LatestCompaction`. `summaryProjection(messages, history?)` is what a shared conversation's compaction may summarize, without anyone's private memory, prompt states, or reasoning, and `privateCompaction(preparation, additionalMessages?)` sets a `session_before_compact` preparation to it. Optional surrounding history pairs calls/results split across summary, prefix and kept context. Independent shared sessions use `memoryProjection(messages, view)` on requests and `hidesPrivateExchange(messages, view)` for raw replay guards (`MemoryView`: `shared` and `reader`; legacy attribution is internal to the bridge guard, not a projection option). `recordMemoryTurn(manager, reader, privateMemory)` appends `MEMORY_TURN_ENTRY` (`roundtable-memory-turn`) before prompting and returns a marker for memory first used later in that turn; `carriesMemory(message)` recognizes built-in memory calls/results and custom `privateTo` results, including immediate failures and unanswered calls. `bridgeHistoryHidesMemory(rawBranch, reader)` checks those records and raw exchanges across compaction; SYSTEM is a reader of its own there, and the core's SYSTEM-as-primary-owner attribution and its pre-scope legacy attribution stay inside the core.
71
+ - Compaction: `CompactionTiers` gives a session the core's compaction tiers (`settings()` for Pi's `SettingsManager`, `wrapCompactor(factory, onBypass, prepare?)` to hold a compaction extension back past the ceiling and change what it is given first, `latest()`: the latest compaction's `contextTokens`, the context it left as a request sends it, system prompt and tools included, `measured` once a request after it reported its usage), with `SOFT_COMPACT_TOKENS` (300,000), `HARD_COMPACT_TOKENS` (500,000), `COMPACT_HEADROOM_TOKENS` (50,000), `compactionEngine(details, engine)` and the types `CompactionEngine`, `CompactionHistory` and `LatestCompaction`. `summaryProjection(messages, history?)` is what a shared conversation's compaction may summarize, without anyone's private memory, prompt states, or reasoning, and `privateCompaction(preparation, additionalMessages?)` sets a `session_before_compact` preparation to it. Optional surrounding history pairs calls/results split across summary, prefix and kept context. Independent shared sessions use `memoryProjection(messages, view)` on requests and `hidesPrivateExchange(messages, view)` for raw replay guards (`MemoryView`: `shared` and `reader`; legacy attribution is internal to the bridge guard, not a projection option). `recordMemoryTurn(manager, reader, privateMemory)` appends `MEMORY_TURN_ENTRY` (`roundtable-memory-turn`) before prompting and returns a marker for memory first used later in that turn; `carriesMemory(message)` recognizes built-in memory calls/results and custom `privateTo` results, including immediate failures and unanswered calls. `bridgeHistoryHidesMemory(rawBranch, reader)` checks those records and raw exchanges across compaction; SYSTEM is a reader of its own there, and the core's SYSTEM-as-primary-owner attribution and its pre-scope legacy attribution stay inside the core.
72
72
  - Jev compaction: `jevCompact(input, options)` compacts through Jev (pi-jev-compaction 1.0.0) and returns `{ compaction }` or `{ skipped, detail? }` (a `JevSkipReason`: pi-jev-compaction's fallbacks `no_key`, `aborted`, `nothing_to_compact`, `no_candidates`, `cannot_fit`, `jev_error`, `reduction_too_small`, or `previous_summary_too_large`), for Pi's own summary to run instead.
73
73
  Its summary carries the previous summary once, as the transcript's `[previous compaction]` message, with `estimatedTokensAfter` measured on the final text; a previous summary over `previousSummaryLimitTokens` (default `JEV_PREVIOUS_SUMMARY_LIMIT_TOKENS`, 60,000) skips without calling Jev, so Pi's summary condenses the chain.
74
74
  Jev judges with `goal` (default `JEV_GOAL`: keep the tool results that set rules still in force, drop stale lookups, listings and finished edits), and the summary ends with a `## Rules loaded before this compaction` section naming each rule load among the summarized messages (tool name and JSON arguments cut at 200 characters) for the agent to load again; `ruleLoad(tool, args)` picks them (default `isRuleLoad`: tools ending in `invoke-skill` or `get-system-prompt`, and tools ending in `read` whose `path` is a `SKILL.md`, an `AGENTS.md` or under `.agents/skills/`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-roundtable",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
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",
@@ -1,9 +1,12 @@
1
1
  import {
2
+ buildSessionProjection,
2
3
  type ExtensionAPI,
3
4
  type ExtensionFactory,
4
5
  estimateTokens,
5
6
  type SessionBeforeCompactEvent,
7
+ type SessionEntry,
6
8
  type SessionManager,
9
+ type SessionProjection,
7
10
  SettingsManager,
8
11
  } from "@earendil-works/pi-coding-agent";
9
12
 
@@ -29,8 +32,14 @@ export type CompactionHistory = Pick<
29
32
  export interface LatestCompaction {
30
33
  id: string;
31
34
  engine: CompactionEngine;
32
- /** Estimated context the compaction left: its summary and the entries it kept. */
35
+ /**
36
+ * The context the compaction left, as a request sends it: system prompt and tools included.
37
+ * The first request after it reports this; until then, its summary and kept entries plus the
38
+ * fixed part the last request before it carried.
39
+ */
33
40
  contextTokens: number;
41
+ /** Whether a request after the compaction reported `contextTokens`, or it is still estimated. */
42
+ measured: boolean;
34
43
  }
35
44
 
36
45
  /** The extension's compactions record its `engine` in their details; Pi's own do not. */
@@ -44,6 +53,41 @@ export function compactionEngine(
44
53
  : "pi";
45
54
  }
46
55
 
56
+ /** What a request sent, as its reply's usage reports it: undefined for a reply without usable usage. */
57
+ function sentTokens(entry: SessionEntry | undefined): number | undefined {
58
+ if (entry?.type !== "message") return undefined;
59
+ const message = entry.message;
60
+ if (message.role !== "assistant") return undefined;
61
+ if (message.stopReason === "aborted" || message.stopReason === "error")
62
+ return undefined;
63
+ const { input, cacheRead, cacheWrite } = message.usage;
64
+ const sent = input + cacheRead + cacheWrite;
65
+ return sent > 0 ? sent : undefined;
66
+ }
67
+
68
+ function estimated(messages: SessionProjection["messages"]): number {
69
+ let tokens = 0;
70
+ for (const message of messages) tokens += estimateTokens(message);
71
+ return tokens;
72
+ }
73
+
74
+ /**
75
+ * The part of a request that is not the conversation, system prompt and tool definitions, as the
76
+ * last reply before `index` reported it: what that request sent less the estimate of its messages.
77
+ */
78
+ function fixedTokens(branch: readonly SessionEntry[], index: number): number {
79
+ for (let at = index - 1; at >= 0; at--) {
80
+ const sent = sentTokens(branch[at]);
81
+ if (sent === undefined) continue;
82
+ const messages = buildSessionProjection(
83
+ branch.slice(0, at),
84
+ branch[at - 1]?.id ?? null,
85
+ ).messages;
86
+ return Math.max(0, sent - estimated(messages));
87
+ }
88
+ return 0;
89
+ }
90
+
47
91
  /** The branch's latest compaction, measured from the session as it stands now. */
48
92
  export function latestCompaction(
49
93
  history: CompactionHistory,
@@ -56,16 +100,22 @@ export function latestCompaction(
56
100
  const upToCompaction = new Set(
57
101
  branch.slice(0, index + 1).map((each) => each.id),
58
102
  );
59
- let contextTokens = 0;
103
+ const engine = compactionEngine(entry.details, extensionEngine);
104
+ for (const after of branch.slice(index + 1)) {
105
+ const sent = sentTokens(after);
106
+ if (sent !== undefined)
107
+ return { id: entry.id, engine, contextTokens: sent, measured: true };
108
+ }
109
+ let messages = 0;
60
110
  for (const projected of history.buildSessionProjection().entries) {
61
111
  if (!upToCompaction.has(projected.sourceEntry.id)) continue;
62
- for (const message of projected.messages)
63
- contextTokens += estimateTokens(message);
112
+ messages += estimated(projected.messages);
64
113
  }
65
114
  return {
66
115
  id: entry.id,
67
- engine: compactionEngine(entry.details, extensionEngine),
68
- contextTokens,
116
+ engine,
117
+ contextTokens: messages + fixedTokens(branch, index),
118
+ measured: false,
69
119
  };
70
120
  }
71
121
 
@@ -124,12 +174,15 @@ export class CompactionTiers {
124
174
  this.#extensionEngine = extensionEngine;
125
175
  }
126
176
 
127
- /** The latest compaction; measured once per compaction, since pi checks before every request. */
177
+ /**
178
+ * The latest compaction; pi checks before every request, so it is measured once per compaction
179
+ * after a request reports it, and estimated again until one does.
180
+ */
128
181
  latest(): LatestCompaction | undefined {
129
182
  const branch = this.#history.getBranch();
130
183
  const entry = branch.findLast((each) => each.type === "compaction");
131
184
  if (!entry) return undefined;
132
- if (this.#cached?.id !== entry.id) {
185
+ if (this.#cached?.id !== entry.id || !this.#cached.latest.measured) {
133
186
  const latest = latestCompaction(this.#history, this.#extensionEngine);
134
187
  if (!latest) return undefined;
135
188
  this.#cached = { id: entry.id, latest };
@@ -207,12 +207,14 @@ export class SessionFactory {
207
207
 
208
208
  /**
209
209
  * Logs who compacted a conversation, the compaction extension or Pi's summary, by how much, and the context size
210
- * that compacts it next, so a move to the hard ceiling shows.
210
+ * that compacts it next, so a move to the hard ceiling shows. `contextAfter` is the context the tiers measure,
211
+ * system prompt and tools included, which `tokensAfter`, the conversation alone, leaves out.
211
212
  */
212
213
  #logCompaction(
213
214
  channel: ChannelKey,
214
215
  event: CompactionEnd,
215
216
  session: AgentSession,
217
+ tiers: CompactionTiers,
216
218
  ): void {
217
219
  const { logger } = this.#options;
218
220
  const trigger = event.reason === "manual" ? "self" : event.reason;
@@ -239,6 +241,7 @@ export class SessionFactory {
239
241
  ),
240
242
  tokensBefore: event.result.tokensBefore,
241
243
  tokensAfter: event.result.estimatedTokensAfter,
244
+ contextAfter: tiers.latest()?.contextTokens,
242
245
  nextCompactionAt:
243
246
  model &&
244
247
  model.contextWindow -
@@ -423,7 +426,7 @@ export class SessionFactory {
423
426
  await this.#awaitTools(session, awaited);
424
427
  session.subscribe((event) => {
425
428
  if (event.type === "compaction_end")
426
- this.#logCompaction(channel, event, session);
429
+ this.#logCompaction(channel, event, session, tiers);
427
430
  });
428
431
  return Object.assign(state, { session });
429
432
  }