Measured in a full-extension session: one `echo "$SECRET"` was reported as
"redacted ... x2". pi's bash tool returns its output both as content text and
as structuredContent.output, and both are (correctly) redacted. The count
now takes, per name, the highest count from any one field instead of the
sum. A test pins it: it failed before the change and passes after. Re-run
in the full-extension TUI: the warning now reads "redacted SG_TEST_TOKEN
from bash output"; 0 raw copies sent, 0 in the session file. Suite: 46/46.
Measured 2026-10-12 on emb-7kj4vr4g. A new session ran `env | grep …` and
tried to redact the output itself with a sed. The sed only matched when TOKEN
came after the `=`, so MEMPALACE_REMOTE_TOKEN=<value> went through. That raw
value was found in 12 local session files back to 2026-08-16, and each of those
sessions had sent it to its model provider. The palace stayed clean only
because mempalace-toolkit's feeder scrubs transcripts before staging. Nothing
guarded the step from tool output to model context.
The new extension ports tier 1 of mempalace_redact.py exactly: the exact
values of env vars whose name says secret are replaced with <redacted:NAME>.
Matching a known value cannot hit anything that is not that secret.
- tool_result: content text, structuredContent (always returned when content
changes, so it is not dropped) and details.
- context: the same pass over every request, for user `!` output, pasted text
and resumed sessions that already hold a raw value.
- tool_call: block a write/edit whose new text contains a live secret's
placeholder, i.e. writing a redacted .env back. The reason says to use bash
and let the shell expand "$NAME".
PI_SECRET_GUARD=off makes it inert.
Tests (16, two-sided): selection, ordering, redaction, deep walk with
identity, every handler, the off switch. Two parity tests run
mempalace_redact.py on the same synthetic env and compare the selected names
and the output text. Thirteen mutations, one per rule, are each caught. Two
mutations initially survived and exposed fixture gaps, which are now fixed.
Full suite: 45/45.
End-to-end in real pi (erdc/deepseek-v4.1-flash). The secret was synthetic,
and a test-only probe on before_provider_request counted it in each payload.
- control without the guard: 1 raw copy sent to the provider, 1 in the
session file;
- with the guard: 0 sent and 0 stored, placeholder in both;
- resumed control session: 1 sent without the guard, 0 with it;
- interactive `!echo $SECRET`: 0 sent, plus a TUI warning;
- a write of the placeholder: blocked, file sha unchanged.
Not covered, as documented: live progress (tool_execution_update), the stored
entry of a user `!` command, secrets not in env (no shape rules), and pi-task
children, which run --no-extensions.
Overnight 2026-10-12: in the session's last hour AWS issued tokens capped at
the end (00:18:48 local, 8 h after sign-in); the old 'expiresAt advanced' rule
read their millisecond jitter as 'refreshed'. A refusal at 00:13 led to a
login that inherited the ending session, and the 30-min wait then delayed the
useful login to 00:47.
Now: new full token = refreshed; new token issued for < 55 min = capped, its
expiry is the end (session_end_seen, status 'until HH:MM'), login at end+30 s;
nothing written = refused, login at once. anchor logs token_min/inherited from
the cap, not a clock. 2 min between unprompted logins after a success;
30 min, 1 h, 2 h, 4 h after unanswered ones.
The mode cannot be inferred: Linux/Windows hosts can have a browser and an
always-on Mac can serve headless. /aws-sso local|remote, PI_AWS_SSO_MODE in
.env; a command-set mode wins; old mac/device names map. PI_AWS_SSO_NTFY
pushes the device link (tap to approve), a heads-up when a local login waits
>60 s, failures, and 'renewed'. Topic shown only as host + fingerprint.
Unprompted logins back off 30 min doubling to 4 h; a run that stopped on an
expired session resumes after any later successful login.
Measured: a session lasted <= 6 h (2026-10-11), not the 8 h assumed, so a
clock cannot predict the end. token-providers 3.1127.0 refreshes only inside
a 5-min window and, on refusal, silently returns the still-valid token. The
keeper now makes that attempt itself (pi's own SDK, same cache) when the
window opens, and if expiresAt did not advance, logs in while the old token
still works. Clock-based renewal, signed-in and hours commands removed.
AWS: CLI sessions refresh only while the user interactive session (browser
sign-in) is valid; a login inside it is silent but inherits its end (green
box 'can be used until your session expires', 2026-10-11). Renewing early
gains nothing, so renew 2 min after the estimated end, forcing a fresh IdP
sign-in while the last 1-hour token still works. Silent renewals keep the
anchor. On an expired-session request failure, renew then send 'continue'
(once per 10 min). New: /aws-sso continue on|off, signed-in HH:MM.
Like Claude Code's awsAuthRefresh: the user still approves each login.
mode mac runs it on the Docker host over SSH (own browser); mode device shows
the device code in pi for SSH/tmux use. Off by default (/aws-sso on).
Session start inferred from refresh-token changes; 8 h default, measured.
The rule was correct and written down twice (global AGENTS.md, this skill)
and was still violated by agents that had just read it: on 2026-09-17 all
five fork briefs in one session carried "do not", one returned confident
verbatim quotes that did not exist, and four had disjoint write boundaries
fork cannot enforce (re-run as pi-task, they passed). Three mechanisms,
none of them wording:
1. fork is a TOOL — its self-recommending description ("implementation,
testing, review…") is in the model's face every turn; pi-task was a CLI
to be remembered and reached through bash with a hand-written JSON.
2. The skill is gone after the first compaction; the tool list never is.
The asymmetry widens in exactly the long sessions where fork is worst.
3. Friction: one string vs a spec file + bash + reading result.json.
extensions/task.ts registers pi-task as the `task` tool. Flat parameters
build the spec; the decision rule sits in the description and in
promptGuidelines (appended to the system prompt, so compaction cannot remove
it). Before spending a model run it rejects the two spec errors that make a
boundary violation certain — write_allowed not an exact subset of roots, and
a writable root nested in a watched-only root (the parent's porcelain would
change every time; pi-task keys deltas by root string) — and it serialises
sibling tasks whose roots overlap (parallel siblings saw each other's writes
as violations, 2026-09-17). Returns the CLI's own parent-facing report; a
FAIL verdict is a result, only a CLI refusal is an error.
extensions/fork-gate.ts is a tool_call hook that BLOCKS a fork whose brief
contains a prohibition, a write boundary, or a clause-initial file-changing
imperative, and returns as the reason the exact task(...) to make instead.
Wording, not intent — the message says so and how to rephrase a genuinely
read-only brief. PI_FORK_GATE=off logs instead; /ext disables.
Evidence: test/fork-gate.test.mjs is two-sided (15 must-block incl. the real
shapes, 10 must-pass incl. "Write a summary…", "Report which files were
modified…", "Give me an update…"); the classifier redirects 5/5 of the real
briefs from the motivating session. test/task.test.mjs pins root overlap and
the pre-launch validation. Live in `pi -p`: the fork was intercepted before
any child spawned (no /tmp/pi-fork-* dir) and the model received the
redirect; task returned PASS with an evidence pointer and audit dir, a
budget-overrun returned a FAIL result (isError=false), and a nested-root spec
was rejected with no audit dir created.
skill/SKILL.md: Part 1 now opens with "decide the rung before the brief"
(the table, the three-question pre-flight, the roots contract, overlap,
what isolation does not fix); the ladder section and quick reference no
longer say pi-task "will never appear in your tool list". package.json gains
"type": "module" and a test script.