Skip to content

fix(concurrency): make lifecycle and durable state transitions ownership-safe (#766 + #767) - #791

Merged
unohee merged 2 commits into
mainfrom
salvage/concurrency-locking
Sep 28, 2026
Merged

unohee merged 2 commits into
mainfrom
salvage/concurrency-locking

Conversation

@unohee

@unohee unohee commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Salvaged from two drafts left by a failed autonomous run (both said "failed 4 times … very likely incomplete"): #766 (lifecycle/durable state transitions ownership-safe) and #767 (atomic + validated task and graph state updates). Merged into one coherent change and rebased onto current main (they were 174-247 commits behind, and #766/#767 collided with each other on fileLock.ts / taskState/store.ts / automation/runnerState.ts).

Kept

  • src/support/fileLock.ts: one withFileLockSync (Atomics.wait for the wait) with ownership-safe stale-lock reclaim and ENOTEMPTY tolerance folded in from both drafts.
  • src/agents/pairPipeline.ts + pairPipelineTypes.ts: an AsyncLocalStorage per-run RunControl replaces the shared mutable abortSignal/stuckDetector (cross-run cancellation isolation), with a regression test.
  • src/adapters/processRegistry.ts: taskId/spawnedAt ownership checks + a cancellable, unref'd escalation timer.
  • src/taskState/store.ts: atomic tryClaimTaskAdmission claim + ENOTEMPTY-tolerant reclaim; src/orchestration/decisionEngine.ts admission claims + in_progress filter; src/orchestration/taskParser.ts zod schemas.
  • Cross-process locks: memoryCore (withCrossProcessMemoryMutationLock), codex, reembed, runnerState locked RMW sites; automation/runnerState.ts rejection counters; support/logRotation.ts restore-from-.1.
  • src/knowledge/gitInfo.ts: enrichment now actually persists (graph.addNode + saveGraph); src/locale/index.ts AsyncLocalStorage locale scope; auth/oauthStore.ts.

Dropped (with reason)

Conflict resolution — main's hardening wins wherever a draft conflicted with it (withRunnerStateLock, reserveDailyCreations, upsertTaskStateUnlocked, the documented updateTaskLinearState lock invariant), while keeping each draft's unique value (tryClaimTaskAdmission, ENOTEMPTY reclaim). pairPipeline re-derives PipelineContext.abortSignal/stuckDetector on top of the AsyncLocalStorage control rather than picking a side.

Bugs found and fixed during verification

  • withFileLockSync is not reentrant → resetDailyCounterIfNeeded now runs outside the decomposition lock.
  • Lock waits used the test-faked setTimeout → the real timer is captured at module load.
  • The prProcessor state lock used a global path with no test seam → OPENSWARM_PR_PROCESSOR_STATE_FILE override (that file change itself was dropped: the repo LOC gate caps prProcessor.ts at 1500 lines and main is already exactly there).

Verification

…hip-safe

Salvages the unique work of draft PRs #766 and #767 into one change.

- fileLock: add withFileLockSync (Atomics wait) for sync RMW callers, with the
  ownership-safe stale reclaim and ENOTEMPTY-tolerant unlink; lock waits use the
  real timer so a test that fakes setTimeout cannot freeze lock hand-off.
- pairPipeline: replace the per-instance abortSignal/stuckDetector with an
  AsyncLocalStorage RunControl, so concurrent run() calls on one instance cannot
  observe or overwrite each other's cancellation state.
- processRegistry: taskId+spawnedAt identity ownership on activity, close and
  health-check, plus a SIGKILL escalation timer that is unref'd, cancelled on
  child close, and cleared by a force kill.
- store: tryClaimTaskAdmission claims a task in one locked read-modify-write;
  stale-lock reclaim tolerates ENOTEMPTY; main's updateTaskLinearState lock
  invariant is preserved.
- runnerState: cross-process locks on the rejection, decomposition, pace,
  project-selection and pipeline-history RMW paths; main's withRunnerStateLock
  and reserveDailyCreations are kept, and the daily reset runs outside the
  decomposition lock because withFileLockSync is not reentrant.
- decisionEngine: admission claims on the auto-execute paths and an in_progress
  filter; file-locked engine state and backlog appends.
- oauthStore, dev, logRotation, codex, reembed, taskParser, gitInfo, locale:
  the locked or scoped variant of the same invariant.

Dropped: telemetry.ts (main has withTelemetryLock), workflow.ts (owned by
another salvage), prProcessor.ts (its state lock cannot fit the repo's
1500-line gate, which main's copy already sits on), task_state_model.py,
agt3420-probe.txt, and package.json/lock version churn.
…hip-safe

Salvages the worthwhile work from draft PRs #766 and #767 (both 'failed 4
times … very likely incomplete', 174-247 commits behind) into one change,
rebased onto current main.

Kept: unified withFileLockSync (Atomics.wait + ownership-safe reclaim +
ENOTEMPTY tolerance); AsyncLocalStorage per-run RunControl in pairPipeline;
processRegistry taskId/spawnedAt ownership + cancellable unref'd timer;
taskState tryClaimTaskAdmission + ENOTEMPTY reclaim; decisionEngine admission
claims + in_progress filter; taskParser zod schemas; cross-process locks in
memoryCore/codex/reembed/runnerState; gitInfo enrichment now persists via
graph.addNode+saveGraph; locale AsyncLocalStorage scope; oauthStore refresh
serialized on a store lock with reload-under-lock.

Also updates the duck-typed AuthProfileStore double in codexResponses.test.ts
(reloadProfileFromDisk/setProfileUnlocked) so it implements the interface its
declared collaborator now requires — the adapter's tests are the only caller
that is not a real AuthProfileStore instance.
@unohee
unohee force-pushed the salvage/concurrency-locking branch from a953bd4 to ca2805a Compare September 28, 2026 07:11
@unohee
unohee merged commit c090958 into main Sep 28, 2026
7 checks passed
@unohee
unohee deleted the salvage/concurrency-locking branch September 28, 2026 07:21
unohee added a commit that referenced this pull request Sep 29, 2026
…dict reuse (#794)

* feat(agents): port harness agent patterns — advisor role + declarative per-role tool/effort scoping

Cherry-picked onto origin/main (the original branch drifted 10 commits).
Conflicts were additive on both sides (main's --harness-only path + this
advisor wiring) and were resolved by keeping both.

* fix(ledger): recovery is idempotent — a recomputed completion effect no longer kills the heartbeat (AGT-4518)

Cherry-picked onto origin/main. Verified main still has the defect:
recoverPublishedRun compares payload_json (~runLedger.ts:1071) and
reconcileDurableArtifacts has no per-row isolation.

* fix(taskSource): paginate the local queue instead of silently dropping past 200 (AGT-3421)

Cherry-picked onto origin/main (main still has the fixed limit:200/offset:0).

* fix(adapters): name protectedFiles/forbidPublication in the delegated-CLI dropped-options warning (AGT-4444)

Cherry-picked onto origin/main; main warns for mcpTools/coordinationContext only.

* feat: port the still-unique half of the audit remediation onto main

Main advanced 10 commits while `feat/omp-agent-patterns` was open, and its
salvage PRs (#785-#791) fixed several of the same issues independently — its
own way. Rather than rebase 10 commits through wide conflicts, this branch was
recreated from `origin/main` and only the parts main does NOT already have were
ported. Every ported item was checked against main first, with the evidence
recorded, and where main's version was newer or better (memory pagination, the
tester prompt bound, TUI width clipping, IPv6 callbacks, pipeline embed budgets)
main's version was kept and nothing was ported.

Still missing on main, now ported:

- `shellCommandGuard.ts` — the guard is lexical on main (`BLOCKED_COMMANDS`),
  so 6 of 21 destructive forms execute (`r{m,} -rf`, `$'\x72\x6d' -rf`, `r"m"
  -rf`, `git clean -fdx`, …) while 4 of 51 harmless mentions are falsely
  blocked. Replaced with a bash-faithful resolver: 72/72 verdicts correct.
- `read_file` — main reads the whole file before slicing; a 512 MiB file with
  `limit=1` throws `RangeError: Invalid string length`. Now a bounded window.
- `recoverPublishedRun` idempotency + per-row reconcile isolation (AGT-4518):
  main still compares `payload_json`, so a recovery of an already-enqueued
  completion throws and kills the heartbeat.
- `taskSource` pagination (AGT-3421): main still caps the local queue at 200.
- Knowledge scanning incompleteness (AGT-3490): a walk stopped by depth,
  timeout or size still persists a graph indistinguishable from a complete one.
- `gitInfo` discovery: main misses staged-but-uncommitted files entirely and
  newline-splits the log query, so a path containing a newline is corrupted.
  Also main strips a leading `\n` from EVERY filename token, though real
  `git log -z` prefixes only the first — verified against real git bytes.
- String-aware JSON extraction for auditor/documenter (main is brace-blind), and
  the documenter prompt now delimits untrusted task/worker text.
- Verification-evidence log bound: main joins every failing log before capping,
  so one suite's output displaces another's and the total is measured post-join.
- Memory: LanceDB returns the vector column as an Arrow `Vector`, which main's
  `normalizeRecords` zeroes — embeddings are destroyed on rewrite and cosine
  similarity over stored rows is NaN, so dedup matched NOTHING. Confirmed
  against a live store. Plus the survivor refusal bound and non-quadratic
  consolidation (main's is still all-pairs under the global write lock).
- Automation: `fixOne` bypasses the PR lease; decomposition capacity reads a
  PROCESS-LOCAL counter, so two real processes are both granted a slot against
  a cap of 1 (reproduced with real spawned processes).
- Daily reporter: main republishes every already-succeeded project on retry.
- Python task-state strictness (main's own test file is currently red: 5
  failed), CI-wait duration validation, verify-manifest command/runtime caps,
  and the notifier delegating to the shared predicate — with TEST-NET-2/3 added
  so delegating does not regress what main's local table rejected.
- CLI output bound, tester/dashboard hardening, Retry-After HTTP-date parsing
  where main still parses with `parseInt`, and the delegated-CLI warning now
  naming `protectedFiles`/`forbidPublication` in addition to the tool list.
- `advisor` role + declarative per-role `tools`/`effort` (the harness patterns),
  which main does not have at all.

Verification: `tsc` clean, `oxlint` clean, full suite 7167 passing. Each ported
item carries a test that fails against `origin/main` and passes here; several
were additionally mutation-checked (removing the bound/coercion/bucket key makes
the named test fail).

* fix(review --max): make the audit verdict independent of --concurrency (AGT-4597)

`--concurrency` was a partition input on the audit path, not just a resource
knob: `balanceAreasToConcurrency` re-split the source with a smaller per-area
cap until `areas.length >= concurrency`, and `reviewMaxCommand` called it. So
the same files and the same reviewer were judged in 2 units at concurrency 1
and 10 units at concurrency 8 — and since `aggregateAuditResults` is worst-wins
(`decision: failed ? 'reject' : worst`), a finer split could only turn an
approve into a revise/reject.

Reproduced deterministically (20 files in one dir, cap 12, a reviewer that
judges the unit it is handed): concurrency 1 -> 2 areas -> approve;
concurrency 8 -> 10 areas -> reject. No LLM call, no timing. The feature that
introduced it, INT-2249 / PR #183, records its own verification as "2 dirs, 10
files: concurrency 2 -> 2 areas, concurrency 8 -> 10 areas".

The audit now partitions from `--max-files-per-area` alone via
`planAuditAreas`; `--concurrency` only decides how many reviewers run at once.
The capability INT-2249 wanted is kept, not dropped: the finer, more parallel
fan-out is reached deterministically with `--max-files-per-area <n>` (cap 2
yields the same 10 areas the concurrency-8 path fabricated), now reproducible at
any concurrency. The `--fix` path keeps `balanceAreasToConcurrency` unchanged —
there is no verdict there and more areas just mean more parallel fix workers.

Verified on the real CLI (`--dry-run` over this repo): 478 files ->
61 areas at concurrency 1, 8 and 32 alike; cap 4 -> 134 areas at 4 and 32.
The regression test drives the real command entry and fails when the partition
is re-derived from concurrency.

* release: 0.24.4 — durable review-verdict reuse (Tier 1)

Promotes the Unreleased section to 0.24.4 and adds the durable
review-verdict store: a review of content this deployment already
reviewed is replayed from `(kind, base, content digest)` instead of
paying for the reviewer again, and the store survives a daemon restart.

Measured over the recorded history (530 records, 512 with a verdict):
68 byte-identical pairs, 15 same-mode, 10 repeating the same verdict —
~10 of 512 reviews, at ~$0.26 and 93s p50 per reviewer call. Proven
across two separate processes: 27.0s / 6 model calls became a 1.58s
replay with zero model calls, same verdict. The advisor pass still runs
over a reused verdict, so a replay can tighten but never fail open.

Also corrects the README's `review --max` claim that areas "auto-split
to fill --concurrency" — the partition has been derived from
--max-files-per-area alone since AGT-4597, so --concurrency no longer
changes the verdict.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant