Skip to content

fix: nested subagent hierarchy + graceful shutdown + no card-click toggle#200

Merged
hoangsonww merged 21 commits into
masterfrom
fix/nested-subagent-hierarchy
Jul 2, 2026
Merged

fix: nested subagent hierarchy + graceful shutdown + no card-click toggle#200
hoangsonww merged 21 commits into
masterfrom
fix/nested-subagent-hierarchy

Conversation

@hoangsonww

Copy link
Copy Markdown
Owner

Summary

Three fixes, plus full docs.

1. Nested subagents rendered flat (main bug)

A subagent that spawns its own subagents rendered at one level under the main agent instead of nesting under its true spawner. Root cause: subagent rows are inserted with parent_agent_id = main because a single hook event or subagent JSONL file carries no spawner identity. The client tree builder was already correct — the data was wrong.

Fix: each subagent transcript records every child it spawned via the Task tool as toolUseResult.agentId. parseSubagentFile now surfaces these as spawnedChildren; the new reconcileSubagentParents() inverts them to a child→parent map and repoints parent_agent_id (new setAgentParent stmt) so subagents nest under their real spawner. Any subagent no other subagent claims stays under main. Idempotent and additive (only rewrites parent_agent_id); runs in all three group-import paths (importSession ×2, live scanAndImportSubagents). It resolves live-or-jsonl DB ids the same way importSubagentFromJsonl does, so it also corrects the live PreToolUse-Agent parent heuristic once transcripts land. scanAndImportSubagents now returns reparented; the SubagentStop broadcast nudges a refetch when created or reparented is non-zero.

2. Graceful shutdown crash/hang

Two bugs in server/index.js shutdown (surfaced as dev crash/restart churn under node --watch):

  • DB was closed immediately, in parallel with httpServer.close(), so in-flight requests threw The database connection is not open (routes/agents.js). DB now closes inside the close() callback, after the HTTP server drains.
  • Open WebSocket + keep-alive sockets kept httpServer.close() from completing, stalling shutdown until the 5 s force-exit (the "waiting for graceful termination" hang). New closeWebSocket() terminates WS clients; httpServer.closeAllConnections() drops lingering keep-alive sockets.

3. Clicking a parent agent card no longer toggles its subagent list

Expand/collapse is handled by the chevron button, so clicking the card (main/root agent or any parent subagent) now behaves like a leaf card — SessionDetail opens the agent conversation, Dashboard navigates to session details. The chevron remains the sole toggle; the "N subagents" badge still expands.

Verification

  • npm run test:server — 508/508 (incl. 2 new nested-hierarchy regression tests)
  • npm run test:client — 240/240 (snapshots unchanged)
  • npm run mcp:typecheck — clean
  • npm run build — ok
  • End-to-end on a real 24-subagent session: reconstructs the exact 3-level tree, idempotent on re-scan. Shutdown exits in ~0.5 s under live keep-alive load with 0 db-not-open errors.

Docs

README (+VN/CN), ARCHITECTURE, docs/HOOKS, docs/DATABASE, server/README, wiki (+ zh/vi i18n + cache bump).

🤖 Generated with Claude Code

hoangsonww and others added 2 commits July 1, 2026 07:32
Nested subagents (a subagent that spawns its own subagents) rendered flat
at one level under the main agent instead of nesting under their true
spawner. Root cause: subagent rows are inserted with parent_agent_id =
main because a single hook event or subagent JSONL file carries no spawner
identity. The client tree builder was already correct — the data was wrong.

Fix: each subagent transcript records every child it spawned via the Task
tool as `toolUseResult.agentId`. parseSubagentFile now surfaces these as
`spawnedChildren`; the new reconcileSubagentParents() inverts them to a
child→parent map and repoints parent_agent_id (new setAgentParent stmt) so
subagents nest under their real spawner. Any subagent no other subagent
claims stays under main. Idempotent and additive (only rewrites
parent_agent_id, never inserts/deletes); runs in all three group-import
paths (importSession x2, live scanAndImportSubagents). It resolves live-or-
jsonl DB ids the same way importSubagentFromJsonl does, so it also corrects
the live PreToolUse-Agent parent heuristic once transcripts land.
scanAndImportSubagents now returns `reparented`; the SubagentStop broadcast
nudges a refetch when created OR reparented is non-zero.

