fix(pi): clear the feed's mine-race timer, so pi -p exits when the mine returns
feedPalace() raced mempalace_mine against a bare setTimeout(reject, feedMineTimeoutMs) and never cleared it. When the mine won (the normal case) the losing 300 000 ms timer stayed referenced. Interactive and RPC mode call process.exit() and never showed it. Print mode (`pi -p`, `--mode json`) returns and waits for an empty event loop, so every `pi -p` on a devbox printed its answer and then idled for the rest of the five minutes. Measured on tor-ms22 (pi 1.1.0, NODE_OPTIONS=--import probe): - before: PONG at 4 s, exit at 315.1 s. The 300 000 ms timer from mempalace.ts:1144 (created +15.1 s) was the ONLY active resource from +50 s on. - after: PONG at 3 s, exit at 45.2 s, the moment the mine's socket closed. - MEMPALACE_FEED=0: 3.8 s. pi 1.0.0 behaved the same as 1.1.0, so this is not a pi regression. The timer is now cleared in a finally once the race settles. It stays referenced while it runs because it is the liveness guard. scripts/test-feed-exit.sh loads the type-stripped SHIPPED extension under a stub pi with a fake palace in a separate process and fires session_shutdown: - A: the mine answers in 1 s with a 20 s deadline. Node must exit within 3 s of the handler resolving. The pre-fix source FAILS here (19 000 ms). - B: the mine never answers, 2 s deadline. The deadline still ends the wait, and the failure is logged. All five scripts/test-*.sh pass. README Debugging section: the remaining expected wait (one catch-up feed per session, ~45 s here) and MEMPALACE_FEED=0 for scripts that want an instant return.
This commit is contained in:
+38
-21
@@ -1123,28 +1123,45 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
|
||||
// after 60000ms" — and the 300 000 below was unreachable. The
|
||||
// race stays as the liveness guard for a transport whose timeout
|
||||
// is disabled (0).
|
||||
await Promise.race([
|
||||
client.callTool(
|
||||
"mempalace_mine",
|
||||
{
|
||||
source,
|
||||
mode: "convos",
|
||||
wing: feedWing,
|
||||
// Internal call: it does not pass through the registered tool's
|
||||
// execute(), so it stamps itself. These ARE this harness's own
|
||||
// transcripts from this device, so the harness segment is the
|
||||
// agent (not `miner`) even though the tool is `mine`.
|
||||
agent: stampProvenance ? `${agentName}@${device}` : agentName,
|
||||
},
|
||||
{ timeoutMs: feedMineTimeoutMs },
|
||||
),
|
||||
new Promise((_resolve, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(`mine timed out after ${feedMineTimeoutMs}ms`)),
|
||||
feedMineTimeoutMs,
|
||||
//
|
||||
// The race's timer MUST be cleared when the race settles (finally,
|
||||
// below). Until 2026-10-13 it was a bare setTimeout that nothing
|
||||
// cleared, so when the mine WON — the normal case — the losing
|
||||
// timer stayed referenced for the rest of feedMineTimeoutMs. In
|
||||
// interactive and RPC mode pi calls process.exit() and nobody saw
|
||||
// it; print mode (`pi -p`, `--mode json`) returns and waits for an
|
||||
// empty event loop, so every `pi -p` on a devbox printed its answer
|
||||
// and then idled until the 300 s deadline (measured on tor-ms22: the
|
||||
// only live resource from the mine's return to exit). Kept ref'd on
|
||||
// purpose while it runs: it is the liveness guard.
|
||||
// scripts/test-feed-exit.sh pins both halves.
|
||||
let raceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
try {
|
||||
await Promise.race([
|
||||
client.callTool(
|
||||
"mempalace_mine",
|
||||
{
|
||||
source,
|
||||
mode: "convos",
|
||||
wing: feedWing,
|
||||
// Internal call: it does not pass through the registered tool's
|
||||
// execute(), so it stamps itself. These ARE this harness's own
|
||||
// transcripts from this device, so the harness segment is the
|
||||
// agent (not `miner`) even though the tool is `mine`.
|
||||
agent: stampProvenance ? `${agentName}@${device}` : agentName,
|
||||
},
|
||||
{ timeoutMs: feedMineTimeoutMs },
|
||||
),
|
||||
),
|
||||
]);
|
||||
new Promise((_resolve, reject) => {
|
||||
raceTimer = setTimeout(
|
||||
() => reject(new Error(`mine timed out after ${feedMineTimeoutMs}ms`)),
|
||||
feedMineTimeoutMs,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
clearTimeout(raceTimer);
|
||||
}
|
||||
} catch (err) {
|
||||
process.stderr.write(
|
||||
`[mempalace ext] feed (${reason}) failed: ${(err as Error).message}\n`,
|
||||
|
||||
Reference in New Issue
Block a user