feat(pi): print and JSON mode skip the wake-up and mailbox poll (MEMPALACE_WAKEUP_PRINT=1 to keep them)

In print and JSON mode, `pi -p` and `--mode json` no longer get the
before_agent_start wake-up injection (status, diary and the tier-1 mailbox),
and the mid-session mailbox poll is off as well. The poll has its own guard,
not just `wokeUp`, because `pi -p -c` resumes and a resume sets wokeUp. The
client still connects, and the mempalace_* tools stay registered.
MEMPALACE_WAKEUP_PRINT=1 restores both. TUI and RPC sessions are unchanged.

Why: a one-shot run is a script asking one question. The wake-up cost it 12
palace calls and a large orientation message, and it is meant for the
operator of an interactive session. It can also do harm: on tor-ms22 a flash
model given "Reply with exactly: PONG" read the injected diary's open items
as its task, ran about 12 tool calls, and filed a wrong drawer in the shared
palace.

Measured on tor-ms22 (workspace copy, -ne -nt --no-session -e) with a fetch
probe that re-wraps pi's own fetch. The earlier preload-only fetch wrapper had
logged 0 lines in every run, because pi replaces globalThis.fetch.
- pi -p: 3 palace requests (initialize, initialized, tools/list), 1.6 s.
- --mode json: the same requests, 1.0 s, and no mempalace-wakeup in stdout.
- --mode json with MEMPALACE_WAKEUP_PRINT=1: 15 requests, 12 of them
  tools/call (status 1, diary 1, event_list 10), 3.6 s, and the wake-up is in
  stdout.

New scripts/test-wakeup-mode.sh has these cases: tui, print, json, print
resumed, tui resumed (a control that shows the poll does run), print with
MEMPALACE_WAKEUP_PRINT=1, and rpc. f590a3a's source fails the print, json and
print-resumed cases, with 12, 12 and 5 calls. All seven scripts/test-*.sh
pass.
This commit is contained in:
2026-10-13 15:32:07 +02:00
parent f590a3ac5b
commit d5a03f4ff5
3 changed files with 192 additions and 2 deletions
+19 -2
View File
@@ -20,7 +20,8 @@
* - Wake-up (auto): on first user prompt of a fresh session, inject
* `mempalace_status` + `mempalace_diary_read` output as context so the
* agent orients itself the way the mempalace skill describes. Skipped
* on resume/fork (palace context is already in the thread).
* on resume/fork (palace context is already in the thread), and in
* print/json mode (`pi -p`, `--mode json`) unless MEMPALACE_WAKEUP_PRINT=1.
* - Feeding (auto): stage + mine this container's pi transcripts into the
* palace on `session_shutdown` and on a debounced `agent_settled`. Needs
* no LLM turn (pi transcripts are JSONL on disk), which is why it CAN be
@@ -1200,6 +1201,19 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
};
const feedsThisRun = (ctx: unknown): boolean => feedInPrintMode || !isOneShot(ctx);
// Print and JSON mode skip the palace start-up work as well, unless
// MEMPALACE_WAKEUP_PRINT=1. That is the wake-up injection (status + diary +
// tier-1 mailbox) and the mid-session mailbox poll. A one-shot run is a
// script asking one question. The wake-up costs it several palace round
// trips and a large orientation message, and it serves the operator of an
// interactive session, who sees it next time they open one. It can also do
// harm: on tor-ms22 a flash model given "Reply with exactly: PONG" read the
// injected diary's open items as its task, ran ~12 tool calls, and filed a
// wrong drawer. The mempalace_* tools stay registered, so a prompt that asks
// for the palace still gets it.
const wakeupInPrintMode = process.env.MEMPALACE_WAKEUP_PRINT === "1";
const wakesThisRun = (ctx: unknown): boolean => wakeupInPrintMode || !isOneShot(ctx);
// Mid-session feed. A hard container kill runs no handler at all, so this is
// what bounds crash loss to one debounce window instead of a whole session.
// Re-mining a grown transcript purges and refiles that source_file, so
@@ -1675,6 +1689,8 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
pi.on("agent_settled", async (_event, ctx) => {
if (!mailboxEnabled || !available) return;
// Explicit, not left to `wokeUp`: `pi -p -c` resumes, which sets wokeUp.
if (!wakesThisRun(ctx)) return;
if (!wokeUp) return; // the wake-up injection has not run yet; it does look #1
if (Date.now() - lastMailboxPollAt < mailboxPollMs) return;
// Claim the slot BEFORE awaiting anything, so overlapping settles cannot
@@ -1759,8 +1775,9 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
});
// --- Auto wake-up (mempalace skill Phase 1) ---
pi.on("before_agent_start", async (_event, _ctx) => {
pi.on("before_agent_start", async (_event, ctx) => {
if (wokeUp || !available) return;
if (!wakesThisRun(ctx)) return; // one-shot run: see wakesThisRun
wokeUp = true; // one-shot, even if the calls below fail
const sections: string[] = [];