"Activity" (placeholder name — jobs/tasks were taken) = watch background tasks
scroll in parallel with chat. Built DB-free; NOT restarted — deploy + test in
the morning.
- NDJSON progress contract (activity/progress.ts): capabilities append
{job,cap,phase,status,pct,detail,ts,...} lines; tolerant parser treats any
JSON object with phase/status as structured progress, else a raw log line.
- Backend (activity/router.ts, owner-only, path-guarded):
- GET /api/activity/tasks — registry by scanning /tmp/claude-*/<cwd>/tasks/
*.output (harness run_in_background) + announced detached jobs.
- POST /api/activity/announce {name,path} — register a detached (setsid) job's
log so it's followable too (the setsid case is on the critical path, since
the warm worker now makes plain run_in_background the default for heavy jobs).
- GET /api/activity/stream?task=<id>|path=<abs> — SSE tail (poll + offset),
emitting {kind:'line'|'progress'} with NDJSON parsed.
- Frontend /activity screen + Radio nav item: task list (active dot) → live tail
with a phase/pct progress header + raw log, following the /system-monitor pattern.
- Retention: startChatEventRetention() prunes chat_session_events >7d every 6h
(wired in bootstrap) so the durable queue stays bounded.
Verified headlessly (no restart): parseTailLine classification, and the scan
finds 53 real task output files. Endpoints + UI untested until deploy.
Deferred (see handoff): cross-device sync + OpenCode parity (both touch the
now-stable chat path — won't ship un-restart-tested); task:progress into chat.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
40 lines
1.6 KiB
TypeScript
40 lines
1.6 KiB
TypeScript
// Shared structured-progress contract for the Activity feature.
|
|
//
|
|
// Any capability that wants live progress bars / phase chips (instead of the UI regexing prose) appends
|
|
// NDJSON lines of this shape to its output/log file, alongside whatever human text it already writes:
|
|
//
|
|
// {"job":"dearly-devoted","cap":"split-audiobook","phase":"transcription","status":"running","pct":9,"detail":"0:50:00 / 9:28:24","ts":1730000000000}
|
|
// {"job":"dearly-devoted","cap":"split-audiobook","phase":"verdict","status":"complete","verdict":"CLEAN","chapters":31,"pieces":52,"ts":...}
|
|
//
|
|
// The parser is deliberately tolerant: a line is treated as structured progress iff it JSON-parses to
|
|
// an object carrying a `phase` or `status`; everything else is passed through as a raw log line. So a
|
|
// capability can dual-write (human lines + NDJSON) and both render correctly.
|
|
|
|
export type ProgressLine = {
|
|
job?: string;
|
|
cap?: string;
|
|
phase?: string;
|
|
status?: 'running' | 'complete' | 'failed' | string;
|
|
pct?: number;
|
|
detail?: string;
|
|
ts?: number;
|
|
[k: string]: unknown;
|
|
};
|
|
|
|
export type TailEvent = { kind: 'progress'; progress: ProgressLine } | { kind: 'line'; text: string };
|
|
|
|
export function parseTailLine(line: string): TailEvent {
|
|
const trimmed = line.trim();
|
|
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
try {
|
|
const obj = JSON.parse(trimmed) as Record<string, unknown>;
|
|
if (obj && (typeof obj.phase === 'string' || typeof obj.status === 'string')) {
|
|
return { kind: 'progress', progress: obj as ProgressLine };
|
|
}
|
|
} catch {
|
|
/* not JSON → fall through to raw line */
|
|
}
|
|
}
|
|
return { kind: 'line', text: line };
|
|
}
|