Skip to content

refactor(modules): extract approvals + interactive as registry-based modules#1840

Merged
gavrielc merged 2 commits intov2from
refactor/pr3-approvals-interactive
Apr 18, 2026
Merged

refactor(modules): extract approvals + interactive as registry-based modules#1840
gavrielc merged 2 commits intov2from
refactor/pr3-approvals-interactive

Conversation

@gavrielc
Copy link
Copy Markdown
Collaborator

Summary

Phase 2 / PR #3 of the module refactor. Moves the approval and interactive-question flows out of core and into src/modules/, wired through the response dispatcher and delivery action registries added in PR #2.

New modules

  • src/modules/interactive/ — response handler claims pending_questions rows, writes question_response system message, wakes container. createPendingQuestion call stays inline in delivery.ts (guarded by hasTable) per plan.
  • src/modules/approvals/ — 3 delivery actions (install_packages, request_rebuild, add_mcp_server), response handler for pending_approvals (with OneCLI fall-through), adapter-ready hook boots OneCLI, shutdown hook stops it. src/onecli-approvals.ts moved into the module.

Core lifecycle hooks added (narrow, not registries)

  • onDeliveryAdapterReady(cb) in delivery.ts — fires when setDeliveryAdapter runs.
  • onShutdown(cb) in index.ts — fires on SIGTERM/SIGINT.
  • getDeliveryAdapter() getter in delivery.ts — for live-flow adapter access.

Core shrinks

  • delivery.ts 911 → 665 lines
  • index.ts 405 → 224 lines

dispatchResponse now logs "Unclaimed response" instead of falling through to an inline handler.

Migration renames

  • 003-pending-approvals.tsmodule-approvals-pending-approvals.ts
  • 007-pending-approvals-title-options.tsmodule-approvals-title-options.ts

Migration.name fields unchanged so existing DBs treat them as already-applied.

Test plan

  • pnpm run build clean
  • 137/137 host tests pass
  • 17/17 container tests pass
  • Degradation verified: emptying src/modules/index.ts → build clean, 137/137 tests still pass. In that state: delivery actions log "Unknown system action", button clicks log "Unclaimed response".
  • Manual: trigger an install_packages flow end-to-end (admin card → approve → image rebuild → container restart → follow-up prompt).
  • Manual: trigger an ask_user_question flow end-to-end (card delivered → click → response returned to agent).
  • Manual: trigger a OneCLI credential approval (host restart sweeps stale rows to "Expired").

🤖 Generated with Claude Code

…modules

Phase 2 / PR #3 of the module refactor. Moves the approval and interactive-
question flows out of core and into src/modules/, wired through the response
dispatcher and delivery action registries.

New modules:
- src/modules/interactive/ — registers a response handler that claims
  pending_questions rows, writes question_response to the session DB, wakes
  the container. createPendingQuestion call stays inline in delivery.ts
  (guarded by hasTable) per plan.
- src/modules/approvals/ — registers 3 delivery actions (install_packages,
  request_rebuild, add_mcp_server), a response handler for pending_approvals
  (including OneCLI action fall-through), an adapter-ready hook that boots
  the OneCLI manual-approval handler, and a shutdown hook that stops it.
  OneCLI implementation (src/onecli-approvals.ts) moves into the module.

Core lifecycle hooks added (narrow, not registries):
- onDeliveryAdapterReady(cb) in delivery.ts — fires when setDeliveryAdapter
  runs (or immediately if already set). Used by approvals for OneCLI boot.
- onShutdown(cb) in index.ts — fires on SIGTERM/SIGINT. Used by approvals
  for OneCLI teardown.
- getDeliveryAdapter() getter in delivery.ts — for live-flow adapter access
  in registered delivery actions.

Core shrinks: delivery.ts 911 → 665 lines, index.ts 405 → 224 lines.
dispatchResponse now logs "Unclaimed response" instead of falling through
to an inline handler — the inline fallback moved into the two modules.

Migration files renamed to the module-<name>-<short>.ts convention:
- 003-pending-approvals.ts → module-approvals-pending-approvals.ts
- 007-pending-approvals-title-options.ts → module-approvals-title-options.ts
Migration.name fields unchanged so existing DBs treat them as already-applied.

Degradation verified: emptying the modules barrel builds clean and 137/137
tests pass. Actions would log "Unknown system action"; button clicks would
log "Unclaimed response".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gavrielc gavrielc requested a review from gabi-simons as a code owner April 18, 2026 12:22
PR #3 introduced a circular-import temporal-dead-zone bug that didn't
surface in unit tests but crashed the service at startup:

  src/index.ts imports './modules/index.js' for side effects
  → src/modules/interactive/index.ts calls registerResponseHandler()
  → that function is declared in src/index.ts
  → but src/index.ts's const responseHandlers = [] hasn't been
    initialized yet (we're in the middle of its module-init)
  → ReferenceError: Cannot access 'responseHandlers' before initialization

Same issue for registerResponseHandler itself (the function reference
resolves to undefined) and for onShutdown in the approvals module.

Caught when the operator started the service and systemd flagged the
process as crashing in auto-restart loop.

Fix: extract responseHandlers + registerResponseHandler + shutdownCallbacks
+ onShutdown into src/response-registry.ts, which has no dependencies on
src/index.ts or on modules. index.ts re-exports the same surface for any
existing consumers; modules import directly from response-registry.js.

The bug was latent because:
- Unit tests import pieces, never src/index.ts's main() flow.
- Host builds clean because TypeScript doesn't catch runtime circular
  init order.
- Only surfaces when the ES module loader actually executes src/index.ts
  as the entry point.

Verified: service boots on Linux host with approvals + interactive
loaded; OneCLI handler starts via onDeliveryAdapterReady callback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gavrielc gavrielc merged commit 71aab8c into v2 Apr 18, 2026
@gavrielc gavrielc deleted the refactor/pr3-approvals-interactive branch April 18, 2026 12:57
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