Fixes the debouncer race where a fast comment message flushes on its
own before slow audio transcription completes, splitting a single
user turn into two.
Adds a `send` custom tool so the agent can push files and images from the
workspace to the owner as Telegram attachments, closing the gap where the
bot could receive files but only ever reply with text.
Bridge @playwright/mcp as a stdio subprocess and re-export its tools
as Pi custom tools, gated on PLAYWRIGHT_ENABLED. Uses Type.Unsafe to
pass MCP inputSchema through to the model without hand-mapping.
Extends the calendar tool to create, modify, and delete recurring events
via raw RFC 5545 recurrence lines, with instance vs. series scope on
update/delete and a list_instances action to fetch a series' occurrences.
- runner.ts: cap captured stdout/stderr at 64 KiB per stream via a new
Capture helper, keeping the first 48 KiB and last 16 KiB once a stream
exceeds the cap, joined by "\n...(truncated)...\n". Prevents a runaway
watcher script from ballooning memory or a Telegram message.
- runner.ts runScript: delete TELEGRAM_BOT_TOKEN from the child environment
before spawning, since model-written scripts must never be able to read
and exfiltrate it. Spec section 6.2 updated to describe the exclusion.
- scheduler.test.ts: the "drain() gives up after its timeout" test no longer
calls s.stop() at the end -- no timer is armed after the at job fires, and
its delivery never resolves, so stop()'s 10s grace period would run out
for nothing. Full suite now runs in ~1.5s.
- runner.test.ts: relax the timeout-kill elapsed-time assertion from 3000ms
to 4000ms to reduce flakiness, per the review.
Claude-Session: https://claude.ai/code/session_01JtuZTCMW9TyogEmBgGf8JT
Pi drains queued follow-ups and steers inside the same run with no
agent_end/agent_start between them, so when a scheduled job's follow-up (or
a user steer) merges into a run, the model can answer twice before
agent_settled fires once. AgentHost.settle() used only the last assistant
message as finalText, so the first answer was lost (it only ever appeared
as progress-message narration, which gets deleted).
RunRecord now tracks `finals`: assistant texts whose stopReason is not
toolUse (and, as before, not error/aborted), collected alongside `texts`.
settle() joins `finals` with blank lines, falling back to `texts` and then
to the placeholder, matching the documented fallback chain.
Claude-Session: https://claude.ai/code/session_01JtuZTCMW9TyogEmBgGf8JT