AI Agents SDE Task Viewer
      • Context
      • Plan
      • Prd
  1. Home
  2. AgentSDE
  3. agent-core
  4. gh-440
  5. plan
  6. plan.md
plan.md(3.5 KB)· Apr 12, 2026· 2 min read
  • Summary
  • Files
  • Steps
  • Verification
  • Risks
  • Open Questions

Plan: AW-4 — Refactor ClaudeInvocationService to enqueue via BullMQ#

Summary#

Replace the direct child_process.spawn() call in ClaudeInvocationService.invoke() with BullMQ queue enqueue (phase-invoke) + result await (phase-result), decoupling agent invocation from the NestJS process. The SignalResult interface remains unchanged for all callers.

Files#

FileActionDescription
src/invoke/claude-invocation.service.tsmodifyRemove spawn import; replace spawn logic with queue.add() + waitForResult() + file-based signal parsing
src/invoke/invocation-result.listener.tscreateBullMQ processor for phase-result queue; resolves pending promises by jobId, warns on unmatched
src/invoke/invoke.module.tsmodifyRegister BullMQ queues (phase-invoke, phase-result) and InvocationResultListener
src/invoke/claude-invocation.service.spec.tsmodifyReplace child_process.spawn mocks with BullMQ queue mocks; add timeout and missing-file tests

Steps#

  1. Create InvocationResultListener — new file src/invoke/invocation-result.listener.ts. BullMQ @Processor('phase-result') that maintains a Map<string, { resolve, reject }> of pending promises. Expose waitForResult(jobId, timeoutMs): Promise<{ exitCode, stdoutLength }>. On unmatched jobId: log warning, return (no throw). On timeout: reject with a descriptive error.
  2. Refactor ClaudeInvocationService.invoke() — remove spawn import and all child-process logic (stdout/stderr streaming, process event handlers, setTimeout kill chain). Replace with: (a) enqueue job to phase-invoke queue with payload { jobId, taskId, phase, skill, provider, env, taskDir, cwd, timeout, args }; (b) await resultListener.waitForResult(jobId, timeoutSecs * 1000 + 100_000); (c) read {TASK_DIR}/meta/ai-output.jsonl, concatenate text, run signalParser.detectSignal() on the concatenated text; (d) return SignalResult with identical shape. Keep: task-dir creation (mkdirSync), PHASE_TO_SKILL map, buildArgString(), formatStreamEvent() (move to utility or keep for future log replay).
  3. Update InvokeModule — import BullModule.registerQueue({ name: 'phase-invoke' }) and BullModule.registerQueue({ name: 'phase-result' }). Add InvocationResultListener to providers.
  4. Update tests — replace child_process mock with BullMQ queue mock ({ add: jest.fn() }). Mock InvocationResultListener.waitForResult() to return controlled results. Add tests for: timeout propagation, missing ai-output.jsonl (returns empty signal), unmatched jobId warning.

Verification#

  • npm run build passes with no type errors
  • npm run test passes — all existing signal/mapping tests still green with mocked queues
  • npm run lint passes — no new warnings

Risks#

  • AW-3 dependency (#439): BullModule import requires @nestjs/bullmq and Redis connection, which AW-3 provides. If AW-3 is not merged first, this PR will not compile. Mitigation: branch from AW-3 branch or use a minimal inline BullMQ stub per AGENTS.md Wave 2+ guidance.
  • File-based signal parsing timing: The worker writes ai-output.jsonl before enqueuing the result, but a race is possible if the filesystem flush is delayed. Mitigation: the worker must fsync before enqueuing the result message.

Open Questions#

  • Should formatStreamEvent() be preserved as a utility for future log-replay, or removed entirely since real-time streaming is gone?
ContextPrd