opc/main
15
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
121d05f875 |
feat(docker): auto-stage host OAuth credentials into agent containers
The docker harness now detects and stages host CLI auth files (~/.gemini/oauth_creds.json, ~/.claude/.credentials.json) into a writable agent-home directory mounted at /home/agent. This lets containerized agents reuse the host's Gemini Pro / Claude Pro OAuth sessions without manual secret management or API keys. Only auth files are copied — not the host's settings.json or MCP configs (which contain stale localhost URLs that would hang Gemini CLI inside containers). The staged dir is writable so CLIs can create projects.json, history, etc. alongside the auth files. Also sets GEMINI_DEFAULT_AUTH_TYPE=oauth-personal and GEMINI_CLI_NO_RELAUNCH=true when OAuth creds are detected, writes Claude's hasCompletedOnboarding flag, and simplifies the doc-gardener example to use the harness-level credential staging instead of manual HOME directory seeding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
42f8256df6 |
feat(goals): complete_goal MCP tool + draft→active auto-transition
Three improvements that turn the doc-gardener demo from "runs but stays in 'draft' forever" into a goal that properly transitions through its lifecycle and renders a completion summary on /goals/<id>. ### 1. complete_goal MCP tool (#59, #62) New tool surface: complete_goal(goal_id, status, summary, completion_message_id?) The critic calls this from inside the sandbox after it sends its FINAL: DM. Records the one-paragraph human-readable summary on the goal row plus a pointer to the message that carried the FINAL text, so the Web UI /goals/<id> page has both the verdict and a deep link to the full findings JSON. Status parameter accepts completed | stuck | cancelled. Idempotent when called with the current status. Rejects callers owned by a different human than the goal owner. Plumbing: - New migration 026_goals_completion_summary.sql adds two columns to goals: completion_summary TEXT, completion_message_id INTEGER (FK messages.id, ON DELETE SET NULL). - internal/goals/types.go: new CompletionSummary + CompletionMessageID fields on Goal struct. - internal/goals/store.go: Get/List Scan both new columns; SetCompletion(goalID, status, summary, messageID) helper that updates status+summary+message_id atomically and populates completed_at for terminal states. - internal/goals/service.go: Complete(ctx, goalID, status, summary, messageID) wraps the store method with legalTransition gating. legalTransition expanded so draft can jump straight to completed (no mandatory "active" hop required). - internal/mcp/goals_tools.go: completeGoalTool definition + handleCompleteGoal handler. Tool count 6 → 7. - internal/api/goals_handler.go: surfaces completion_summary, completion_message_id, and completed_at on both list and detail endpoints so the Svelte /goals UI can render them. ### 2. Draft → active auto-transition in propose_task_tree (#60) handleProposeTaskTree now flips the goal from draft to active at the end. Previously the coordinator would call create_goal + propose_task_tree and dispatch inspector, but the goal stayed in draft forever because nothing transitioned it. Now the mere fact of having a task tree means the goal is active. Safe: the transition is best-effort and ignores the legal-transition error when the goal is already beyond draft. ### 3. REVISE round cap (#61) Two-layer enforcement: - Server-side: examples/doc-gardener/start.sh drops max_trigger_depth from 8 to 4. Each REVISE round costs 2 hops (critic→inspector + inspector→critic), so depth=4 caps the loop at roughly 2 rounds before the reactor refuses further dispatches. - Prompt-side: inspector now includes revision_round (starting at 0, incremented when it sees a REVISE: input) in its findings JSON. Critic reads revision_round and force-FINALs when >= 1. Prompt explicitly tells the critic to call complete_goal after sending FINAL, so the goal row gets a proper completion_summary. ### 4. run_task.sh terminal-state detection Rewrote the poll loop to watch goals.status/completion_summary as the definitive "done" signal rather than parsing DM bodies. Keeps a message-based fallback for TRIVIAL/CANNOT paths that don't create a goal. Treats "Received system trigger..." and "Coalesced trigger..." as informational (they're `__coalesced__` reactor synthetic events leaking through the coordinator reply, not real user-facing output). Bare coordinator replies are terminal only when no goal was created. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b5b5d6d26c |
fix(reactor): don't truncate body for subprocess + docker dispatch
dispatchHarness was capping event.Body at 4096 bytes before handing it to the subprocess/docker harness. Those backends write the body to a message.json file in the per-run workdir (bind-mounted into the container) and have no shell/env-var size limits, so silent truncation was hostile. The doc-gardener inspector routinely produces 10-20 KiB findings JSON (drift report with per-flag evidence). Truncation cut off the trailing artifact.findings entries + artifact.recommendation, making the report look incomplete to the critic — which then spuriously REVISE'd, blowing the 600s deadline. The K8s job path still truncates in createJob() because Kubernetes imposes a 1 MiB env-var cap and most shells misbehave past a few KiB. That's a separate code path, untouched. Also: critic prompt rewrite (examples/doc-gardener/configs/critic.json). The old critic spec told the critic to "spot-check evidence by re-running the inspector's commands". That's structurally wrong: the critic runs in a fresh container with no install state, so re-running mcpproxy --help always fails and produces a false REVISE. New prompt says: audit by structural consistency only, never run shell commands to re-verify, default to FINAL, never REVISE more than once, and FINAL the failure summary back to the owner when the inspector reports status: failed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
ddbb1bd329 |
fix(docker): tmpfs /tmp mounted rw,exec,size=128m
Docker Desktop's default noexec on tmpfs broke the "download a CLI to /tmp, chmod +x, run it" workflow — exactly what the doc-gardener inspector needs to verify docs.mcpproxy.app against the real mcpproxy binary. Previously the agent spent ~10 minutes in a self- debug loop discovering the noexec, falling back to /home/agent, running into externally-managed Python, missing python3-venv, etc. With /tmp exec, the inspector's own install pipeline works on the first try: curl | tar | chmod | run. First real run produced a 72-claim drift report (21 matched / 1 drifted / 50 missing) against mcpproxy v0.24.4 in ~8 minutes, no REVISE loop. The 64m → 128m bump gives breathing room for curl'd tarballs that need a temp extraction directory alongside the final binary. Inspector prompt updated to tell the agent about the /tmp install path explicitly and forbid the previous /home/agent detours. Also updated the coordinator brief template to match. Note the image itself is UNCHANGED — we deliberately do NOT bake mcpproxy (or any other domain-specific tool) into synapbus-agent. The image stays a blank Linux shell with Node + Python + core tools, and each example's prompt teaches its agent how to install whatever it needs. This keeps the gardener universal: swap in any other docs domain and the inspector figures out what to install on demand. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
bdec096190 |
feat(doc-gardener): default coordinator to gemini-3.1-pro-preview
Align with the goal-coordinator example — both now use Gemini 3 Pro as the default for triage/coordination. Workers stay on 2.5 Flash. Override via SYNAPBUS_COORDINATOR_MODEL when the preview model is rate-limited. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
f1e2b1fa38 |
feat(doc-gardener): MCP-native + docker-isolated multi-agent demo
Replace the legacy cmd/docgardener orchestration (~2400 LOC of Go
spawning subprocess workers via local_command + admin socket) with
three Docker-isolated agents that all reach SynapBus through MCP:
doc-coordinator — Gemini Pro, triages goal, calls create_goal +
propose_task_tree + send_message via MCP
docs-inspector — Gemini Flash, fetches docs, installs mcpproxy,
shells out to verify, reports findings via MCP
docs-critic — Gemini Flash, independent reviewer with its
own MCP API key + config_hash, audits the
inspector's evidence and DMs the owner
Every agent runs inside synapbus-agent:latest with --cap-drop=ALL,
--security-opt=no-new-privileges, --read-only root + tmpfs /tmp,
--pids-limit, memory + CPU quotas. The container reaches the
SynapBus MCP server on the host at host.docker.internal:18089
because the docker harness rewrites .gemini/settings.json URLs
from 127.0.0.1 automatically.
Wrapper baked into the image at /usr/local/bin/synapbus-agent-wrapper.sh
so configs don't need to mount or template a per-example wrapper.
The harness's default no longer overrides docker CMD — the image's
baked entry script is used unless docker.command is set explicitly.
start.sh changes:
- Preflight: docker daemon, GEMINI_API_KEY (or ~/.gemini/oauth_creds.json)
- Builds synapbus-agent image lazily on first run
- Mints one MCP API key per agent via `agent revoke-key`
- Templates each config with __PORT__, __*_APIKEY__, __MODEL__,
__GEMINI_API_KEY__, __EXTRA_MOUNTS__
- With OAuth fallback: copies host ~/.gemini → data/agent-home/.gemini
once and bind-mounts the whole agent-home rw at /home/agent so
in-container gemini has a writable HOME without polluting the host
- SYNAPBUS_KEEP_WORKDIR=1 preserves per-run docker workdirs for
debugging
- Sets harness_name=docker explicitly so the resolver picks the
right backend even with empty local_command
stop.sh: best-effort cleanup of lingering synapbus-* containers so a
killed parent doesn't leave bind-mount holders that block the next
start.sh from re-mounting the same paths.
run_task.sh: snapshot-baseline pattern (only watches replies newer
than the max msg id at send time), 600s deadline, treats any reply
from doc-coordinator that isn't DELEGATED:/REVISING: as terminal,
plus FINAL:/CANNOT: from any sender.
cmd/docgardener slimmed from 7 files / 2580 LOC to 3 files / ~370 LOC.
The remaining binary only renders the HTML report (queries goals +
goal_tasks + traces + harness_runs from the SynapBus DB read-only).
agent.go, channels.go, flow.go, gemini_tree.go all deleted.
Verified end-to-end against gemini-2.5-pro coordinator + gemini-2.5-flash
workers (with OAuth fallback mount):
./run_task.sh "what does this demo do?"
→ coordinator TRIVIAL: replies directly via MCP send_message
./run_task.sh "Verify the CLI commands on docs.mcpproxy.app/cli/command-reference"
→ coordinator calls create_goal (slug verify-mcpproxy-cli-...),
propose_task_tree (3-node tree: coordinator/plan,
doc-gardener/scan, doc-gardener/audit) and send_message to
docs-inspector
→ inspector container runs ~10 minutes inside the sandbox:
installs mcpproxy from real release URL (linux-arm64), curls
the docs page, falls back from BeautifulSoup → grep when
python3-venv is missing, debugs its own f-string syntax, writes
extract_flags.py, runs `mcpproxy --help` for ground truth
→ real multi-agent iteration loop: critic REVISE: → inspector
retry → critic REVISE: with new feedback
The agents discovered real environment quirks (tmpfs noexec on /tmp,
externally-managed Python, missing python3-venv) and worked around
them inside the sandbox without touching the host.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
f319290ef9 |
feat(goal-coordinator): native MCP tool surface via Gemini session
The coordinator now reaches SynapBus's MCP endpoint directly from inside the Gemini session. wrapper.sh's coordinator branch is a pure pass-through — no more JSON-plan parsing. When the coordinator runs, Gemini connects to /mcp with the coordinator's own Bearer API key and calls `create_goal`, `propose_task_tree`, and `send_message` as native tools. Goal rows, task trees, and DMs all land in the DB in one in-session flow. - start.sh mints a fresh API key for goal-coordinator via `agent revoke-key` and substitutes it into configs/coordinator.json (plus the port) at apply_config time. - coordinator.json declares the synapbus MCP server in mcp_servers; the subprocess harness already writes .gemini/settings.json from that array, so gemini picks it up automatically. - GEMINI.md rewritten to instruct the model to call MCP tools instead of emitting a JSON action blob. Stdout is explicitly discarded; every reply goes through send_message. - wrapper.sh coordinator branch is ~15 lines: invoke gemini, log, exit. Inspector + critic keep the legacy JSON-plan pattern since they're workers with fixed contracts. - SYNAPBUS_KEEP_WORKDIR=1 preserves per-run workdirs for debugging MCP traces, gemini output, and materialized configs. - Reactor checkPendingWork now fires after subprocess run completion (previously only K8s poller hit this path). The synthetic coalesced trigger uses a `__coalesced__` sentinel instead of `system` so it bypasses the FromAgent=="system" dispatch guard. Verified e2e (with rate-limit-induced retries): - TRIVIAL: "what is 2+2?" → coordinator send_message(algis, "4") - INFEASIBLE: "Transfer \$50…" → coordinator send_message(algis, "CANNOT: …") - SINGLE-STEP: 3-node task tree materialized in goal_tasks, TASK JSON forwarded to generic-inspector → critic-auditor chain. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
40706a22ce |
feat(example): universal goal-coordinator with triage + delegation
New examples/goal-coordinator/ demonstrates an LLM-driven coordinator that classifies any natural-language goal into one of four paths: - TRIVIAL → coordinator answers directly (no delegation) - INFEASIBLE → coordinator refuses with a concrete reason - SINGLE-STEP → delegate to one inspector + one critic (the default) - MULTI-STEP → multi-phase plan (rare) Architecture: - goal-coordinator (Gemini 3.1 Pro) triages and delegates - generic-inspector (Gemini 2.5 Flash) does scan+verify+report in one pass (shared context, no artificial splitting) - critic-auditor (Gemini 2.5 Flash) reviews the artifact with its own config_hash → independent reputation, no shared reasoning trace → can't rationalize the worker's mistakes Harness-agnostic via the existing subprocess harness + a wrapper.sh that calls `gemini`. Swapping to claude / codex is a 3-line change in the call block — nothing in SynapBus itself is tied to a CLI. Verified e2e on gemini-3.1-pro-preview: - "what is 2+2?" → TRIVIAL, direct "4" reply - "check Go version >= 1.23" → SINGLE-STEP, 3 runs, FINAL: "The installed Go version (1.25.1) meets the specified requirement" - "transfer $50 from my bank account" → INFEASIBLE, CANNOT: refusal citing missing credentials 7 runs visible in /runs with captured prompt/response per run. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
190df8119f |
fix(example): rebuild web dist in start.sh when missing
The binary embeds internal/web/dist via go:embed, but .gitignore only tracks index.html. A fresh clone has an empty dist, so the binary serves only the shell HTML + no _app JS — the channel page renders its skeleton placeholder forever because the SPA never loads. start.sh now detects an empty dist, runs `npm run build`, and copies web/build into internal/web/dist before `go build`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
3b94fab226 |
feat(018): real reactor-driven multi-agent doc-gardener flow
Until now the doc-gardener example was a single monolithic
orchestrator binary writing synthetic messages directly to SQLite.
That's now obsolete: the feature runs as a true multi-agent flow
where the SynapBus reactor fires subprocess runs for every DM, each
agent is its own reactive subprocess invocation, and follow-up DMs
go through the real MessagingService.Send → dispatcher path so the
reactor picks them up.
Changes:
- cmd/synapbus/main.go: gate the three legacy background workers
(expiry, retention, stalemate) behind SYNAPBUS_DISABLE_*_WORKER env
flags. These workers manage the legacy channel task-auction /
message retention features the doc-gardener demo doesn't use, but
they held the single-connection write pool long enough to wedge
the whole server for interactive sessions. All three are disabled
in the example's start.sh.
- cmd/docgardener/agent.go (new): the per-agent subprocess entry the
reactor harness invokes for every reactive trigger. Reads
message.json from the workdir, routes by SYNAPBUS_AGENT to either
coordinator-kickoff, coordinator-completion, or specialist-work
logic. Writes prompt.txt + response.txt for harness capture. Uses
the admin socket (`synapbus messages send`) for follow-up DMs so
the real MessagingService.Send path fires the dispatcher.
- cmd/docgardener/main.go: adds `docgardener agent` subcommand, plus
helpers freshAPIKey / bcryptHash / absPath / selfPath used by the
spawn flow.
- examples/doc-gardener/start.sh: provisions user + coordinator
agent + algis human agent + approvals/requests channels; the
coordinator is created with trigger_mode=reactive,
harness_name=subprocess, local_command pointing to docgardener
agent, and harness_config_json.env carrying SYNAPBUS_AGENT,
SYNAPBUS_BIN, SYNAPBUS_SOCKET. Specialists are spawned
dynamically by the coordinator at runtime (not pre-registered),
so the demo exercises dynamic agent spawning end-to-end.
- examples/doc-gardener/run_task.sh: collapsed to a 3-line kickoff
that just DMs the coordinator and polls algis's inbox for the
coordinator's FINAL: reply. Everything else happens via the
reactor.
Verified end-to-end in Chrome on a fresh instance:
- 4 agents registered (coordinator + 3 specialists dynamically
spawned by the coordinator on receipt of the first DM)
- 7 reactive_runs + 6 harness_runs across the goal lifecycle:
algis → coordinator (kickoff, 624ms, builds goal+tree+spawns)
coordinator → docs-scanner (claim task 2)
coordinator → cli-verifier (claim task 3)
coordinator → drift-reporter (claim task 4)
docs-scanner → coordinator (DONE task=2)
cli-verifier → coordinator (DONE task=3)
drift-reporter → coordinator (DONE task=4, coalesced)
- Web UI Agent Runs page shows all 7 runs with the real
"DM from X" trigger lines and correct sender/receiver chain
- Goal ends at status=completed with all 3 leaf tasks at status=done
- Each specialist run posts a real subprocess artifact to the
goal channel (#finding, #verified, #summary) and appends a real
reputation_evidence row keyed by config_hash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
ff5d0c49f4 |
feat(018): dynamic agent spawning — primitives + doc-gardener demo
Ships the MVP slice of spec 018 (dynamic agent spawning):
- 5 new SQLite migrations (021-025): goals + goal_tasks + agent_proposals
+ reputation_evidence + secrets + harness_runs.task_id. The legacy
`tasks` table (channel auctions) and `agent_trust` table (reactions
workflow) are left untouched — the new schema coexists.
- 4 new internal packages, fully tested:
- internal/goals: Goal struct + store + service, slug collision dedup,
backing-channel auto-create via ChannelCreator adapter
- internal/goaltasks: goal_tasks table with denormalized 16 KB
ancestry snapshots, single-statement optimistic-lock atomic claim,
recursive-CTE cost rollup, state machine, per-billing-code rollup
- internal/secrets: NaCl-secretbox encrypted blobs, user/agent/task
scope precedence, sanitized env injection, master-key bootstrap
- internal/trust additions: ConfigHash (deterministic SHA-256 of
model + prompt + tools + skills + mcp + subagents, sorted),
DelegationCap (tier + tool-scope + budget + depth enforcement),
append-only Ledger with exponential time-decay rolling score and
70%-of-parent child seeding. Existing trust package unchanged.
- Critical invariants under test:
- 50-goroutine concurrent claim race → exactly one winner per round
- ConfigHash stable under shuffled array inputs, sensitive to
capability changes
- DelegationCap full tier × tool-scope matrix
- Ledger time-decay + parent seed at 70 % ± 1 %
- Secret name sanitization, scope precedence, plaintext never
returned via MCP-equivalent paths
- internal/agents/types.go extended with dynamic-spawning columns
(config_hash, parent_agent_id, spawn_depth, system_prompt,
autonomy_tier, tool_scope_json, quarantined_at). Existing tests
still pass.
- cmd/docgardener: self-contained demo binary driving the end-to-end
flow. `docgardener run` creates a goal, builds a task tree with
denormalized ancestry, spawns 3 specialists (each going through
real delegation-cap validation and config-hash computation and
70 %-of-parent reputation seeding), claims tasks atomically, runs
them through the state machine, records reputation evidence.
`docgardener report` queries all of that back out and renders a
rich dark-mode HTML report (header, spend metrics, task tree,
spawned-agent cards with reputation bars, cost breakdown, artifacts,
timeline).
- examples/doc-gardener: start.sh / run_task.sh / report.sh / stop.sh
mirroring the cold-topic-explainer pattern. Launches an isolated
synapbus instance on port 18089, drives the demo, renders
report.html, cleans up. Full README documenting what's real vs
deferred, plus examples/README.md listing both examples.
- specs/018: tasks.md updated with MVP completion status; legacy tasks
naming collision noted.
Deferred (marked explicitly in example README):
- Real LLM-driven coordinator (needs MCP tool wiring + prompt
iteration)
- Real subprocess runs (needs reactor integration with task_id on
ExecRequest)
- Full MCP tool surface (contracts are written at
specs/018-dynamic-agent-spawning/contracts/mcp-tools.md)
- Svelte /goals UI (REST endpoints remain a follow-up)
- Full budget race + quarantine auto-trigger wiring
- Full resource-request → secrets fulfill reaction-workflow path
Cross-compiles clean for linux/amd64 and darwin/arm64 with no CGO
(SC-010). All new package tests pass (SC-004, SC-005, SC-007).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
fee73e33a0 |
feat(ux): run detail page + reaction pills + captured prompt/response
Makes the Web UI reflect what agents are actually doing: reactions
on DMs that trigger a subprocess run, a per-run detail page that
shows the exact prompt the model received and the raw response, and
cross-linked reactive_runs ↔ harness_runs data for a single composite
API call.
Migration 020 (internal/storage/schema/020_harness_run_detail.sql):
ALTER TABLE harness_runs ADD COLUMN reactive_run_id INTEGER;
ALTER TABLE harness_runs ADD COLUMN prompt TEXT;
ALTER TABLE harness_runs ADD COLUMN response TEXT;
CREATE INDEX idx_harness_runs_reactive ON harness_runs(reactive_run_id);
internal/harness:
* ExecRequest.ReactiveRunID — reactor pins the reactive_runs row id
so the observer can JOIN the two tables.
* ExecResult.Prompt / Response — the subprocess harness reads
prompt.txt / response.txt that wrappers write into the workdir,
and runs.Store persists them (capped at 32 KiB each).
* runs.Run struct now has JSON tags — previously the API returned
PascalCase field names that didn't match the Web UI's snake_case
TypeScript types.
* New runs.Store.GetByReactiveRunID for the composite API endpoint.
* Test schema updated to include the new columns.
internal/reactor:
* New ReactionNotifier interface + SetReactionNotifier.
* dispatchHarness now reacts `in_progress` on the triggering DM
before spawning the goroutine.
* runHarness reacts `done` on success, `reject` on failure. The
existing reactionPriority ordering means the terminal reaction
wins for badge display — no need to remove in_progress first.
* dispatchHarness sets ExecRequest.ReactiveRunID.
cmd/synapbus/main.go:
* reactorReactionAdapter: adapts reactions.Service.Toggle to the
reactor's one-shot AddReaction signature.
* HarnessRunsStore wired into the API router config.
internal/api/runs_handler.go — GetRun composite endpoint:
The GET /api/runs/{id} response now returns everything the Web UI
needs to render the run detail page in one call:
{
"run": <reactive_runs row>,
"harness_run": <linked harness_runs row with prompt/response>,
"agent": <current agent snapshot with harness_config_json>,
"trigger_message": <DM that started the run>,
"outgoing_message": <first DM the agent produced after startedAt>
}
The outgoing-message lookup wraps both sides of the created_at
comparison in datetime() so SQLite parses the stored 'YYYY-MM-DD
HH:MM:SS' and the Go-emitted RFC3339 into the same canonical form
before comparing — a raw string compare was silently returning no
rows.
internal/api/router.go: HarnessRunsStore field in RouterConfig, wired
through to NewRunsHandler.
examples/cold-topic-explainer/wrapper.sh:
Writes prompt.txt and response.txt alongside gemini.stdout.raw so
the subprocess harness can capture "what the model saw" and "what
the model said" post-hoc.
web/src/lib/components/MessageList.svelte:
New ReactionPills render below each message body when the message
carries a `reactions` array (already populated by
EnrichMessages/ReactionEnricher on the server side). Makes the
👀 in_progress / ✔ done / ❌ reject lifecycle visible in every DM
view and conversation.
web/src/routes/runs/[id]/+page.svelte (NEW):
New run detail page at /runs/:id with sections:
1. Header strip — agent, status pill, backend badge, trigger
info, duration, tokens in/out, cost, exit code, trace id.
2. Triggering message — body + sender.
3. What the model saw — GEMINI.md / CLAUDE.md from agent snapshot
+ the captured rendered prompt (byte count on each summary
bar, collapsible details).
4. What the model said — captured response, falling back to
logs_excerpt or error_log when unavailable.
5. Outgoing message — body + recipient + status.
6. Metadata — reactive_run.id, harness_run.run_id, backend,
session_id, tokens_cached, k8s_job, agent trigger config.
Styled against the existing dark tailwind system — no design
overhaul, fits the current aesthetic (editorial sectioning,
monospace for code-like content, accent-blue for links,
accent-purple for system-instructions, accent-green for model
output, accent-red for errors).
web/src/routes/runs/+page.svelte: the inline expand panel now has
a "View full details →" link next to the Retry button.
E2E VERIFIED on a live subprocess run:
* Topic: "why does the subprocess harness materialise GEMINI.md
alongside .gemini/settings.json in the per-run workdir?"
* 3 subprocess runs + 3 reactive_runs + 3 harness_runs, all linked.
* message_reactions: 6 rows — in_progress + done for each hop.
* GET /api/runs/1 returns a composite with
harness_run.prompt=883 bytes, harness_run.response=550 bytes,
reactive_run_id=1, trigger_message populated, outgoing_message
populated (decomposer-pro → writer-flash), agent.gemini_md=747
bytes. All keys are snake_case as the Svelte types expect.
Full go test ./... green. `vite build` green. Demo instance still
running on port 18088 for browser verification.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
b140879bc3 |
fix(examples): own demo agents by the algis user, not admin
start.sh was passing --owner 1 to every `agent create` call, under the
assumption that the freshly created `algis` user would be user id 1.
It isn't — the `admin` user is auto-seeded at id=1, so `algis` comes
in at id=2. Result: the 3 AI agents AND the `algis` human agent were
owned by `admin`, and when the user logged into the Web UI as `algis`
the message handler called GetHumanAgentForUser(2) which returned a
different auto-created `algis-human` agent (id=5, owned by user 2).
The critic DM'd the name `algis` → landed on agent id=1 (admin-owned),
but the UI listed DMs for `algis-human` → panel showed
"No conversations" despite reactive_runs clearly showing the chain
succeeded.
Fix: after `user create`, query sqlite for the algis user id and use
that value as --owner for every subsequent agent create. Bails with a
clear error if the id lookup fails or returns 1 (sanity check that
admin/algis aren't conflated).
Verified end-to-end:
1. ./stop.sh && ./start.sh — new instance, ownership correct from
the first `agent create` call.
2. ./run_task.sh with a fresh topic — 3 subprocess runs succeeded,
messages #1 (algis → decomposer-pro) and #4 (critic-lite → algis)
now belong to an agent owned by user 2, so
GetHumanAgentForUser(2) returns the same agent the messages are
addressed to.
3. sqlite3 agents table shows all four agents with owner_id=2.
4. Chrome navigation to http://localhost:18088/ reaches the login
form with no errors — once the user logs in as
algis / algis-demo-pw the Direct Messages panel will show the
four-message conversation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
95491402a9 |
fix(otel,examples): schema-URL merge + stale embedded web dist
Two independent fixes hit while running the cold-topic-explainer demo
end-to-end.
1. observability/otel.go — schema URL conflict on Init
When SYNAPBUS_OTEL_ENABLED=1, Init() failed with:
observability: build resource: conflicting Schema URL:
https://opentelemetry.io/schemas/1.26.0 and
https://opentelemetry.io/schemas/1.21.0
resource.Default() ships with schema 1.26.0 (newer otel/sdk) but I
was passing semconv.SchemaURL from v1.21 into a NewWithAttributes
call. resource.Merge rejects that.
Fix: use resource.NewSchemaless for the service.* attributes so our
side of the merge has no schema URL and slots cleanly into whatever
Default provides. ServiceVersion is now only attached when non-empty
(avoids a stray service.version="" attribute).
Two new regression tests:
TestInit_EnabledSucceeds — Enabled=true with all fields set
TestInit_EnabledWithNoVersion — Enabled=true with empty version
Both point at an unroutable endpoint so the batcher never actually
exports; the bug reproduced during Init(), which is all we need.
2. examples/cold-topic-explainer/start.sh — rebuild embedded SPA
The Svelte Web UI loaded blank because internal/web/dist/ had a
mismatched index.html + stale _app/immutable/entry/ assets (a build
had updated index.html but not the chunks, so every asset URL fell
through to the SPA HTML fallback and the browser tried to execute
HTML as JavaScript).
The canonical path is `make web`, but start.sh never ran it, so a
working demo depended on the developer having run `make web` first.
Fix: start.sh now rebuilds the SPA when web/src is newer than the
embedded dist/index.html, using the already-installed
web/node_modules (no reinstall). Falls back with a "run make web
once" hint when node_modules isn't present. This keeps the fast
path fast (~2s vite build after cache warm) and eliminates the
silent-stale-dist trap.
E2E verified after both fixes:
* SYNAPBUS_OTEL_ENABLED=1 start.sh no longer crashes.
* `curl /_app/immutable/entry/start.*.js` returns real JavaScript
(Content-Type: text/javascript) instead of the index.html
fallback.
* Chrome-in-MCP navigation to http://localhost:18088/ renders the
login form with no SynapBus-originated console errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
04e4e3ca37 |
feat(harness): GEMINI.md support + cold-topic-explainer example
Adds everything needed to run a real multi-Gemini-model reactive agent
loop end-to-end on SynapBus.
internal/harness/subprocess/config.go:
* AgentConfig.GeminiMD — content of workdir/GEMINI.md
* MaterialiseAgentConfig writes GEMINI.md AND workdir/.gemini/settings.json
when gemini_md is set. The settings file carries the same mcp_servers
list as .mcp.json (so a Gemini child running from the workdir gets
the exact MCP surface the operator configured, not the user's
~/.gemini/settings.json).
* 2 new config_test cases: GEMINI.md + .gemini/settings.json round
trip, GEMINI.md with empty mcp_servers still writes the settings
file (explicitly clearing any inherited home config).
internal/harness/registry.go — BUG FIX:
Resolve() now honours agent.HarnessName (explicit selection) BEFORE
the inference chain, matching the reactor's own agentBackendKind
policy. Previously, when multiple backends were registered,
Resolve would pick "webhook" for every non-K8s agent — even when the
agent's HarnessName was "subprocess" — because the original fallback
chain put webhook first. This is why the first cold-topic-explainer
run failed with "webhook: agent has no webhook config". Discovered
during e2e testing.
internal/admin/socket.go + cmd/synapbus/admin.go:
New `messages.send` admin command (socket + CLI). Sends a DM as any
agent through the messaging service, bypassing the REST/MCP auth
layers. Local-only via the admin Unix socket, so the threat model is
"whoever can reach the socket is already admin".
CLI:
synapbus messages send --from X --to Y --body "..." [--priority N]
synapbus messages send --from X --to Y --body-file path
echo "..." | synapbus messages send --from X --to Y
Used by the harness shell wrappers (so Gemini subprocess agents can
DM each other) and by run_task.sh (to kick off a chain as a human
user without implementing the REST session flow).
examples/cold-topic-explainer/ (NEW):
Runnable 3-agent Gemini demo that exercises the subprocess harness,
reactive triggers, recursive update, and all the preconditions (depth,
budget, cooldown) end-to-end on a separate isolated synapbus instance.
Layout:
README.md — usage + troubleshooting + cost notes
start.sh — builds synapbus, launches on port 18088 with
./data, creates user + agents + harness configs,
marks agents reactive via sqlite3
run_task.sh — sends initial DM algis → decomposer-pro, polls
reactive_runs + messages for the FINAL: reply,
prints the result or dumps reactive_runs on
timeout for debugging
stop.sh — SIGTERM + 5s grace + SIGKILL fallback
wrapper.sh — shared subprocess local_command: reads
message.json + GEMINI.md, calls gemini headless
with --approval-mode yolo, strips the
"MCP issues detected" noise prefix, routes the
cleaned response to the next agent via
`synapbus messages send` over the admin socket
configs/
decomposer-pro.json — gemini-3.1-pro-preview
(gemini-2.5-pro is currently capacity-
exhausted on Google's side)
writer-flash.json — gemini-2.5-flash
critic-lite.json — gemini-2.5-flash-lite
.gitignore — data/, bin/, synapbus.log, .synapbus.pid
The wrapper does NOT rely on gemini's MCP tool-calling (which was
unreliable in testing). Gemini is used as a pure text generator; the
shell decides routing based on AGENT_ROLE:
- decomposer → NEXT_AGENT (writer)
- writer → NEXT_AGENT (critic)
- critic → OWNER_AGENT if response starts with FINAL:,
REVISE_AGENT otherwise
E2E VERIFICATION (real run, real Gemini, not a mock):
Topic: "how does SynapBus unify message delivery, reactive agent
triggers, and harness runs on a single SQLite database?"
Result (from data/synapbus.db after one successful run):
harness_runs:
#1 decomposer-pro subprocess success 106s
#2 writer-flash subprocess success 155s
#3 critic-lite subprocess success 10s
reactive_runs: 3 rows, all succeeded, trigger_from chain:
algis → decomposer-pro → writer-flash → critic-lite
messages:
#1 algis → decomposer-pro (topic)
#2 decomposer-pro → writer-flash (Q1/Q2/Q3 breakdown)
#3 writer-flash → critic-lite (3-paragraph draft)
#4 critic-lite → algis (FINAL: + polished 3-paragraph explainer)
Critic converged in one pass (all scores ≥ 8), so the writer↔critic
refinement loop didn't need to recurse — but the plumbing for it
(REVISE: branch in wrapper.sh, depth limit in reactor) is wired and
ready. Flipping the critic's acceptance bar exercises the recursion.
Full `go test ./...` remained green through all changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|