Files
Dragan Spiridonov b2e6090c6d chore(release): v3.9.25 (#452)
* feat(hooks): post-route Stop hook closes route sentinels 1:1 (#451)

The `route` hook fires on UserPromptSubmit and writes a sentinel row to
routing_outcomes with quality_score=-1, expecting `post-task` to close
it. But `post-task` only fires on PostToolUse ^(Task|Agent)$, which is
rare in direct-work sessions (Bash/Edit/Read without spawning sub-
agents). Sentinels accumulated indefinitely — Jordi reported 123+
unresolved rows per session. Stream D never converged.

Changes:

1. New `aqe hooks post-route` subcommand
   (src/cli/commands/hooks-handlers/routing-hooks.ts)
   Closes the most-recent unresolved route sentinel using a
   `task_json NOT LIKE '%"taskId"%'` discriminator that isolates
   route entries from pre-task entries. Quality formula collapses the
   6-dim outcome formula for unknown duration:
     0.325 + 0.25·success + 0.10·1.0
   = 0.675 (success) / 0.425 (failure)

2. Inverse discriminator on updateRoutingOutcomeQuality
   (src/cli/commands/hooks-handlers/hooks-dream-learning.ts)
   Added `task_json LIKE '%"taskId"%'` to the WHERE clause so
   post-task only ever closes pre-task sentinels. Without this,
   post-task could grab a stale route sentinel by accident and
   miscredit duration to a row that wasn't about a Task() call.
   Symmetric counterpart to the post-route filter.

3. Stop-hook wiring in both init template paths
   (src/init/phases/07-hooks.ts + src/init/init-wizard-hooks.ts)
   Added `npx agentic-qe hooks post-route --success true --json`
   to the Stop block. Timeout 5000ms, continueOnError: true, so a
   failed post-route cannot block session teardown.

4. CLI help text (src/cli/commands/hooks.ts)
   Added example for the new subcommand.

5. Regression test (tests/unit/cli/commands/post-route.test.ts)
   4 tests using in-memory better-sqlite3:
   - Closes most-recent route sentinel; leaves pre-task sentinels
     and resolved rows untouched
   - quality_score = 0.425 when --success false
   - No-op when only pre-task sentinels exist (discriminator works)
   - No-op on second invocation when only one sentinel was outstanding

Bug-introduce verification: stashed routing-hooks.ts, ran new test —
all 4 cases failed (Commander rejects unknown subcommand). Restored
fix, all 4 pass. Test distinguishes pre-fix and post-fix states.

Co-authored-by: Jordi Izquierdo <noreply@github.com>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(release): bump version to v3.9.25

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:05:29 +02:00

3.4 KiB

v3.9.25 Release Notes

Release Date: 2026-05-12

Highlights

Closes the routing_outcomes sentinel that route hooks (UserPromptSubmit) wrote but nothing reliably closed. In direct-work sessions (Bash/Edit/Read, no Task/Agent sub-agents) the sentinel sat at quality_score=-1 indefinitely — Jordi observed 123+ unresolved rows per session. New post-route subcommand wired into the Stop hook closes them 1:1 with each turn. Stream D can finally converge.

Added

  • aqe hooks post-route (#451) — Stop-hook counterpart to UserPromptSubmit/route. Closes the most-recent unresolved route sentinel using a task_json NOT LIKE '%"taskId"%' discriminator that isolates route entries from pre-task entries. Quality formula collapses the 6-dim outcome formula for an unknown duration: 0.325 + 0.25·success + 0.10 = 0.675 on success / 0.425 on failure.
  • Wired into both init template paths — src/init/phases/07-hooks.ts (the auto/main init flow) and src/init/init-wizard-hooks.ts (the wizard flow) both gain a new Stop block entry: npx agentic-qe hooks post-route --success true --json. Timeout 5000ms, continueOnError: true, so a failed post-route cannot block session teardown.

Fixed

  • Route sentinels accumulated at quality_score=-1 forever (#451). The route hook fired on UserPromptSubmit and wrote a sentinel row, expecting post-task to fill it in. But post-task only fires on PostToolUse ^(Task|Agent)$, which is rare in direct-work sessions. Sentinels stayed open and any consumer reading routing_outcomes without a quality_score >= 0 filter saw success=0 (sentinel value) as if every turn had failed.
  • updateRoutingOutcomeQuality could close the wrong sentinel (#451). Prior to this release, the post-task UPDATE picked the most-recent unresolved row within 30 minutes biased on used_agent, with no discriminator separating route sentinels from pre-task sentinels. When dozens of route sentinels were sitting around, post-task could close one of those instead of the actual pre-task sentinel it had just written. Added the inverse discriminator task_json LIKE '%"taskId"%' — post-task now only closes pre-task sentinels, symmetric with the new post-route filter.

Verification

  • npm run build — clean.
  • npm run typecheck — clean.
  • npx vitest run tests/unit/cli/ tests/unit/learning/qe-hooks.test.ts tests/hooks/task-completed-hook.test.ts tests/unit/init/phases — 833 / 833 passing across 30 files, including the new post-route.test.ts (4 tests).
  • Bug-introduce verification: stashed routing-hooks.ts, re-ran new test — all 4 cases failed (Commander rejects the unknown subcommand). Restored the fix, all 4 pass. The test correctly distinguishes pre-fix and post-fix states.

Upgrade Notes

  • No breaking changes. After upgrading and re-running aqe init, the Stop hook gains a post-route invocation that closes the route sentinel 1:1 with each turn.
  • Existing installs continue working but won't close route sentinels until aqe init is re-run. The chronic-flake symptom (123+ unresolved sentinels per session) will continue until the Stop hook is wired.
  • Resolved sentinels on disk from prior versions are not retroactively adjusted — they stay at quality_score=-1 until cleanup or natural expiry. New turns get the correct quality_score on session end.

Thanks to @Jordi-Izquierdo-DDS for the diagnosis, evidence, and patch shape.