Skip to content

fix(dag): cross-instance adoption guard, subscription defect survival, cancel-skip race - #131

Merged
LeXwDeX merged 3 commits into
devfrom
fix/dag-guards
Jul 28, 2026
Merged

fix(dag): cross-instance adoption guard, subscription defect survival, cancel-skip race#131
LeXwDeX merged 3 commits into
devfrom
fix/dag-guards

Conversation

@LeXwDeX

@LeXwDeX LeXwDeX commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes four verified defects in the DAG module (plan: DAG 缺陷修复计划), plus a husky typecheck fallback for sandboxed checkouts.

P0 — Cross-instance workflow adoption (incident root cause)

DagLoop is per-directory InstanceState, but its event subscriptions and recovery scans had no project filter: with multiple directories mounted on one server, ANY instance could adopt ANY workflow, and the first spawn wave could run children under a foreign directory context (observed live: an explore node spawned into a different project and asked the user where the source tree was).

  • recoverWorkflow (covers startup scan + WorkflowReplanned entry-miss), WorkflowStarted handler, and startup wake redelivery now adopt only workflows whose projectId matches the instance.

P1 — Subscription death by defect

Effect.ignore only absorbs the error channel; orDie defects (e.g. transient SQLITE_BUSY inside store.getWorkflow) punched through and killed the forked runForEach fiber — that event type stayed permanently unhandled for the process lifetime. All 9 subscription handlers plus the idle wake trigger now end in a catchCause boundary (guarded).

P1 — Cancel-skip race leaks child sessions

Workflow-level cancel publishes NodeSkipped for running nodes; when that handler won the cross-stream race against WorkflowCancelled it deleted the fiber uninterrupted, so the child session kept running until prompt completion/timeout. The NodeSkipped handler now aborts live fibers (abortChild + interrupt), mirroring NodeCancelled.

P2 — Scheduling hot path wrote user config

DagConfig.load auto-seeded the global dag.jsonc on every scheduling round with all write errors swallowed. Seeding is now opt-in (autoSeed), happens once at instance init, and non-EEXIST failures are logged.

P2 — State machine drift guard

The declared transition tables and the projector's SQL from-guards had already drifted (PAUSED→SKIPPED happens in production but was undeclared; the projector accepted pending→completed which nothing produces). The node table now matches production paths, projector guards are exported as NodeStatusProjection/WorkflowStatusProjection (NodeCompleted tightened to running), and a new drift test welds the two encodings together (completed→running reopen is the single documented exemption).

chore(husky)

bun typecheck dies with CouldntReadCurrentDirectory in sandboxed checkouts whose ancestors are unreadable; tsgo -b buildinfo writes can be denied (TS5033). Hooks now fall back to per-package typecheck on exactly those environment defects; real type errors still fail.

Tests

  • new packages/opencode/test/dag/dag-loop-guards.test.ts: foreign-project non-adoption (WorkflowStarted + recovery), own-project control, defect survival, skip-interrupts-live-child — 5 pass
  • new packages/core/test/dag-projector-drift.test.ts: projector guards ⊆ declared tables — 3 pass
  • updated dag-config tests (pure-read default / opt-in seed / non-EEXIST degradation)
  • packages/opencode test/dag: 249 pass; packages/core: 1129 pass (single failure is the pre-existing environment-bound Watcher .git/HEAD test, unrelated)
  • typecheck (tsgo --noEmit): both packages clean

LeXwDeX added 3 commits July 28, 2026 21:30
… race

- Adopt workflows only in the owning project's instance (recoverWorkflow, WorkflowStarted, wake redelivery) so multi-directory servers no longer spawn children under a foreign directory context
- Replace handler-level Effect.ignore with a catchCause boundary so orDie defects from transient DB errors cannot kill event subscriptions for the process lifetime
- Abort still-live fibers (abortChild + interrupt) when NodeSkipped lands on a running node, closing the cancel-race child session leak
- Seed global dag.jsonc once at instance init via opt-in autoSeed; scheduling rounds stay pure reads and non-EEXIST seed failures are logged
- Align the node PAUSED transitions with production paths and weld projector from-guards to the declared tables via exported projections + drift test
…iscovery fails

bun's ancestor-directory workspace walk dies with CouldntReadCurrentDirectory in sandboxed checkouts, failing hooks on an environment defect. The shared runner keeps 'bun typecheck' as the primary path and falls back to running each package's own typecheck script only on that specific error; real type errors still fail on either path.
…fallback

tsgo -b packages (app/desktop) can fail with TS5033 'operation not permitted' writing .tsbuildinfo under sandboxed hook contexts. Tolerate a package failure only when TS5033 is the sole TS diagnostic; real type errors still fail.
@LeXwDeX
LeXwDeX merged commit 6e14c78 into dev Jul 28, 2026
4 checks passed
@LeXwDeX
LeXwDeX deleted the fix/dag-guards branch July 28, 2026 13:42
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