diff options
| -rw-r--r-- | internal/api/taskops.go | 7 | ||||
| -rw-r--r-- | web/app.js | 9 | ||||
| -rw-r--r-- | web/test/stats.test.mjs | 16 |
3 files changed, 30 insertions, 2 deletions
diff --git a/internal/api/taskops.go b/internal/api/taskops.go index 254d73c..e6b6239 100644 --- a/internal/api/taskops.go +++ b/internal/api/taskops.go @@ -110,7 +110,12 @@ func (s *Server) submitTask(ctx context.Context, spec submitTaskSpec) (*task.Tas return nil, err } t.State = task.StateQueued - if err := s.pool.Submit(ctx, t); err != nil { + // Use the server's long-lived lifecycle context, not the caller's request + // context — the request (REST or chatbot MCP tool call) returns long + // before the task finishes running, and its context is cancelled when it + // does. Submitting with it would cancel the task's execution moments + // after dispatch. Mirrors the pattern at server.go's other Submit call sites. + if err := s.pool.Submit(s.ctx, t); err != nil { return nil, err } return t, nil @@ -665,7 +665,14 @@ export function computeExecutionStats(executions) { for (const e of executions) { const outcome = e.state || 'unknown'; byOutcome[outcome] = (byOutcome[outcome] || 0) + 1; - if (outcome === 'completed') completed++; + // Executions carry the task state machine's uppercase values (see + // internal/executor.handleRunResult): a top-level task (no parent) that + // succeeds lands its execution at READY, not COMPLETED — COMPLETED is + // only set for subtask executions. Both represent a successful run from + // the executor's perspective; only the human/chatbot accept step (a + // separate, later transition) turns READY into COMPLETED. + const normalized = outcome.toUpperCase(); + if (normalized === 'COMPLETED' || normalized === 'READY') completed++; totalCost += e.cost_usd || 0; if (e.duration_ms != null) { durationSum += e.duration_ms; diff --git a/web/test/stats.test.mjs b/web/test/stats.test.mjs index a7fe657..19f2b1e 100644 --- a/web/test/stats.test.mjs +++ b/web/test/stats.test.mjs @@ -150,4 +150,20 @@ describe('computeExecutionStats', () => { assert.equal(stats.byOutcome.failed, 1); assert.equal(stats.byOutcome.cancelled, 1); }); + + // READY is the execution state for a successful top-level (no parent) task + // run — see internal/executor.handleRunResult. COMPLETED is only reached + // after a separate human/chatbot accept step, so READY must count as + // success here or every top-level task's success rate reads as 0. + it('counts READY executions as successful', () => { + const execs = [makeExec('READY'), makeExec('READY'), makeExec('FAILED')]; + const stats = computeExecutionStats(execs); + assert.equal(stats.successRate, 2 / 3); + }); + + it('counts real-world uppercase state values the same as lowercase fixtures', () => { + const execs = [makeExec('COMPLETED'), makeExec('READY'), makeExec('FAILED'), makeExec('CANCELLED')]; + const stats = computeExecutionStats(execs); + assert.equal(stats.successRate, 0.5); + }); }); |
