v2.15.7
[2.15.7] — 2026-09-22
Fixed
- A role could end its turn with its own task still open and nothing noticed. On the 2.15.6 release run the publisher reported its results with
org_sendand ended its turn without callingorg_task_done; the coordinator waited on a completion that never came and the run sat still for ~10 minutes until a human intervened, because the only backstop was the org-wide idle watchdog (run_config.idle_minutes, 45 in that org). When a role's turn ends the runtime now checks what it left open and delivers one short[task:<id>] STILL OPEN — …message naming the task and what closing it takes (includingevidencewhenrun_config.completion_evidenceis on), plus atask-open-at-turn-endaudit event. It is bounded: nothing is sent while the role still has mail queued or coalescing, a task blocked on a real-world time (org_task_block) is never nudged, and each task earns at most one nudge per dispatch. The idle watchdog's own behaviour is unchanged. - A completion notification could name an already-closed task. (Fixes #319) With
run_config.session_scope: "task"a role's model session is keyed per task and resumed per task, so a session resumed for a follow-up task still carries the previous, already-closed task in its context. Closing that remembered id used to succeed — nothing in the runtime refused a second close — so withnotify_task_creatorthe creator received a second[task:<already-closed id>] DONEwhile the task actually in flight stayedrunninguntil a human cross-checkedorg_tasksagainst the notifications (observed twice on the 2.15.6 release run).org_task_donenow refuses a task that has already reached a terminal status, names the caller's open task(s) in the refusal, records atask-already-closedaudit event, and leaves the DAG and the closed task's evidence untouched; the notification's tag and title come from the task that just closed.
Changed
-
expectExitcan no longer hide failures inside an aggregate command. In the 2.15.6 release run a role putexpectExit: 1onpnpm run test:all:run— a ~7,800-test suite — and the completion-evidence gate accepted it: a suite's exit 1 means "at least one of 7,800 things failed", so the known failure and any new regression are the same exit code and the gate cannot tell them apart. A human had to read the log by hand to confirm only the expected test had failed.org_task_donenow refusesexpectExiton a command that runs a test suite or an aggregate runner (vitest,jest,npm/pnpm/yarntest scripts,node --test,pnpm -r,pnpm --filter … test,run verify,test:all— a command naming a single test file is not one), and the refusal says to run the failing test file on its own and declareexpectExiton that check, or exclude the known failure and record the exclusion. Every otherexpectExitnow requires a one-lineexpectReasonsaying why the non-zero exit is correct; the reason travels with the exit code into the refusal, the task's stored result and the review packet (exit 1 (expected 1: 404 = branch not protected)), and each acceptedexpectExitraises an audit event (evidence-expect-exit) so they can be swept after a run.expectExit: 0is the default and declares nothing. -
Two
monomind browse opencalls running at once fought over one browser. (Fixes #318) With no--port, everyopentargeted the same default CDP port (9222) and the profile directory derived from it, so two uncoordinated invocations either collided at launch (Chrome exited before the CDP endpoint opened on port 9222) or silently shared one Chrome, where the second navigation aborted the first (net::ERR_ABORTED) and onebrowse closekilled both. Reproduced 5/5 with two concurrent opens; 5/5 pass now, each with its own browser.openwith no--portnow starts its own session: Chrome binds a kernel-assigned free port in a profile directory of its own, andopenreports it —✓ Opened: … [port 41337]. Sessions are recorded one file per port, per working directory, in.monomind/monobrowse/sessions/<port>.json(they shared a singleactive-port.jsonbefore), and every later command follows one rule:--port Nacts on that session, no--portacts on the newest session in this directory whose browser still answers. Dead records are dropped as they are passed, so a crashed or manually-killed browser self-heals instead of wedging later commands, andcloseends exactly the session it resolved — never another invocation's browser.open --port Nandconnect --port Nkeep today's attach-to-that-port behaviour exactly, and each session keeps its own snapshot ref cache so concurrentsnapshot/clickwork does not cross over.Upgrading with a session already open does not strand its browser: a directory that still has the old single-session file (
.monomind/monobrowse/active-port.json) has it treated as one more candidate — the oldest, so live per-port sessions win. If its browser still answers it is adopted (rewritten assessions/<port>.json, keeping port, PID andopen/connectprovenance, old file deleted) and behaves like any other session forsnapshot,--portandclose; if it does not answer, the old file is just removed. Nothing writes that file again, and adoption never makes a bareopenjoin an existing session.@monoes/monobrowse1.0.21.
npm: monomind@2.15.7, @monoes/monomindcli@2.15.7, @monoes/monobrowse@1.0.21