Compare commits
2 Commits
e2b060a940
...
e68ee2071c
| Author | SHA1 | Date | |
|---|---|---|---|
| e68ee2071c | |||
| 309980b62c |
+25
-1
@@ -117,7 +117,7 @@ side of the wiring.
|
|||||||
| `MEMPALACE_FEED_WING` | `wing_conversations` | Target wing — passed to both the exporter and the `mempalace_mine` call. |
|
| `MEMPALACE_FEED_WING` | `wing_conversations` | Target wing — passed to both the exporter and the `mempalace_mine` call. |
|
||||||
| `MEMPALACE_FEED_DEBOUNCE_MS` | `600000` (10 min) | Minimum gap between mid-session (`agent_settled`) feeds. Bounds crash loss to one window instead of a whole session. |
|
| `MEMPALACE_FEED_DEBOUNCE_MS` | `600000` (10 min) | Minimum gap between mid-session (`agent_settled`) feeds. Bounds crash loss to one window instead of a whole session. |
|
||||||
| `MEMPALACE_FEED_PREPARE_TIMEOUT_MS` | `120000` | Kills a wedged `--prepare` subprocess. |
|
| `MEMPALACE_FEED_PREPARE_TIMEOUT_MS` | `120000` | Kills a wedged `--prepare` subprocess. |
|
||||||
| `MEMPALACE_FEED_MINE_TIMEOUT_MS` | `30000` | Caps the `mempalace_mine` call so a stalled palace can't hang session exit. |
|
| `MEMPALACE_FEED_MINE_TIMEOUT_MS` | `300000` (5 min) | Bounds how long the extension *waits* for `mempalace_mine`, so a stalled palace can't hang session exit. It does **not** cancel the mine — see [Debugging](#debugging). Raised from `30000` in 2026-09: the mine is the slowest call this extension makes (30–60s in normal operation), so the old deadline fired routinely and reported healthy behaviour as an error. |
|
||||||
|
|
||||||
**Remote palace:** if `$MEMPALACE_REMOTE_URL` is set (see
|
**Remote palace:** if `$MEMPALACE_REMOTE_URL` is set (see
|
||||||
[Transport](#transport-local-vs-external)), `mempalace_mine`'s source path is
|
[Transport](#transport-local-vs-external)), `mempalace_mine`'s source path is
|
||||||
@@ -465,6 +465,30 @@ the next tool call transparently respawns `mempalace-mcp` and retries.
|
|||||||
`mempalace-mcp` manually with raw JSON-RPC on stdin to read the
|
`mempalace-mcp` manually with raw JSON-RPC on stdin to read the
|
||||||
server-side error — much faster than guessing.
|
server-side error — much faster than guessing.
|
||||||
|
|
||||||
|
### `feed (tick) failed: mine timed out after …ms`
|
||||||
|
|
||||||
|
**Nothing has been lost.** The deadline bounds only how long the extension
|
||||||
|
*waits*; it cannot cancel the mine, which continues on the server. The
|
||||||
|
transcript is already staged before the mine is invoked, and
|
||||||
|
`mempalace mine --mode convos` dedups by `source_file` and is idempotent, so the
|
||||||
|
work either completed after the deadline or is redone by the next tick.
|
||||||
|
|
||||||
|
**Do not "fix" it by retrying harder from the client.** The palace is a single
|
||||||
|
writer; a blind retry is what turns one slow mine into a queue of them.
|
||||||
|
|
||||||
|
Before 2026-09 this message appeared many times per session, which made it look
|
||||||
|
like a persistent failure. That was a real defect, now fixed: `lastFeedAt` was
|
||||||
|
recorded only after a *successful* wait, so a timeout left the debounce clock
|
||||||
|
stale and every following settled turn started another mine on top of the one
|
||||||
|
still running. Two changes — recording the attempt before the wait, and raising
|
||||||
|
the deadline to sit far above the slowest honest completion — mean a healthy
|
||||||
|
fleet should now never see it.
|
||||||
|
|
||||||
|
If you *do* still see it, it is now informative rather than noise: a mine
|
||||||
|
exceeded five minutes. Check palace size and whether another writer (a
|
||||||
|
host-side feeder, a scheduled `mine`) is holding the write lock, rather than
|
||||||
|
raising the timeout again.
|
||||||
|
|
||||||
## The `Type.Unsafe` gotcha
|
## The `Type.Unsafe` gotcha
|
||||||
|
|
||||||
Earlier versions of this extension registered every MCP tool with
|
Earlier versions of this extension registered every MCP tool with
|
||||||
|
|||||||
@@ -855,7 +855,22 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
|
|||||||
const feedWing = process.env.MEMPALACE_FEED_WING ?? "wing_conversations";
|
const feedWing = process.env.MEMPALACE_FEED_WING ?? "wing_conversations";
|
||||||
const feedDebounceMs = num(process.env.MEMPALACE_FEED_DEBOUNCE_MS, 600_000);
|
const feedDebounceMs = num(process.env.MEMPALACE_FEED_DEBOUNCE_MS, 600_000);
|
||||||
const feedPrepareTimeoutMs = num(process.env.MEMPALACE_FEED_PREPARE_TIMEOUT_MS, 120_000);
|
const feedPrepareTimeoutMs = num(process.env.MEMPALACE_FEED_PREPARE_TIMEOUT_MS, 120_000);
|
||||||
const feedMineTimeoutMs = num(process.env.MEMPALACE_FEED_MINE_TIMEOUT_MS, 30_000);
|
// 300_000, raised from 30_000 on 2026-09-10. The mine is the SLOWEST thing
|
||||||
|
// this extension does — it offers every qualifying session transcript to a
|
||||||
|
// SINGLE-WRITER palace, measured at 30–60s in normal operation and growing
|
||||||
|
// with the corpus — yet it carried by far the TIGHTEST deadline: 4x tighter
|
||||||
|
// than the prepare step that precedes it (120_000) and 10x tighter than the
|
||||||
|
// init handshake (300_000), which is a fast call. All three were introduced
|
||||||
|
// together in 29e660e (2026-08-12) and this one was never revisited, so the
|
||||||
|
// deadline fired during entirely normal operation and the resulting message
|
||||||
|
// read as an error when nothing had gone wrong.
|
||||||
|
//
|
||||||
|
// Matched to the init timeout because liveness is the ONLY legitimate job
|
||||||
|
// left for this deadline: the Promise.race below abandons our WAIT, it cannot
|
||||||
|
// cancel the server's work, so the deadline buys nothing except an escape
|
||||||
|
// from a permanently hung call. It must therefore sit far above the slowest
|
||||||
|
// honest completion, not near it.
|
||||||
|
const feedMineTimeoutMs = num(process.env.MEMPALACE_FEED_MINE_TIMEOUT_MS, 300_000);
|
||||||
let lastFeedAt = 0; // 0 => the first settled turn also acts as a catch-up
|
let lastFeedAt = 0; // 0 => the first settled turn also acts as a catch-up
|
||||||
let feedInFlight: Promise<void> | null = null;
|
let feedInFlight: Promise<void> | null = null;
|
||||||
|
|
||||||
@@ -909,6 +924,23 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
|
|||||||
try {
|
try {
|
||||||
const source = await prepareFeed(reason);
|
const source = await prepareFeed(reason);
|
||||||
if (!source) return;
|
if (!source) return;
|
||||||
|
// Record the attempt HERE, before awaiting — not after a successful
|
||||||
|
// wait. The race below abandons only our WAIT; the mine keeps running
|
||||||
|
// server-side, and `mine --mode convos` dedups by source_file and is
|
||||||
|
// idempotent, so a timeout is emphatically not a "did not happen".
|
||||||
|
//
|
||||||
|
// Leaving lastFeedAt stale on the timeout path defeated the debounce
|
||||||
|
// guard in the agent_settled handler below (`Date.now() - lastFeedAt <
|
||||||
|
// feedDebounceMs`): with lastFeedAt unchanged that guard passed on EVERY
|
||||||
|
// settled turn, and because `run` had already settled, feedInFlight was
|
||||||
|
// null too — so BOTH guards stood open. Each settled turn then launched
|
||||||
|
// another mine while the previous one was still running: overlapping
|
||||||
|
// writers queueing on a single-writer palace, each making the next one
|
||||||
|
// slower and the next timeout likelier. That positive feedback loop,
|
||||||
|
// not the tight deadline by itself, is why operators saw "mine timed out
|
||||||
|
// after 30000ms" many times per session rather than at most once per
|
||||||
|
// debounce window.
|
||||||
|
lastFeedAt = Date.now();
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
client.callTool("mempalace_mine", {
|
client.callTool("mempalace_mine", {
|
||||||
source,
|
source,
|
||||||
@@ -927,7 +959,6 @@ export default async function mempalaceExtension(pi: ExtensionAPI) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
lastFeedAt = Date.now();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`[mempalace ext] feed (${reason}) failed: ${(err as Error).message}\n`,
|
`[mempalace ext] feed (${reason}) failed: ${(err as Error).message}\n`,
|
||||||
|
|||||||
Reference in New Issue
Block a user