Migrate to Postgres + Taskiq (conform to orchestration plan) #1

Merged
kaykayyali merged 2 commits from migrate/postgres-taskiq into main 2026-06-23 18:57:36 +00:00
Owner

Summary

Brings the orchestrator to the design plan: MySQL → Postgres 16 and cron → Taskiq (the Python BullMQ-equivalent over a Redis broker). Postgres FOR UPDATE SKIP LOCKED stays the atomic claim; the per-tick "claim one item, run one phase" model is unchanged. damascus cycle (CLI / bin/run-cycle.sh) remains the deterministic one-shot operator path.

Approved fixes folded in

  • claim_for_merge — removed the call to the non-existent state.claim_for_merge; claim order is now review → build → spec (merge happens inside review on pass).
  • Loop-breaker — a non-pass verdict with attempts >= budget_cycles parks the row as blocked, opens a human_issue, and emits work.blocked (design §5/§16).
  • spec_wrong — added to phases.VERDICTS; refine_spec emits it when the spec is missing required sections (Goal/Acceptance Criteria/TDD Plan/Test Command). Routes to spec (re-run refiner), not awaiting_human — distinct from spec_ambiguous.

Driver / schema

  • PyMySQL → psycopg3 sync (dict_row cursor, Jsonb() for JSONB, %s params).
  • schema.sql rewritten to PG16: guarded CREATE TYPE enums, JSONB, TIMESTAMPTZ, BIGSERIAL, a BEFORE UPDATE trigger replacing MySQL's ON UPDATE CURRENT_TIMESTAMP, v_active_claims view. cli init guard-creates the DB and applies the whole schema in one execute() (incl. DO $$ blocks).

Queue

  • New src/damascus/tasks.py: ListQueueBroker + TaskiqScheduler with a sync run_cycle task (→ cycle.tick()) on a * * * * * cron label. Dockerfile CMD runs the worker; compose adds redis:7 + an orchestrator-scheduler service.

Bugs found & fixed during verification

  1. cycle.py / cli.py status hardcoded /data/status/active.json → now settings.data_dir / "status".
  2. redis-py 8.0.0 DEFAULT_SOCKET_TIMEOUT=5 killed idle Taskiq workers (indefinite BRPOP + uncaught TimeoutError). Broker now sets socket_timeout=None.
  3. compose orchestrator-scheduler pointed at damascus.tasks:broker → fixed to damascus.tasks:scheduler.
  4. tasks.py docstring referenced non-existent --concurrency → corrected to --max-threadpool-threads.

Verification (all green)

  • Schema idempotent against real postgres:16; damascus init end-to-end.
  • 19 contract + unit tests pass against Postgres.
  • damascus cycle smoke: spec→build on pass; forced-fail at budget → blocked + human_issue + work.blocked; spec_wrong→spec; spec_ambiguous→awaiting_human; answer→spec.
  • Taskiq worker path: run_cycle.kiq() → worker runs cycle.tick() → row advances spec→build.
  • Taskiq scheduler path (zero damascus cycle calls): seeded spec row went spec→build (pass) → build→build (tests_failed, retry) → build→blocked (tests_failed) + work.blocked + open human_issue. Proves the queue replaces cron and the loop-breaker via the queue.

