Stream agent transcripts live into the Forgejo UI (full observability; heartbeat v1 shipped) #17
Labels
No labels
bug
discussion
duplicate
enhancement
goal
help wanted
horizon:backlog
horizon:governance
horizon:mvp
invalid
operator-decision
question
roadmap
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
open-platform/mitosis#17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The owner's idea: stream the agent's transcript LIVE into the Forgejo UI so a long (now-uncapped) run reads as 'actively building,' watchable from a phone — and so debugging needs no cluster access. The 🔄 heartbeat comment shipped in v0.7.2 is observability v1; this is the full version.
Implementation is already complete and green on
feat/agent-resilience. Build, typecheck, and the 47 tests all pass. Here is my return payload: a short status block, then the issue-ready design proposal.STATUS — resilience + observability (implemented on
feat/agent-resilience)Branch
feat/agent-resilience(3 commits atop the merged dev-scope code) is built, typechecks clean (bunx tsc --noEmit), andbun test→ 47 pass / 0 fail. Working tree clean, not pushed (human deploys via tag).What the code now does, mapped to the root cause:
setTimeout(SIGTERM, timeoutMs)is gone.runner.tsnow runs a polling idle watchdog (idleVerdict,src/runner.ts:154): a run is reaped only onidleTimeoutMsof zero log growth (genuinely hung — a stuck process emits no stream-json) or anabsoluteMaxMsbackstop (builder/worker ≈24h ≈ uncapped; reviewer bounded). The adventuring run (92 tool calls, log growing every few seconds) would now never be killed.RunResult.timeoutKind: "idle" | "absolute"flows intodescribeExit(src/dispatcher.ts:127): the word "crash" is reserved for a true non-zero exit; a reaped run reads "paused after Nm with no activity" / "stopped at the absolute cap." No more "crashed twice" for a working agent.resumeBuilder, bounded at 2) re-spawns against the 🏗️ prime PR; pushed commits survive, so an idle-reaped builder continues rather than restarting.Runner.activeRuns()exposes arunId→{logPath,startedAt}index;extractProgresstail-parses the stream-json firehose (steps, last tool, last text); the dispatcher upserts ONEHB_MARKERcomment per run everyHEARTBEAT_INTERVAL_MS(default 3m). This is the v1 the proposal below supersedes.--disable-dev-shm-usageprimary, plus--no-sandbox/--disable-gpu), addressing theConnection closedsidecar drop.Key files:
/tmp/agents-impl/src/runner.ts,/tmp/agents-impl/src/dispatcher.ts, tests in/tmp/agents-impl/src/runner.test.ts+/tmp/agents-impl/src/dispatcher.test.ts.Proposal: Live agent transcripts streamed into the Forgejo UI
Problem
Runs are now effectively uncapped for genuinely-active agents (good —
slash-/goal+ ultracode builds will run for hours). But "uncapped" only works if a user can see it working. The v1 heartbeat (one comment edited every few minutes with step count + last tool) proves liveness but is not watchable — it's a number that ticks, not a transcript. The owner wants the agent's actual transcript, pretty-printed, streaming live into the Forgejo UI, watchable from a phone, reading unmistakably as "actively building."Options evaluated
A — Run the agent as a Forgejo Actions job (stream into the native job-log UI)
The dispatcher stops spawning
claudelocally and instead triggers a Forgejo Actions workflow;act_runnerchecks out the repo and runsclaude -p --output-format stream-json | prettyso stdout lands in Forgejo's existing live, tailing, ANSI, collapsible, phone-friendly job-log viewer.workflow_dispatch(needs a newForgejo.dispatchWorkflow()method — not in the client today) or pushes a control branch/tag that a workflowon:matches, passing role/issue/PR/model as inputs.PLAT_TOKEN, and the per-run dev secret map become Forgejo Actions secrets — this is exactly the self-serve-secrets (#15) plumbing, reused. The job container must be the agents image (Chromium + MCP baked), so the playwright/plat MCP sidecars run in-job. Clone becomes nativecheckout— a real win.MAX_CONCURRENT+ the builder>worker>reviewer priority queue have no Actions-native equivalent — concurrency becomes "number ofact_runnerreplicas," priority becomes separate labeled runner pools. The priority semantics you just built are lost unless re-modeled.PLAT_TOKEN. This is the adversary-safe-multitenancy north star.act_runneras a new cluster subsystem. Large, multi-month re-architecture; also risks Forgejo's per-job log-size caps on multi-hour firehoses.B — Log-shipping sidecar (tail stream-json → a Forgejo-visible surface, run model unchanged)
Keep dispatcher-spawn; a sidecar tails
/data/logs/<run>.logand writes elsewhere. The three candidate surfaces all disappoint as live surfaces:.plat/transcripts/<run>.md) — viewable in Forgejo's file browser but not live, requires commits, pollutes the branch.Verdict: B as a standalone degrades to "v1++." But its b2 idea (render to a committed
.md) is the right permanent-retention artifact, so we fold it into the winner rather than ship it alone.C — Keep dispatcher-spawn, expose a live transcript via a small platform endpoint/app
The dispatcher already holds everything: the
runningmap,activeRuns()(runId→logPath),extractProgress, and a stream-json firehose persisted on the PVC (the code comment atrunner.ts:330literally anticipates "the future transcript SSE endpoint"). Add a small HTTP server in the dispatcher process that tails the log, pretty-prints each stream-json line, and pushes it over SSE to a tiny static SPA attranscripts.${appsDomain}. The PR/issue heartbeat becomes a "▶ Watch live" link.act_runner, no brain-relocation, no risk to the idle-watchdog/resume you just built.Recommendation — C now, A as the north star (phased C→A); fold B's committed-transcript in as retention
Ship C as v2 because it is the fastest path to the real thing and it reuses the resilience code just landed rather than fighting it. Keep A as the explicit v3 target — its end-state (native Forgejo job-log UI, native artifacts/retention, repo-scoped identity) is strictly better, but only once
act_runner+ secrets(#15) + scoped-identity(#16) are mature enough that the orchestration brain can be expressed as a trigger layer. B is rejected standalone, but its committed-.mdbecomes C's permanent record.Rationale for not leading with A despite its "native UI" appeal: A demands relocating the idle watchdog, resume-on-timeout, reviewer Stop-hook, and the builder>worker>reviewer priority queue — all freshly built, none with Actions-native equivalents — and adds a whole
act_runnersubsystem. C delivers the owner's actual goal (live, pretty, phone-watchable) on top of code that already exists.Winner (C) — design detail
Data flow (unchanged spine, two new read-only consumers):
Runner changes: essentially none —
activeRuns()/logPathare already exposed and logs already survive run cleanup (run()deletes onlywork/<runId>, notlogs/). Add only a terminal "run done + final logPath" signal so the server flips a live stream to replay.Dispatcher changes: add a Bun HTTP server with three routes —
GET /runs(fromactiveRuns()),GET /runs/:id/stream(SSE live tail),GET /runs/:id(replay a finished run from PVC or the committed.md). Change the heartbeat to upsert a low-churn "▶ Watch live: · last:HB_MARKERfor find/replace upsert; drop beat frequency to ~10m since detail now lives in the SPA). Gate the whole thing behindTRANSCRIPT_URL_BASEso it's inert until deployed.Pretty-printer (
renderEvent(json) → TranscriptLine, pure + unit-tested, generalizingextractProgress's parser):system/init→▶ starting <model>assistanttext → markdown bubble ·assistanttool_use→🔧 <tool>(<short args>)user/tool_result→✓ result (<n> lines)/✗ error(long outputs collapsed)result→✅ done — <summary> · <tokens>/<cost>or the timeout/error subtypeRetention: stream-json stays on the PVC with an N-day TTL (live + replay); on completion, render once to
.plat/transcripts/<runId>.mdcommitted to the PR branch — a permanent, in-Forgejo, infra-free record that survives dispatcher restarts (this is B's good idea). No object store needed for v2.Migration from v1 heartbeat: purely additive — no data migration. v1's churning progress comment becomes a static "Watch live" link comment (same
HB_MARKER, same upsert path, lower frequency); the live detail moves to SSE. IfTRANSCRIPT_URL_BASEis unset, behavior falls back to exactly today's v1 heartbeat.Deploy path: the transcript server is the same Bun process as the dispatcher (no new image). Add a
Service+Ingress(transcripts.${appsDomain}) tocharts/agents; ship via tag → open-platform.sh CI builds image+chart → plat/gitops pins the new agents OCIRepository/HelmRelease tag.Phased plan
feat/agent-resilience).renderEventpretty-printer + minimal SPA; heartbeat becomes the live link;charts/agentsService+Ingress; behindTRANSCRIPT_URL_BASE..mdretention on completion, replay of finished runs.act_runner+ Actions-secrets(#15) + scoped identity(#16) + MCP-in-job; evaluate migrating builder/worker once orchestration-in-Actions is proven.Decisions for the owner
.plat/transcripts/<run>.mdto the PR branch (permanent + in-Forgejo, slight branch noise) vs PVC-only (cleaner branch, lost on GC/restart).Observability v1 SHIPPED this session: agents 0.7.x posts a single edit-in-place
🔄 working…heartbeat comment (elapsed/steps/last-action) on the PR/issue during a run, plus the idle-based uncapped timeout (long runs live as long as they emit; killed only when genuinely idle). This issue tracks the FULL version (streaming the live transcript into the Forgejo Actions UI). Design/options in the issue body; recommended path = run the agent AS a Forgejo Actions job so its stream-json stdout streams to the native job-log UI.