Release held proposals, show held count
Epic 6659: Project memory, open the pipe, honest trust. This is child C1 (improvement 1, PRD S1). It builds on C0's plan gate.
Sources: PRD (Task 6660) §3.1, and Architecture (Task 6661) §1 C1, §2.1 and §2.2.
## Original requirement (verbatim)
"It is important that all memory related mechanism stays behind the feature flag on the billing plan." … "Each later subtask ships its change inside this gate and adds a test proving the mechanism is inert with the term off."
"1. Open the pipe: never hold a `propose`-feed unit with no path to release. In `apps/api/app/project_memory/curate.py` (canon_verdict ~455–456, promote_unit ~559–562, recheck_held ~571–656): without a memory model, a `propose`-feed unit that passes the regex secret gate is accepted injectable; keep the rules-only hold for settle-mined, stop-delta and distilled units; make `recheck_held` (or a rules-only equivalent) release the already-held `propose` backlog on the next tick. Show the held count in the Project › Memory tab banner (`apps/web/src/components/projects/ProjectMemoryTab.tsx`) so a stuck pipe is visible. Done means: a new proposal reaches a later dispatch prompt in the local stack with no model credential set; the held backlog count drops to the settle/distill units only."
The audit (Task 6581) found that every runner proposal was parked as "canon check inactive", so no proposal has ever reached a prompt.
## Acceptance criteria (verbatim, PRD §3.1)
- **S1-1** With no memory model, a `propose`-feed unit that passed ingest (which includes the regex secret gate) is promoted `accepted`, `injectable=true` and `status_reason=NULL`. It is never `canon check inactive`. Units of feed `settle`, `stop_delta` and `distill` keep today's rules-only hold. With a model set, behaviour is unchanged: the model canon and secret checks still apply to `propose` units.
- **S1-2** On the first curate tick after deploy, already-held `propose` units are released by the same rule. The release is bounded per tick like `CURATE_RECHECK_MAX`, and it continues on later ticks until none remain. A held unit that hits the secret regex is not released.
- **S1-3 The held count is visible.** The Project › Memory tab shows the number of held units (`accepted` with reason `canon check inactive`) by feed, next to the canon/model status. It shows whenever the count is above 0. The text is readable and never truncated, and it uses the same type scale as the feed chips. The API exposes the count, for example in `feed-status`.
- **S1-4 End to end.** On the local stack with no model credential: a runner proposal accepted in task A appears in the PROJECT MEMORY section, or is searchable through the bundle, of a later dispatch B whose paths, persona or keywords match.
- **S1-5 Inert** (S0-7): with the term off, a tick releases nothing and the count route answers 403.
- **Q1** No UI text is truncated. New labels and counts follow `docs/standards/ui-copy-standards.md` and are legible at every supported viewport width.
## Approach (Architect §1 C1; settled)
- `canon_verdict`/`promote_unit` (`curate.py:521-562`): when `llm is None and unit.feed == "propose"`, a `hold` verdict becomes `ok`. Feeds `settle`, `stop_delta` and `distill` keep the hold. The model path is unchanged.
- **Release.** Add a rules-only branch to `recheck_held` (`curate.py:571-596`) and stop gating it on `llm` at `run_tick:127`.
- With no llm, select `status_reason == CANON_CHECK_INACTIVE and feed == "propose"` within C0's `scope`, limited to `CURATE_RECHECK_MAX`.
- For each unit, re-run `gates.secret_hit(title, body)`. A hit stays held.
- A clean unit gets `status_reason=None` and `injectable = feed in INJECTABLE_ON_ACCEPT_FEEDS`, then `bump_bundle_revision`.
- Reuse the release tail of `recheck_unit:649-650`; do not copy it.
- Only clear `CANON_CHECK_INACTIVE`. Never clear or overwrite another status_reason, such as C4's `needs repro` (Architect §2.2).
- **Held count.** Add `"held": {feed: count}` to `feed_status` (`curate.py:699-747`) from one grouped count over `accepted ∧ CANON_CHECK_INACTIVE`. The web adds a "Held" line to `FeedStatus` (`ProjectMemoryTab.tsx:363-404`), at the chip type scale, hidden at 0.
- Run the gate on the C0 `scope` keyword and use the C0 fixture `plan_off_project` for the inert test.
Shared edit points (rebase expected): `curate.promote_unit` (C4 also edits it), `curate.run_tick`, and `vocabulary.py`.
Credentials & reach (verbatim rows from Architecture §4):
- All unit/API/web tests, `./dc check full`, and the "local stack" oracles (S1-4 local) | Nothing beyond the worktree and its `./dc` stack's Postgres | verified: worktree present, gate runs in-slot. **Decided:** the "local stack" oracles run as API integration tests. They drive the real agent routes through the FastAPI test client against the slot DB, with a test-minted node caller. `jaah-call` targets the node's control plane (dev/prod), never the `./dc` stack, so the live leg is the dev runner check.
- Dev console checks (S1-2/3/4), operator sign-in on `https://app.dev.jaah.ai` | `jaah/debug/main/operator-login` | verified: on the roster, `task_credential_get` succeeded, and `GET https://app.dev.jaah.ai/api/health` returned 200 from h6-3.
- PR merge and Jaah Deploy watch (E1) | `jaah-forge` (authenticated per task) | verified: a read-only `jaah-forge pr view 5590` returned `state: merged`.
Out of scope:
- From the epic, verbatim: "configuring the production memory model credential (an operator action in Settings › Runners › Project memory, not code); retiring mem-2/mem-3 and purging mem-7834 (operator does it in the console); any change to the OMC notepad/project-memory tier; new embedding providers."
- Do NOT configure or change memory model credential resolution.
- Do NOT change the model-backed canon/secret path.
- Do NOT release settle/stop_delta/distill holds.
- Do NOT change the codemap feed's exclusion (Task 6297).
- Make no migration and no destructive data change.
- Do NOT touch vote weighting (C2), settle evidence (C3), proposal gates (C4) or the hook tier (C5).
Done means:
- S1-1 — a curate test with no model and one unit per feed, all route-hitting, asserts the `propose` unit is injectable and the other three are held. A test with a model asserts no change.
- S1-2 — a test seeds 60 held `propose` units (including one secret-hit) plus 5 held `settle` units. After enough ticks it asserts 59 are released and 6 remain held: the secret-hit plus the 5 settle units. A release that clears the hold without re-scanning secrets FAILS. On dev, open Project › Memory and observe the held count fall over two ticks to only settle, stop_delta and distill units.
- S1-3 — a web test asserts the count renders, and that the line is hidden at 0. An API test checks the count. On dev, open Project › Memory and observe a "Held" count that matches the number of units showing "canon check inactive" in the list, readable and untruncated at every supported viewport width.
- S1-4 — in an API integration test against the slot DB with no `memory_llm_credential_id`: propose a unit via `memory.propose` from an armed task, run one `curate.run_tick`, compose a matching dispatch prompt, and observe `mem-N` in the PROJECT MEMORY section. Then run the same check on dev with a real runner.
- S1-5 — an inert test using `plan_off_project` shows a tick releases nothing and the count route answers 403. Name the test in the PR body.
- E1 — the PR is merged to develop through auto-merge with `./dc check full` green, and the Jaah Deploy run succeeded (or was superseded).
Outcome
With no model, proposals are served and the held propose backlog is released per tick with a secret re-scan. The Memory tab shows a Held count and held rows. Full gate green; PR #5628 merged and Jaah Deploy OK. The dev-console leg was not run (tests and the local stack cover it). UX draft #6713 filed.
Artifacts
Activity
- Draft
- Ready
- Runner started
- Runner ended (done)