feat: post-review backlog hardening (cancel/parser/stall/history/9502)
Five independent items from the post-review backlog. F1: Stop on an external agent task now aborts the running child via a per-task AbortController registry reachable from the cancel route, and finalizes the assistant message as cancelled (fixing two latent bugs — catch blocks left the message streaming, and warm success-paths wrote complete on an aborted turn); warm pools/worktrees are preserved and the native path is unchanged. F2/F3: prune the tool-call parser to its two load-bearing exports (unexport eight zero-caller symbols, add a gate test for the <invoke>-as-text fallback) and route placeholder-rejection logging through pino. F6: a 90s per-chunk stall-timeout wraps native inference's fullStream via AbortSignal.any so a hung stream finalizes the message instead of hanging — no retry (a pure classifyStreamError helper is added). F7: a read-only view_session_history MCP tool (newest-N, chronological). F9: retire the unused apps/coder/web :9502 fallback SPA, keeping every API/WS/health/MCP route. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
50
apps/coder/src/services/cancel-registry.ts
Normal file
50
apps/coder/src/services/cancel-registry.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* F1 — per-task abort registry. A Stop on an external-agent task must reach the
|
||||
* in-flight run and abort its child / prompt. Each external run-function registers
|
||||
* its per-turn AbortController here keyed by task id; the cancel route calls
|
||||
* `cancel(taskId)` to fire it; the run-function's `.finally` deletes the entry.
|
||||
*
|
||||
* Idempotent by construction:
|
||||
* - `cancel()` on an already-aborted controller no-ops (AbortController.abort()
|
||||
* is idempotent) → a rapid double-Stop is safe.
|
||||
* - `cancel()` on an unknown / already-finished task returns false → a
|
||||
* cancel-after-natural-exit (entry already deleted) and a Stop on a native
|
||||
* boocode task (never registered) are both safe no-ops.
|
||||
*
|
||||
* Pure (no DB / child / IO) so the abort wiring + idempotency contract is
|
||||
* unit-testable in isolation — mirrors the turn-guard / lifecycle-decisions
|
||||
* pure-helper precedent.
|
||||
*/
|
||||
export interface CancelRegistry {
|
||||
/** Create + store an AbortController for this task, returning it for the run. */
|
||||
register(taskId: string): AbortController;
|
||||
/** Abort the task's in-flight run. Returns false when no controller is registered. */
|
||||
cancel(taskId: string): boolean;
|
||||
/** Drop the task's entry (called from the run's `.finally`). No-op if absent. */
|
||||
delete(taskId: string): void;
|
||||
/** Whether a controller is currently registered for this task. */
|
||||
has(taskId: string): boolean;
|
||||
}
|
||||
|
||||
export function createCancelRegistry(): CancelRegistry {
|
||||
const controllers = new Map<string, AbortController>();
|
||||
return {
|
||||
register(taskId) {
|
||||
const ac = new AbortController();
|
||||
controllers.set(taskId, ac);
|
||||
return ac;
|
||||
},
|
||||
cancel(taskId) {
|
||||
const ac = controllers.get(taskId);
|
||||
if (!ac) return false;
|
||||
ac.abort();
|
||||
return true;
|
||||
},
|
||||
delete(taskId) {
|
||||
controllers.delete(taskId);
|
||||
},
|
||||
has(taskId) {
|
||||
return controllers.has(taskId);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user