Out of scope (deliberately)

  • Larger roadmap items (reviewer assess advisory, sprint reconciler, wiki snapshot-pinning, outbox drainer/overseer, metrics analyzer, fair-share scheduler) — design Phase-gated.
  • E2E suite (tests/e2e/*) stays docker-stack-dependent; CI runs contract + unit only.
  • The Gitea token is still committed in docker-compose.yml:57 — it's a personal admin token. Recommend rotating + moving to a secret.
## Summary Brings the orchestrator to the design plan: **MySQL → Postgres 16** and **cron → Taskiq** (the Python BullMQ-equivalent over a Redis broker). Postgres `FOR UPDATE SKIP LOCKED` stays the atomic claim; the per-tick "claim one item, run one phase" model is unchanged. `damascus cycle` (CLI / `bin/run-cycle.sh`) remains the deterministic one-shot operator path. ### Approved fixes folded in - **`claim_for_merge`** — removed the call to the non-existent `state.claim_for_merge`; claim order is now `review → build → spec` (merge happens inside `review` on `pass`). - **Loop-breaker** — a non-pass verdict with `attempts >= budget_cycles` parks the row as `blocked`, opens a `human_issue`, and emits `work.blocked` (design §5/§16). - **`spec_wrong`** — added to `phases.VERDICTS`; `refine_spec` emits it when the spec is missing required sections (`Goal`/`Acceptance Criteria`/`TDD Plan`/`Test Command`). Routes to `spec` (re-run refiner), **not** `awaiting_human` — distinct from `spec_ambiguous`. ### Driver / schema - PyMySQL → **psycopg3 sync** (`dict_row` cursor, `Jsonb()` for JSONB, `%s` params). - `schema.sql` rewritten to PG16: guarded `CREATE TYPE` enums, JSONB, TIMESTAMPTZ, BIGSERIAL, a `BEFORE UPDATE` trigger replacing MySQL's `ON UPDATE CURRENT_TIMESTAMP`, `v_active_claims` view. `cli init` guard-creates the DB and applies the whole schema in one `execute()` (incl. `DO $$` blocks). ### Queue - New `src/damascus/tasks.py`: `ListQueueBroker` + `TaskiqScheduler` with a sync `run_cycle` task (→ `cycle.tick()`) on a `* * * * *` cron label. Dockerfile `CMD` runs the worker; compose adds `redis:7` + an `orchestrator-scheduler` service. ### Bugs found & fixed during verification 1. `cycle.py` / `cli.py status` hardcoded `/data/status/active.json` → now `settings.data_dir / "status"`. 2. **redis-py 8.0.0 `DEFAULT_SOCKET_TIMEOUT=5`** killed idle Taskiq workers (indefinite `BRPOP` + uncaught `TimeoutError`). Broker now sets `socket_timeout=None`. 3. compose `orchestrator-scheduler` pointed at `damascus.tasks:broker` → fixed to `damascus.tasks:scheduler`. 4. `tasks.py` docstring referenced non-existent `--concurrency` → corrected to `--max-threadpool-threads`. ## Verification (all green) - Schema idempotent against real `postgres:16`; `damascus init` end-to-end. - **19 contract + unit tests** pass against Postgres. - `damascus cycle` smoke: `spec→build` on pass; forced-fail at budget → `blocked` + `human_issue` + `work.blocked`; `spec_wrong→spec`; `spec_ambiguous→awaiting_human`; `answer→spec`. - **Taskiq worker path**: `run_cycle.kiq()` → worker runs `cycle.tick()` → row advances `spec→build`. - **Taskiq scheduler path** (zero `damascus cycle` calls): seeded `spec` row went `spec→build (pass) → build→build (tests_failed, retry) → build→blocked (tests_failed) + work.blocked + open human_issue`. Proves the queue replaces cron **and** the loop-breaker via the queue. ## Out of scope (deliberately) - Larger roadmap items (reviewer `assess` advisory, sprint reconciler, wiki snapshot-pinning, outbox drainer/overseer, metrics analyzer, fair-share scheduler) — design Phase-gated. - E2E suite (`tests/e2e/*`) stays docker-stack-dependent; CI runs contract + unit only. - **The Gitea token is still committed in `docker-compose.yml:57`** — it's a personal admin token. Recommend rotating + moving to a secret.
kaykayyali added 1 commit 2026-06-23 18:01:09 +00:00
Migrate to Postgres + Taskiq (conform to orchestration plan)
Some checks failed
test / contract-and-unit (pull_request) Failing after 8s
e21a8c1f53
Bring the code to the plan: MySQL→Postgres 16 and cron→Taskiq (Python
BullMQ-equivalent over a Redis broker), with Postgres FOR UPDATE SKIP LOCKED
retained as the atomic claim. The per-tick "claim one item, run one phase"
model is unchanged.

Approved fixes folded in:
- claim_for_merge: delete the call to the non-existent state.claim_for_merge;
  claim order is now review→build→spec (merge happens inside review on pass).
- loop-breaker: a non-pass verdict with attempts>=budget_cycles parks the row
  as `blocked` + opens a human_issue + emits work.blocked (design §5/§16).
- spec_wrong: added to phases.VERDICTS and emitted by refine_spec when the
  spec is missing required sections (routes to spec, not awaiting_human).

Driver: PyMySQL→psycopg3 sync (dict_row cursor, Jsonb() for JSONB). schema.sql
rewritten to PG16 (enums, JSONB, TIMESTAMPTZ, BIGSERIAL, BEFORE UPDATE trigger
replacing MySQL ON UPDATE). cli init guard-creates the DB and applies the whole
schema in one execute().

New src/damascus/tasks.py wires ListQueueBroker + TaskiqScheduler with a
run_cycle task (→ cycle.tick()) on a cron label. Dockerfile CMD runs the
worker; docker-compose adds redis:7 + an orchestrator-scheduler service.

Bugs found and fixed during verification:
- cycle.py/cli.py status file was hardcoded to /data; now uses settings.data_dir.
- redis-py 8.0.0 defaults socket_timeout=5s, which killed idle Taskiq workers
  (indefinite BRPOP + uncaught TimeoutError). Broker now sets socket_timeout=None.
- docker-compose scheduler command pointed at :broker; fixed to :scheduler.
- tasks.py docstring referenced non-existent --concurrency; corrected to
  --max-threadpool-threads.

Verified: schema idempotent against postgres:16; damascus init end-to-end;
19 contract+unit tests green; Taskiq worker kiq path advances a row; Taskiq
scheduler path (no damascus cycle call) drives spec→build→retry→blocked +
human_issue, proving the queue replaces cron and the loop-breaker via the queue.

Co-Authored-By: Claude <noreply@anthropic.com>
Author
Owner

🤖 Notes for the other agents — read before touching the orchestrator.

What this branch established

  • DB is Postgres 16 (not MySQL). Authoritative scheduler. Atomic claim = SELECT ... FOR UPDATE SKIP LOCKED in state.py. Keep SKIP LOCKED; it's the whole concurrency story.
  • Queue is Taskiq over Redis (replaces cron). src/damascus/tasks.py is the wiring. The per-tick model is unchanged — run_cycle just calls cycle.tick(). Do not redesign dispatch into per-phase queues; the plan keeps one claim + one phase per tick.

How to run it

taskiq worker    damascus.tasks:broker --max-threadpool-threads $N   # $N = DAMASCUS_MAX_CONCURRENT
taskiq scheduler damascus.tasks:scheduler                            # exactly one of these
  • damascus cycle / bin/run-cycle.sh = deterministic one-shot (operators + E2E). Bypasses the queue.
  • Global concurrency cap = the worker's --max-threadpool-threads (sync tasks run in a threadpool). There is no --concurrency flag in taskiq 0.12.x.

Gotchas you will hit if you forget

  1. redis-py 8.x defaults socket_timeout=5. ListQueueBroker does an indefinite BRPOP; a 5s read timeout raises TimeoutError, which taskiq's listen() does NOT catch (it's a sibling of ConnectionError, not a subclass) → worker dies + restarts in a loop while idle. The broker is constructed with socket_timeout=None — keep that. Don't "helpfully" add a socket_timeout.
  2. taskiq scheduler takes the TaskiqScheduler instance, not the broker. Path is damascus.tasks:scheduler (not :broker). The compose orchestrator-scheduler service is set correctly; don't revert it.
  3. psycopg3 does NOT auto-adapt dict → JSONB. Wrap dict/list values bound to JSONB columns with psycopg.types.json.Jsonb(...). See state.upsert_story / set_phase (last_feedback) / emit_event / cli event inserts. A bare dict → "cannot adapt type 'dict'".
  4. Status file path uses settings.data_dir / "status" / "active.json" (configurable). Don't re-hardcode /data — it breaks anywhere /data isn't writable.
  5. attempts is post-increment. The claim increments it, so in _next_phase_on_verdict compare item["attempts"] >= item["budget_cycles"] directly (no off-by-one). pass is exempt from the breaker.

Verdict routing (cycle._next_phase_on_verdict)

  • pass: review→merged, build→review, spec→build
  • tests_failed / rebase_conflict / no_pr → build (retry)
  • spec_ambiguous → awaiting_human (opens a human_issue in refine_spec)
  • spec_wrong → spec (re-run refiner; no human issue — it's an internally broken spec, not an ambiguity)
  • any non-pass with attempts >= budgetblocked + human_issue + work.blocked event

Still open / next steps (not done in this PR)

  • Rotate the Gitea token in docker-compose.yml:57 (it's a personal admin token in VCS) → move to a secret.
  • Roadmap Phase items are untouched: reviewer assess advisory + lint/build gate, sprint-status.yaml reconciler, wiki snapshot-pinning + merge-gate fact writing, outbox drainer + overseer, metrics analyzer, global spend caps, scope-disjoint/fair-share dispatch.
  • E2E suite (tests/e2e/*) still needs the docker stack; it's not in CI. If you add it to CI, stop the worker service in E2E setup before exec-ing one-shot ticks (worker would race the seeded rows).
  • Changes were verified against postgres:16 + redis:7 containers on host alt-ports (5433/6380) due to WSL2 port conflicts; the real compose uses the docker network.

Tests

pytest tests/contract/ tests/unit/ — 19 passing. Requires live Postgres (DAMASCUS_PG_* + DAMASCUS_ROOT + DAMASCUS_SCHEMA_PATH); run damascus init first.

> 🤖 Notes for the other agents — read before touching the orchestrator. ## What this branch established - **DB is Postgres 16** (not MySQL). Authoritative scheduler. Atomic claim = `SELECT ... FOR UPDATE SKIP LOCKED` in `state.py`. Keep SKIP LOCKED; it's the whole concurrency story. - **Queue is Taskiq over Redis** (replaces cron). `src/damascus/tasks.py` is the wiring. The per-tick model is unchanged — `run_cycle` just calls `cycle.tick()`. Do **not** redesign dispatch into per-phase queues; the plan keeps one claim + one phase per tick. ## How to run it ``` taskiq worker damascus.tasks:broker --max-threadpool-threads $N # $N = DAMASCUS_MAX_CONCURRENT taskiq scheduler damascus.tasks:scheduler # exactly one of these ``` - `damascus cycle` / `bin/run-cycle.sh` = deterministic one-shot (operators + E2E). Bypasses the queue. - Global concurrency cap = the worker's `--max-threadpool-threads` (sync tasks run in a threadpool). **There is no `--concurrency` flag in taskiq 0.12.x.** ## Gotchas you will hit if you forget 1. **redis-py 8.x defaults `socket_timeout=5`.** `ListQueueBroker` does an indefinite `BRPOP`; a 5s read timeout raises `TimeoutError`, which taskiq's `listen()` does NOT catch (it's a sibling of `ConnectionError`, not a subclass) → worker dies + restarts in a loop while idle. The broker is constructed with `socket_timeout=None` — keep that. Don't "helpfully" add a socket_timeout. 2. **`taskiq scheduler` takes the `TaskiqScheduler` instance, not the broker.** Path is `damascus.tasks:scheduler` (not `:broker`). The compose `orchestrator-scheduler` service is set correctly; don't revert it. 3. **psycopg3 does NOT auto-adapt `dict` → JSONB.** Wrap dict/list values bound to JSONB columns with `psycopg.types.json.Jsonb(...)`. See `state.upsert_story` / `set_phase` (`last_feedback`) / `emit_event` / `cli` event inserts. A bare dict → "cannot adapt type 'dict'". 4. **Status file path** uses `settings.data_dir / "status" / "active.json"` (configurable). Don't re-hardcode `/data` — it breaks anywhere `/data` isn't writable. 5. **`attempts` is post-increment.** The claim increments it, so in `_next_phase_on_verdict` compare `item["attempts"] >= item["budget_cycles"]` directly (no off-by-one). `pass` is exempt from the breaker. ## Verdict routing (`cycle._next_phase_on_verdict`) - `pass`: review→merged, build→review, spec→build - `tests_failed` / `rebase_conflict` / `no_pr` → build (retry) - `spec_ambiguous` → awaiting_human (opens a `human_issue` in `refine_spec`) - `spec_wrong` → spec (re-run refiner; **no** human issue — it's an internally broken spec, not an ambiguity) - any non-pass with `attempts >= budget` → **blocked** + `human_issue` + `work.blocked` event ## Still open / next steps (not done in this PR) - Rotate the Gitea token in `docker-compose.yml:57` (it's a personal admin token in VCS) → move to a secret. - Roadmap Phase items are untouched: reviewer `assess` advisory + lint/build gate, `sprint-status.yaml` reconciler, wiki snapshot-pinning + merge-gate fact writing, outbox drainer + overseer, metrics analyzer, global spend caps, scope-disjoint/fair-share dispatch. - E2E suite (`tests/e2e/*`) still needs the docker stack; it's not in CI. If you add it to CI, stop the worker service in E2E setup before exec-ing one-shot ticks (worker would race the seeded rows). - Changes were verified against `postgres:16` + `redis:7` containers on host alt-ports (5433/6380) due to WSL2 port conflicts; the real compose uses the docker network. ## Tests `pytest tests/contract/ tests/unit/` — 19 passing. Requires live Postgres (`DAMASCUS_PG_*` + `DAMASCUS_ROOT` + `DAMASCUS_SCHEMA_PATH`); run `damascus init` first.
kaykayyali added 1 commit 2026-06-23 18:13:20 +00:00
docs: add original plan + reviewed amendment doc
Some checks failed
test / contract-and-unit (pull_request) Failing after 3s
cc7f442c14
Adds docs/multi-project-orchestration-plan_1.md (the original design plan,
previously only in Downloads) and docs/multi-project-orchestration-plan_
amendments.md (the six architecture amendments, reviewed and revised against
the post-migration codebase).

Revisions vs the amendment draft:
- SRVG: mechanical rubric checks are the objective gate; the LLM judge is a
  soft, budget-tolerant fallback (not "objective"). Adds a non-code pipeline
  variant.
- Trust threshold / fair-share: unchanged numbers; flags the §7 metrics
  dependency and SKIP LOCKED claim nuance.
- File scope: reframed from "locking granularity" to claim-time scope-overlap
  policy (no lock layer exists; per-item worktrees isolate; rebase_conflict
  is the signal).
- Budget: fixed cap, default N=3 (was 5), kept per-row configurable;
  spec_ambiguous (awaiting_human) does NOT consume the budget.
- Coordination store: reversed the per-node SQLite injection; Postgres stays
  the sole store, with a lighter filesystem/append-table remediation only if
  telemetry volume is demonstrated.
- Wiki fact trigger: added per-project configurable glob list + manual
  override; notes the §11 merge-gate writer is unbuilt.

Co-Authored-By: Claude <noreply@anthropic.com>
Author
Owner

📋 Added the design docs to this branch (commit cc7f442), side by side in docs/:

  • docs/multi-project-orchestration-plan_1.md — the original design plan (was only in Downloads; now in-repo).
  • docs/multi-project-orchestration-plan_amendments.md — the six architecture amendments, reviewed and revised against the post-migration codebase.

🤖 For the other agents: read the amendments doc before implementing any of the six items. Headline revisions vs the draft:

  • SRVG (§1): mechanical rubric checks are the objective gate; the LLM judge is a soft, budget-tolerant fallback — not "objective". Implies a non-code pipeline variant.
  • Trust threshold (§2) + scope granularity (§3): both depend on the unbuilt §7 metrics component — build that first. §3 is reframed as claim-time scope-overlap policy (there is no file-scope lock layer; per-item worktrees isolate; rebase_conflict rate is the signal).
  • Budget (§4): fixed cap, default N=3 (was 5), kept per-row configurable; spec_ambiguous (awaiting_human) does not consume the budget.
  • Coordination store (§5): reversed the per-node SQLite injection. Postgres stays the sole store; revisit only with demonstrated telemetry-volume evidence.
  • Wiki trigger (§6): per-project configurable glob list + manual override; the §11 merge-gate fact writer is still unbuilt.

Sequencing is at the bottom of the amendments doc. §4 is a one-line default change and can land immediately; §5 is "do nothing now."

📋 Added the design docs to this branch (commit cc7f442), side by side in `docs/`: - `docs/multi-project-orchestration-plan_1.md` — the original design plan (was only in Downloads; now in-repo). - `docs/multi-project-orchestration-plan_amendments.md` — the six architecture amendments, **reviewed and revised** against the post-migration codebase. > 🤖 For the other agents: read the amendments doc before implementing any of the six items. Headline revisions vs the draft: > - **SRVG (§1):** mechanical rubric checks are the objective gate; the LLM judge is a *soft, budget-tolerant fallback* — not "objective". Implies a non-code pipeline variant. > - **Trust threshold (§2) + scope granularity (§3):** both depend on the **unbuilt §7 metrics component** — build that first. §3 is reframed as *claim-time scope-overlap policy* (there is no file-scope lock layer; per-item worktrees isolate; `rebase_conflict` rate is the signal). > - **Budget (§4):** fixed cap, **default N=3 (was 5)**, kept per-row configurable; `spec_ambiguous` (awaiting_human) does **not** consume the budget. > - **Coordination store (§5):** **reversed** the per-node SQLite injection. Postgres stays the sole store; revisit only with demonstrated telemetry-volume evidence. > - **Wiki trigger (§6):** per-project configurable glob list + manual override; the §11 merge-gate fact writer is still unbuilt. > > Sequencing is at the bottom of the amendments doc. §4 is a one-line default change and can land immediately; §5 is "do nothing now."
kaykayyali reviewed 2026-06-23 18:56:27 +00:00
kaykayyali left a comment
Author
Owner

§4 amendment compliance — attempts is not reset when resuming from awaiting_human.

The amendments doc (line ~190, §4 "Which verdicts consume budget") is explicit:

only autonomous failures consume an attempt: tests_failed, rebase_conflict, spec_wrong, no_pr... spec_ambiguous does not consume budget: it routes to awaiting_human, and the item waits on a human — it should not be penalized for a question it asked. The budget resumes counting only on autonomous retries after the human answers and the item returns to spec.

This SET phase='spec' clause returns the row to spec for re-refinement but does not reset attempts. Concrete failure mode:

  1. Row hits awaiting_human after attempts=2 (a spec_ambiguous open question, no autonomous failure).
  2. Human answers.
  3. Next tick re-claims for specattempts becomes 3. The first autonomous failure (e.g. spec_wrong) puts the row over budget_cycles (default 3) and parks it as blocked — even though the human just spent time giving the answer.

Two ways to fix; the second is the one the amendment text implies:

  • (a) Reset attempts to 0 in this UPDATE when transitioning awaiting_human → spec. The autonomous budget starts fresh because the human just removed the blocker.
  • (b) Decrement attempts by 1 (or the count of spec_ambiguous events the row has logged in events_outbox) so the resume is cheaper than a fresh claim but not a full reset. Subtler; needs the spec_ambiguous-event count.

Either is fine; (a) is simpler and matches the plain reading of "the budget resumes counting only on autonomous retries after the human answers." Please add a one-line test in tests/contract/ that:

  • Inserts a row, claims it 2x as spec_ambiguous so attempts=2 and phase='awaiting_human'.
  • Answers via damascus answer.
  • Asserts attempts is now 0 (or 1, if you go with option (b)).
  • Asserts a subsequent claim_for_spec then tests_failed does not park the row.

Without this fix, the §4 amendment as worded is not actually enforced by the code, even though the schema default change in PR #2 makes the budget cap tighter and exposes the bug faster. Worth landing in PR #1 (or a follow-up) before §7 / §2 / §3 work piles more load on the budget loop-breaker.

(Side note: the diff also drops the trailing newline on src/damascus/state.py and src/damascus/tasks.py\ No newline at end of file markers. Not a correctness issue; some linters and git diff UIs complain. Worth a one-character fix on rebase.)

**§4 amendment compliance — `attempts` is not reset when resuming from `awaiting_human`.** The amendments doc (line ~190, §4 "Which verdicts consume budget") is explicit: > only autonomous failures consume an attempt: `tests_failed`, `rebase_conflict`, `spec_wrong`, `no_pr`... `spec_ambiguous` does **not** consume budget: it routes to `awaiting_human`, and the item waits on a human — it should not be penalized for a question it asked. The budget resumes counting only on autonomous retries after the human answers and the item returns to `spec`. This `SET phase='spec'` clause returns the row to `spec` for re-refinement but does not reset `attempts`. Concrete failure mode: 1. Row hits `awaiting_human` after `attempts=2` (a `spec_ambiguous` open question, no autonomous failure). 2. Human answers. 3. Next tick re-claims for `spec` → `attempts` becomes 3. The first autonomous failure (e.g. `spec_wrong`) puts the row over `budget_cycles` (default 3) and parks it as `blocked` — even though the human just spent time giving the answer. Two ways to fix; the second is the one the amendment text implies: - **(a) Reset `attempts` to 0** in this UPDATE when transitioning `awaiting_human → spec`. The autonomous budget starts fresh because the human just removed the blocker. - **(b) Decrement `attempts` by 1** (or the count of `spec_ambiguous` events the row has logged in `events_outbox`) so the resume is *cheaper* than a fresh claim but not a full reset. Subtler; needs the spec_ambiguous-event count. Either is fine; (a) is simpler and matches the plain reading of "the budget resumes counting only on autonomous retries after the human answers." Please add a one-line test in `tests/contract/` that: - Inserts a row, claims it 2x as `spec_ambiguous` so `attempts=2` and `phase='awaiting_human'`. - Answers via `damascus answer`. - Asserts `attempts` is now 0 (or 1, if you go with option (b)). - Asserts a subsequent `claim_for_spec` then `tests_failed` does not park the row. Without this fix, the §4 amendment as worded is not actually enforced by the code, even though the schema default change in PR #2 makes the budget cap tighter and exposes the bug faster. Worth landing in PR #1 (or a follow-up) before §7 / §2 / §3 work piles more load on the budget loop-breaker. (Side note: the diff also drops the trailing newline on `src/damascus/state.py` and `src/damascus/tasks.py` — `\ No newline at end of file` markers. Not a correctness issue; some linters and `git diff` UIs complain. Worth a one-character fix on rebase.)
kaykayyali merged commit 60cc8d7586 into main 2026-06-23 18:57:36 +00:00
kaykayyali deleted branch migrate/postgres-taskiq 2026-06-23 18:57:36 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kaykayyali/damascus-orchestrator#1