Also fixes two graceful-shutdown bugs in server/index.js (surfaced as dev
crash/restart churn under `node --watch`):
- DB was closed immediately, in parallel with httpServer.close(), so
  in-flight requests threw "The database connection is not open"
  (routes/agents.js). DB now closes inside the close() callback, after the
  HTTP server has drained.
- Open WebSocket + keep-alive sockets kept httpServer.close() from
  completing, stalling shutdown until the 5s force-exit ("waiting for
  graceful termination" hang). New closeWebSocket() terminates WS clients;
  httpServer.closeAllConnections() drops lingering keep-alive sockets.

Verified: server 508/508, client 240/240, mcp typecheck, build all pass.
Reconstructs the real 3-level tree on a 24-subagent session; shutdown
exits in ~0.5s under live keep-alive load with 0 db-not-open errors.

Docs updated: README (+VN/CN), ARCHITECTURE, docs/HOOKS, docs/DATABASE,
server/README, wiki (+i18n +cache bump).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking a parent agent card (the main/root agent, or any subagent with
children) toggled its subagent list expand/collapse. Expand/collapse is
already handled by the dedicated chevron button, so the card should behave
like a leaf card instead.

- SessionDetail: parent card click now always opens the agent conversation
  (same as leaf subagent cards), instead of toggling.
- Dashboard: parent card click now falls through to AgentCard's default
  navigation (session details), instead of toggling.

The chevron button remains the sole expand/collapse control on both
surfaces; the "N subagents" count badge still expands as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 1, 2026 00:35
@github-actions github-actions Bot added bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed question Further information is requested labels Jul 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes nested subagent tree reconstruction by repairing parent_agent_id based on transcript-derived spawn relationships, improves server shutdown behavior (WS teardown + DB close ordering), and adjusts UI interactions so parent agent cards no longer toggle expansion on click. It also updates documentation/wiki content to reflect these behavior changes.

Changes:

  • Rebuild nested subagent hierarchy by parsing toolUseResult.agentId spawn links and reconciling parent_agent_id across import + live JSONL scans.
  • Improve shutdown sequence by terminating WebSocket clients and deferring SQLite close until after HTTP server drain.
  • Update client click behavior so only the chevron controls expand/collapse; card click consistently navigates/opens details.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
wiki/sw.js Bumps wiki service-worker cache version.
wiki/index.html Documents nested-subagent hierarchy rebuild + bumps i18n cache param.
wiki/i18n-content.js Updates i18n strings to match new hierarchy wording (zh/vi).
server/websocket.js Adds closeWebSocket() to terminate WS clients during shutdown.
server/routes/hooks.js Triggers UI refetch on subagent scan when created or reparented is non-zero; adjusts broadcast summary.
server/README.md Documents nested hierarchy rebuild + graceful shutdown ordering.
server/index.js Implements shutdown ordering (WS teardown, HTTP close, keep-alive handling, DB close).
server/db.js Adds setAgentParent prepared statement for re-parenting agents.
server/tests/subagent-attribution.test.js Adds regression tests for nested hierarchy + idempotent rescans.
scripts/import-history.js Parses spawnedChildren and reconciles real parent/child relationships during import and live scans.
README.md Updates feature docs to describe authoritative nested hierarchy reconstruction.
README-VN.md VN doc update reflecting hierarchy reconstruction behavior.
README-CN.md CN doc update reflecting hierarchy reconstruction behavior.
docs/HOOKS.md Documents SubagentStop scan now also rebuilds nested hierarchy and returns reparented.
docs/DATABASE.md Clarifies parent_agent_id semantics + re-parenting behavior post-insert.
client/src/pages/SessionDetail.tsx Makes agent card click always open conversation; no longer toggles children.
client/src/pages/Dashboard.tsx Removes parent-card click-to-toggle; leaves navigation behavior unchanged.
ARCHITECTURE.md Updates architecture notes for shutdown sequencing and subagent reparenting behavior.
Files not reviewed (1)
  • wiki/i18n-content.js: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/index.js Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a mechanism to reconstruct the nested-subagent hierarchy by parsing subagent transcripts for Task tool results and repointing parent_agent_id to the true spawner. It also implements a structured graceful shutdown sequence for the server to ensure clean resource teardown. The review feedback highlights several important improvements: preventing circular hierarchy loops in the subagent re-parenting logic to avoid stack overflows, combining creation and re-parenting counts in the broadcast event summary, ensuring the database is closed in the shutdown timeout backstop to prevent corruption, and guarding against potential TypeError crashes in the WebSocket broadcast function during shutdown.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread scripts/import-history.js
Comment thread server/routes/hooks.js Outdated
Comment thread server/index.js Outdated
Comment thread server/websocket.js
hoangsonww and others added 17 commits July 1, 2026 08:21
…h-water-mark token baseline

Two pre-existing bugs surfaced while diagnosing a slow `npm run dev` and a
$22.5k dashboard cost.

Startup unresponsiveness / WebSocket "closed before established":
- The startup sync sweep re-imports every transcript whose file mtime is
  newer than its DB updated_at. importSubagentFromJsonl dedups each tool
  event with `WHERE agent_id = ? AND event_type = ? AND data LIKE '%id%'`,
  but there was no index on events.agent_id, so each dedup full-scanned the
  events table. On a large DB one subagent-heavy session took ~50s,
  blocking the event loop so the Vite /ws proxy handshake timed out. Add
  `idx_events_agent_type (agent_id, event_type)` — that single session's
  re-import drops 50s -> <1s (measured on the real 362MB DB).
- Defer the immediate sweep so HTTP + WS serve the first page load first,
  and yield (setImmediate) between heavy re-parses so a cold catch-up never
  monopolizes the loop. Measured: time-to-first-API-response 42.7s -> 0.20s.

Token cost double-counting ($22.5k -> ~$15.2k true):
- Two writers hit the same token_usage bucket with different scopes — the
  live hook writer stores main-transcript-only tokens, importSession stores
  main+subagents combined. replaceTokenUsage's old "baseline += current on
  any decrease" logic mistook every downward fluctuation for a compaction
  and accumulated baseline without bound. One 26-day/80-repo session
  (a33b4922) reached baseline_cache_read = 8.5B vs its real 774M (proven
  against the raw transcript), inflating the global total by ~$7.3k.
- Rewrite replaceTokenUsage as a monotonic high-water mark:
  `baseline := max(old_live + old_baseline - new_live, 0)`, so
  `effective = max(old_effective, new_live)`. Never decreases (compaction
  safe), never inflates past the true max, idempotent and writer-order
  independent. New token-baseline.test.js covers growth, idempotency,
  decrease-preserves-max, and the exact writer-alternation runaway.

Also guards idx_events_agent_type existence in a test, and updates
ARCHITECTURE.md (index table, sweep behavior, high-water-mark token model).

Note: the existing a33b4922 baseline was separately repaired in the live DB
(baselines zeroed; live columns already matched the transcript), correcting
the shown total to ~$15.2k. The high-water-mark logic keeps it there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A session that hit a transient API error (e.g. "Connection closed
mid-response") got marked `error`, but the CLI auto-retries and keeps
working — so the session recovered while the dashboard showed `error`
indefinitely. Recovery only fired on a live UserPromptSubmit/PreToolUse
hook; a session monitored via the transcript sweep (imported, or whose
recovering hooks never reached the dashboard) had no path back, and
SessionEnd then froze it in `error` forever. One real 26-day session
showed `error` with 902 healthy assistant turns after its last error.

Fix: treat an error as current only when it sits at the transcript tail
(new isErrorAtTail: latest API error has no successful turn after it,
computed from the errors[].timestamp vs lastTurnTs the transcript cache
already exposes — no re-parse).

- The 15s watchdog now scans `error` sessions too; when the transcript has
  progressed past the last error, it clears the session back to `active`.
- SessionEnd preserves `error` only if isErrorAtTail; a recovered-then-
  exited session finalizes as `completed`.

Docs updated: server/README (error flow + state machine), docs/HOOKS
(SessionEnd), ARCHITECTURE (hooks.js row). Existing error sessions
self-heal on the next watchdog tick after deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude-sonnet-5 had no row in model_pricing, so calculateCost treated it
as unpriced and reported $0 despite real token volume (one session had
34.7M input / 112.7k output / 38.0M cache-read on Sonnet 5). Add the
Sonnet-tier rate ($3 in / $15 out / $0.30 cache-read / $3.75 5m-write /
$6 1h-write, no fast tier) to DEFAULT_PRICING.

The startup top-up (INSERT OR IGNORE of DEFAULT_PRICING, runs every boot,
preserves user edits) adds the row to existing DBs automatically, and cost
is computed live from token_usage x model_pricing at query time — so every
user gets Sonnet 5 pricing applied RETROACTIVELY to historical tokens on
their next dashboard restart, no reimport or reset needed.

Pattern matching is longest-pattern-first regex (`%` -> `.*`, anchored):
`claude-sonnet-5%` matches bare `claude-sonnet-5` and any dated
`claude-sonnet-5-YYYYMMDD`, and cannot cross-match `claude-sonnet-4-*`.
Verified tokens are genuinely attributed to claude-sonnet-5 (transcript
model strings == DB, no 4.x mislabeling). New tests cover dated-id match
and no-cross-match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ND after promo cutoff

Sonnet 5 launched with an intro discount ($2/$10 per MTok) through
2026-08-31, reverting to list ($3/$15) after. A single rate per model
can't bill both eras correctly: usage during the promo must stay at intro
forever, and usage after must use list.

Add time-limited intro rates to model_pricing (additive columns:
intro_input/output/cache_read/cache_write/cache_write_1h + intro_until).
When intro_until is set, usage on/before that date prices at the intro_*
rates and usage after at the standard rates.

- calculateCost + ratesForBucket take an asOf date; each token bucket is
  priced by its own usage date where one exists (row.date), else the
  caller's asOf, else today.
- /api/pricing/cost now prices the per-day usage rows, so the aggregate
  total AND per-model breakdown split intro/list correctly across the
  cutoff (breakdown re-collapses per model; coverage is identical to the
  old undated query — token_usage cascades with sessions).
- Per-session cost (/cost/:sessionId, sessions list) prices as of the
  session's start date; analytics joins the session date too.
- Sonnet 5 intro rates seeded via applyIntroPricing(), run at startup and
  on reset-pricing; idempotent (only fills rows whose intro_until is NULL,
  so user edits/clears in Settings are preserved). Editing standard rates
  via Settings preserves intro (ON CONFLICT UPDATE touches only listed
  columns). Applies retroactively on restart — no reimport.

Verified: pre-cutoff Sonnet 5 = $14.71 (intro), post-cutoff = $22.07
(list), on the real DB. New tests cover before/on/after cutoff, per-row
date precedence, and no-intro rules. server 522/522, client, mcp, build.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Active Subagents stat card showed "N total" as its trend, where N is
allSubagents.length — the subagents across currently-ACTIVE sessions, not
an all-time all-sessions count. With a single active session it reads as
"this session only", so "total" is misleading.

Relabel the trend to "N in active sessions" (en; zh/vi mirrored) so the
scope is explicit. Card value (working subagents) and trend (pool in
active sessions) now share a coherent, honest scope. Snapshot baseline
regenerated for the label change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sion ticks

The prior self-heal only ran in the watchdog, which re-examines STALE
sessions (updated_at older than the threshold). An actively-running
session — transcript and cost still growing — is never stale, so an error
set by a transient API blip stayed `error` forever unless a
UserPromptSubmit/PreToolUse hook happened to fire (Stop / SubagentStop /
PostToolUse / Notification don't trigger the normal recovery). That's why
one long-running session kept showing Error even after a dev-server
restart.

Add a self-heal in processEvent: on any hook event carrying a transcript,
if the session is `error` but the transcript has progressed past the last
API error (isErrorAtTail false), reactivate it. isErrorAtTail is
transcript-driven, so a genuinely current error (at the tail) is left in
place. Verified: the stuck 26-day session recovered error -> waiting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pricing table only showed standard rates, so Sonnet 5 read as $3/$15
with no hint of its active launch discount — looking like the promo wasn't
applied. Surface intro rates on any row that has them: a violet sub-line
under the model name ("Intro $2/$10 through 2026-08-31") and the intro
per-MTok values beneath the standard Input/Output cells. Rows without an
intro rate are unchanged. Editing standard rates still preserves intro
(the API's ON CONFLICT UPDATE only touches listed columns). Adds the
intro_* fields to the client ModelPricing type + an introNote string
(en/zh/vi); Settings snapshot regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fold in the PR #200 review feedback (Copilot + Gemini) and harden the
session error-state machine so recovered API errors never flip a session
to Error.

server/index.js (graceful shutdown)
- Prefer httpServer.closeIdleConnections() over closeAllConnections() so
  in-flight requests drain instead of being killed mid-flight (the whole
  point of closing the DB in the close() callback); fall back to
  closeAllConnections only on runtimes without the idle variant.
- The 5s force-exit backstop now also closeDb() — if close() never drains
  (a stuck request), its callback never runs, so this is the only path
  that flushes SQLite before exit (closeDb is idempotent).

server/routes/hooks.js (error flapping)
- processEvent + watchdog now flip a session to `error` only when a NEW
  error is recorded AND it is still unrecovered at the transcript tail
  (isErrorAtTail). A transient error the CLI already retried past (later
  successful turns) records its APIError event for history but must not
  trip the whole session to Error. This is the primary guard; the
  self-heal remains the safety net for sessions flipped by older builds.
- Combine the SubagentJsonlImported broadcast summary so a call that both
  imports and re-parents reports both counts.

scripts/import-history.js (reconcileSubagentParents)
- Cycle guard: skip a parent link that would make the proposed parent a
  descendant of the child, so a corrupt DAG can't create a loop that the
  recursive tree builders spin on.

client/src/pages/Dashboard.tsx, SessionDetail.tsx
- Guard the recursive agent-tree renderers and descendant counters against
  a cyclic parent_agent_id (corrupt data) via an ancestors set / seen set /
  cache sentinel, so a bad link can't stack-overflow the page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard "Active Subagents" stat counts subagents only (it excludes
main agents), but the main-agent cards showed `agent_count`, which
includes the main itself. So two active sessions reading "29 agents" and
"2 agents" summed to 31 while the stat said 29 — they never reconciled.

Show `agent_count - 1` (the subagents the session spawned) on the main
card via a new `session.subagentSummary` plural key (en/zh/vi). Now the
per-card counts sum to the "Active Subagents" total: 28 + 1 = 29.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The agent-tree "N subagent" badge on the Dashboard rendered
t("common:subagent", { count }) — but common:subagent is a flat word,
not an i18next plural key (no _one/_other), so the count never selected a
plural and it always read "2 subagent", "5 subagent", etc.

Use common:subagent_label (which already has _one/_other and embeds the
count) — the same key SessionDetail's badge uses — so both surfaces read
"1 subagent" / "2 subagents" consistently. Add an i18n test locking the
plural forms for common:subagent_label and kanban:session.subagentSummary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Users could edit a model's standard rates but not its time-limited
introductory (promo) rates — those were display-only, so the Sonnet 5
intro pricing could only be changed by editing seed code. Since intro
promos are not a one-off (any future model launch may ship one), make the
whole mechanism editable and generic.

Backend
- New setIntroPricing prepared statement updates ONLY the intro_* columns,
  kept separate from upsertPricing so a standard-rate edit never touches
  the promo and vice versa.
- PUT /api/pricing now accepts optional intro_input/output/cache_* and
  intro_until. It only writes intro columns when the caller sends at least
  one intro field, so older clients that PUT just the standard rates
  preserve any existing promo (backward compatible). intro_until is
  validated as YYYY-MM-DD; an empty value clears the promo and zeroes the
  intro rates (so a re-added date can't resurrect stale values). Both
  writes run in one transaction.

Frontend (Settings → Model Pricing)
- The edit/add form gains a second row: an "Introductory rates" box with a
  cutoff-date field plus intro input/output/cache-read/cache-write(5m/1h)
  inputs. Empty date = no promo. Client mirrors the YYYY-MM-DD validation.
- The display row now shows the intro subline on every rate column (was
  input/output only) so all editable intro values are visible.

Nothing here is Sonnet-5-specific: the date-driven promo window works for
any model pattern, so a future launch promo needs no code change — just an
edit in the UI. Adds a server route test covering persist / preserve /
clear / reject-malformed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n total

Subagent cards (Dashboard tree, Session Detail tree, Kanban) rendered the
session's total cost because AgentCard always read session.cost. On a
subagent card that reads as if that one subagent cost the entire session's
spend — misleading.

Derive per-subagent cost accurately instead:
- Import: parseSubagentFile already extracts each subagent's own token
  buckets (their transcripts are separate JSONL files). importSubagentFromJsonl
  now stamps those buckets into the agent's metadata.tokens — on both the
  JSONL-keyed row and, via backfill, the live hook-created row. Covers the
  bulk-import and live SubagentStop-scan paths (both funnel through it).
- API: new pricing.attachAgentCosts() computes each agent's OWN cost from
  metadata.tokens with the CURRENT pricing rules (priced at the agent's
  start date, so promo/standard cutovers apply — same as session cost, and
  consistent with editable intro pricing). Wired into GET /api/agents and
  GET /api/sessions/:id.
- UI: AgentCard shows session.cost for a main agent (it stands in for the
  whole session) and agent.cost for a subagent. A subagent with no recorded
  usage shows no cost — truthful rather than misleading.

Session total is unchanged (combineSessionTokens still sums main + all
subagents). Existing subagent rows backfill their metadata.tokens on the
next import/rescan of their transcript; until then their card simply omits
cost. Adds server + client tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per-agent cost only appeared on subagents (re)imported after the feature
landed. Historical sessions whose transcripts never change again are
mtime-skipped by the continuous sync, so their subagents never gained a
metadata.tokens bucket — their cards showed no cost (e.g. every subagent
under an old "Create feature branches…" session).

Add a startup backfill (backfillSubagentTokenMetadata) that re-parses the
subagent transcripts for any session still holding a non-compaction
subagent without a tokens key and stamps their metadata. It routes through
importSubagentFromJsonl, so it is METADATA-ONLY — it never writes session
token_usage, leaving session totals untouched; only the per-agent
breakdown is filled in. Wired into startBackgroundServices as a deferred,
non-blocking, self-limiting pass (rows with a tokens key are skipped, so
steady-state startups do near-zero work). importSubagentFromJsonl now also
stamps an empty tokens array on genuinely-zero-usage rows so they aren't
re-scanned on every boot.

Existing dashboards backfill automatically on their next server restart.
Adds a server test (stamps from transcript, no token_usage side effects,
self-limiting on a second run).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…widen backfill

Audit of the import pipeline (verified against the live DB) surfaced several
correctness/completeness gaps; this fixes them:

1. Duplicate subagent rows. importSession created BOTH `-subagent-N` rows
   (from the main transcript's Agent blocks, via importSubagents) AND
   `-jsonl-` rows (from subagent transcripts), deduped only by a fragile
   type+timing match that misses when the two timestamps differ — so every
   subagent was doubled (140 dupe rows across 16 sessions live). Now
   importSubagents runs ONLY when there are no subagent transcripts; when
   transcripts exist, the richer `-jsonl-` rows are authoritative. Fixes
   inflated agent/subagent counts, the hierarchy tree, and $0 twin cards.

2. transcript_path never persisted on import (insertSession has no such
   column and nothing called setSessionTranscriptPath). 899/1023 sessions
   had it NULL, which silently disabled the abandon sweep, compaction
   scanner, and per-agent cost backfill for imported sessions. parseSessionFile
   now returns its source path and importSession stamps it (both branches).

3. backfillSubagentTokenMetadata widened: no longer requires transcript_path
   (derives <projectsDir>/<slug>/<sid>.jsonl when the column is NULL) and now
   uses findSessionSubagents so BOTH on-disk layouts are covered, not just the
   flat one.

4. classifyJsonl misclassified dynamic-workflow inner-agent transcripts
   (subagents/workflows/<run>/agent-*.jsonl) as top-level sessions on
   directory import, creating bogus `agent-<id>` sessions. It now treats any
   file under a `subagents/` ancestor (at any depth) as a subagent.

5. Re-import metadata completeness: the metadata-refresh gate now also fires
   when turn_count / thinking_blocks grew (not only message counts), and the
   subagent merge-into-live path now enriches tools / message counts /
   thinking blocks (previously only model + tokens), so hook-created subagent
   rows don't render with empty metadata.

Confirmed correct-as-designed (no change): session token_usage is not
double-counted (per-agent metadata.tokens is a separate store), token writes
are idempotent (high-water-mark), and workflow inner-agents get per-agent
cost via workflow-ingest when their transcript exists.

Adds server tests for the no-duplicate rule, the fallback, transcript_path
persistence, and the classifyJsonl workflow guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…urfaces

Propagate the PR's observable changes into the doc set (en/vi/zh + wiki +
landing), per the update-project-docs mapping.

- Editable introductory (promo) pricing: README (+VN/CN) Cost Tracking &
  Settings rows, docs/API.md (PUT /api/pricing intro fields), docs/DATABASE.md
  (model_pricing intro_* + intro_until columns), ARCHITECTURE.md (pricing row),
  server/README.md, wiki (+ zh/vi i18n), index.html landing.
- Per-subagent cost on cards: same surfaces — agent-list responses now carry a
  per-agent `cost` (docs/API.md agent response + note), computed from
  agents.metadata.tokens (docs/DATABASE.md agents.metadata note), via
  pricing.attachAgentCosts (ARCHITECTURE.md agents/pricing rows).
- docs/DATABASE.md: correct the pricing table (was documented as `pricing_rules`
  with per-1m fields) to the real `model_pricing` schema incl. cache/fast/intro
  columns; add the `idx_events_agent_type` index; note metadata.tokens.
- docs/API.md: pricing mutation is `PUT` (was documented as `POST`), with the
  real field names + intro block + validation.
- Import correctness: ARCHITECTURE.md import row now describes skipping
  importSubagents when transcripts exist (no duplicate rows), persisting
  transcript_path on import, stamping per-agent metadata.tokens +
  backfillSubagentTokenMetadata, and classifyJsonl covering the workflow tree.
- README env table (+VN/CN): document DASHBOARD_STALE_MINUTES (idle→abandoned
  timeout), which existed only in .env.example and prose.
- Wiki cache bumped (CACHE_NAME wiki-v26→v27, i18n-content.js?v=17→18).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hoangsonww and others added 2 commits July 1, 2026 19:17
The main-agent card renders session.cost, but /api/sessions/:id returns the
raw session row, which has no cost column — SessionDetail loads the total
separately into `cost` state — so the main card showed no cost on this page
(the Dashboard works because the sessions-list endpoint computes per-session
cost). Inject the loaded total into the card's session prop. Subagent cards
are unaffected (they use agent.cost). Adds a SessionDetail regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion)

The Session Complexity Scatter card rendered shorter than its side-by-side
companion (Compaction Impact Analysis) in the same lg:grid-cols-2 row. The
grid stretches the Section wrapper to the tallest sibling, but the inner
`.card` shrank to its content, so the shorter chart left a shorter card.
Make Section a flex column with `h-full` and its card `flex-1` so the
row-stretch reaches the card; the scatter fills its card and centers so the
taller row height doesn't leave a gap. Applies to every Workflow section, so
all side-by-side chart cards now align. Snapshot regenerated (className-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hoangsonww hoangsonww merged commit 52c9a5e into master Jul 2, 2026
19 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

bug Something isn't working documentation Improvements or additions to documentation enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed question Further information is requested

Projects

Development

Successfully merging this pull request may close these issues.

2 participants