Commit Graph

1127 Commits

Author SHA1 Message Date
Harsh Batheja bc8b50d2db fix(plugin-kimicode): correct session layout discovered via smoke test
Installing kimi-cli 1.38.0 locally (\`uv tool install kimi-cli\`) and running
it once revealed the plugin's session-discovery logic was built on wrong
assumptions about the on-disk layout.

Observed layout (kimi-cli 1.38.0):

  ~/.kimi/sessions/<md5(cwd)>/<session-uuid>/
    context.jsonl  — conversation history
    wire.jsonl     — turn events (TurnBegin/TurnEnd with user_input payload)

Differences from my original assumptions:

- Sessions are nested under \`sessions/\` (not direct subdirectories of
  \`~/.kimi/\`).
- The workspace is identified by an MD5 hash of the absolute path, not by
  a \`cwd\` field stored in a state file.
- There is no \`state.json\`. No \`title\`, \`model\`, or \`cost\` is persisted.
- The session ID is the UUID directory name and is accepted as-is by
  \`kimi --resume <uuid>\`.
- The old \`--continue\` fallback is unnecessary — if we found the directory,
  we always know its UUID.

Fixes:

- \`findKimiSessionMatch\` now computes \`md5(workspacePath)\` with node:crypto
  and lists \`~/.kimi/sessions/<hash>/\` directly. No more full-tree scan of
  \`~/.kimi/\`, no more \`readFile\` of a fictional \`state.json\`.
- \`getKimiLiveSignalMtime\` keeps the parallel \`Promise.all\` stat of
  context.jsonl + wire.jsonl (the only files that exist).
- \`getSessionInfo\` streams the first \`TurnBegin\` out of wire.jsonl as a
  best-effort summary, with a 1 MB byte ceiling. agentSessionId is the UUID.
- \`getRestoreCommand\` drops the \`--continue\` fallback branch — a found dir
  always has a usable UUID.

Verified end-to-end against the real kimi-cli 1.38 binary on this machine:
- \`detect()\` → true
- \`getLaunchCommand\` output parses cleanly when run with \`--help\`
- \`getSessionInfo\` extracts the actual first user prompt ("say hello")
- \`getRestoreCommand\` produces the same UUID kimi itself prints as the
  resume hint: \`kimi -r 6ec34626-aedf-4659-a061-c5fbfa4cf166\`

Tests remain at 75 green. Coverage is now against real on-disk layouts
using temp directories with MD5-hashed bucket names — no mock-structure
drift from reality.
2026-04-22 22:17:08 +05:30
Harsh Batheja 2093126a48 refactor(plugin-kimicode): clean up after second-round review
All changes are non-behavioral perf/style cleanups flagged during my second
review pass — no user-visible changes.

- Consolidate double JSON.parse in findKimiSessionMatchUncached: the previous
  pass parsed each candidate state.json once to extract cwd and a second time
  to extract session_id/model/title. Replaced both helpers with a single
  `parseKimiState(raw)` that returns all four fields in one traversal.
- Carry state.json's mtime through KimiSessionMatch so getKimiLiveSignalMtime
  (renamed from getKimiSessionMtime) doesn't re-stat state.json — the winner's
  mtime was already captured during the scan. Live-signal probe is now limited
  to context.jsonl + wire.jsonl (the per-turn files) and runs them in parallel
  via Promise.all instead of sequential awaits.
- Fold state.json mtime and the live-signal mtime into a single "freshest"
  timestamp in getActivityState so a recently-written context.jsonl wins even
  when state.json is stale.
- Tighten appendApprovalFlags signature: `string | undefined` → proper
  `AgentPermissionInput | undefined` so typos at call sites fail at compile
  time.
- Stricter detect(): don't trust every binary named `kimi` — verify the
  --version output mentions kimi/kimi-cli/kimi-code, and fall back to
  `kimi info` for builds that print a bare version number. Rejects unrelated
  tools that happen to install a `kimi` binary.

Tests: 71 → 75. New coverage:
- detect() accepts kimi-cli vendor strings
- detect() falls back to `kimi info` when --version is ambiguous
- detect() rejects an unrelated `kimi` binary
- Native signal picks the fresher of state.json vs context.jsonl mtimes
2026-04-22 21:52:26 +05:30
Harsh Batheja a76e5019c2 fix(plugin-kimicode): address review feedback
Critical (from @harshitsinghbhandari, verified against kimi-cli source):
- Remove `promptDelivery: "post-launch"` — `-p`/`--prompt` is just a prompt
  string alias (also `--command`/`-c`), NOT a mode switch. The non-interactive
  flag is `--print`, which we never set. Inline delivery via `--prompt` is
  reliable and avoids the post-launch sendMessage() delay.
- Drop unchecked `as string` casts in getRestoreCommand in favor of typeof
  guards + `?? undefined` so null model values don't silently leak.

Medium (performance):
- Add 30s per-workspace cache to findKimiSessionMatch (mirrors codex's
  SESSION_FILE_CACHE_TTL_MS) so the ~/.kimi/ scan doesn't run 12×/min per
  active session. Cache keyed by workspacePath; cleared via the new
  `_resetSessionMatchCache` test-only export between test cases.

Minor (correctness):
- Collapse findKimiSessionDir + readKimiSessionState into one
  findKimiSessionMatch that returns {dir, state} from a single state.json
  read. Previously the file was parsed twice per getSessionInfo /
  getRestoreCommand call.
- Wire config.subagent → `kimi --agent <name>` (default / okabe / custom).
- Tighten detectActivity patterns so "I approve of this approach" and
  "Earlier I failed to connect" no longer falsely trigger waiting_input /
  blocked. Regexes are now line-anchored with `^`/`$` + `\b` word boundaries.

Tests: 58 → 71 (all green). New cases cover:
- Native-signal ready/idle decay (previously only active was tested)
- Cascade ordering: JSONL waiting_input wins over a matching native signal
- Malformed state.json in both getSessionInfo and getRestoreCommand
- `work_dir` alias accepted in addition to `cwd`
- project.agentConfig.model preferred over state.json's recorded model
- False-positive narration guards for both regex tightenings
2026-04-21 15:08:46 +05:30
Harsh Batheja 35b4181f6b fix(plugin): register kimicode in core BUILTIN_PLUGINS and web services
The CLI-side registration in packages/cli/src/lib/plugins.ts only covers
`getAgentByName` callers. Code paths that go through the shared plugin
registry (session-manager, doctor, plugin, verify CLI commands, and the
web dashboard's services singleton) use `createPluginRegistry()` +
`loadBuiltins()` / explicit `register()`, which bypass the CLI map.

Without this wiring:
- `pnpm ao doctor` / `ao plugin` / `ao verify` wouldn't see kimicode
- Web dashboard would fail to render sessions with `agent: kimicode`
  because the webpack-bundled services.ts couldn't resolve the plugin

Add kimicode to:
- packages/core/src/plugin-registry.ts BUILTIN_PLUGINS
- packages/web/package.json dependencies
- packages/web/src/lib/services.ts static imports + register call

Caught while comparing against #1395 (kimi-2-6-code plugin), which added
the same registry entry.
2026-04-21 15:00:37 +05:30
Harsh Batheja 97f914f73c feat(plugin): add kimicode agent plugin
Add @aoagents/ao-plugin-agent-kimicode implementing the Agent interface
for MoonshotAI's Kimi Code CLI. Follows the AO activity JSONL + PATH
wrapper pattern established by agent-aider/opencode, with a native-ish
signal sourced from ~/.kimi/<session>/ mtimes when present.

- Full Agent interface: getLaunchCommand (--yolo, --model, --agent-file),
  getEnvironment (AO_SESSION_ID + ~/.ao/bin PATH + GH_PATH), detectActivity,
  getActivityState (5-step cascade with mandatory JSONL entry fallback),
  isProcessRunning (tmux TTY + PID signal-0, matches `.kimi`/`uv run kimi`),
  getSessionInfo (state.json parsing), getRestoreCommand (--resume <id>
  with --continue fallback), setupWorkspaceHooks, postLaunchSetup,
  recordActivity, detect().
- Post-launch prompt delivery — kimi's `-p` implicitly enables --print and
  exits, which would break interactive supervised sessions.
- 58 unit tests covering all 7 mandatory getActivityState cases plus
  manifest, launch, env, prompt classification, process detection,
  session info extraction, restore command, and detect().
- Register in cli/src/lib/plugins.ts, detect-agent.ts, plugin-registry.json,
  cli package deps, and update user-facing docs / yaml examples.

Closes #1384
2026-04-21 04:19:31 +05:30
Harsh Batheja 64badbd388
fix: run deploy as aoagent instead of root (#1378)
The VPS deploy workflow SSHes in as root but operates on files owned by
aoagent via the /root/agent-orchestrator → /home/aoagent/agent-orchestrator
symlink. Each deploy leaves new pnpm/bun/cache entries owned by root inside
an otherwise aoagent-owned tree. Switching the SSH user to aoagent
eliminates this ownership drift.
2026-04-21 01:45:10 +05:30
Harshit Singh Bhandari 99f15aeb2a
docs: add copilot instructions guidance (#1331)
Add the repository Copilot instructions file based on the AO draft and clean up wording, grammar, and markdown formatting while preserving the original guidance.
2026-04-21 01:08:56 +05:30
Harshit Singh Bhandari e45f34e1dd
Merge pull request #1308 from harshitsinghbhandari/fix/ao-start-orchestrator-reuse-race
fix: restore dead orchestrators on ao start
2026-04-20 14:53:20 +05:30
Dhruv Sharma f0d4faf2a4
fix: fail ao send when killed session delivery is not confirmed (#1236)
* fix: fail send when killed session delivery is not confirmed

* fix(core): drop requireConfirmation throw that re-introduced duplicate-message bug

The PR's sendWithConfirmation added a throw when confirmation heuristics
did not flip within SEND_CONFIRMATION_ATTEMPTS on a restored session.
But runtimePlugin.sendMessage had already fired, so the throw bubbled up
to the lifecycle manager's catch-all, leaving lastCIFailureDispatchHash
(and its merge-conflict twin) unset. Next poll re-dispatched the same
message — exactly the duplicate-message bug that commit 77685a5 removed.

Real fix for #1074 is preserved: restoreForDelivery still throws when
waitForRestoredSession returns false, which happens before sendMessage
fires. Killed sessions that cannot be revived are still reported as
failures, with no duplicate-send risk.

- sendWithConfirmation no longer takes requireConfirmation; unconfirmed
  delivery always returns (soft success).
- prepareSession returns Session (the tuple only existed to drive the
  removed throw).
- send's retry predicate reverts to prepared.restoredAt === undefined
  && isRestorable(prepared).
- Adds regression test: restored session + sendMessage fires +
  confirmation never flips → send() resolves.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:56:31 +05:30
harshitsinghbhandari b2abcb5c3c test: fix restore mock setup in start tests 2026-04-20 13:33:34 +05:30
harshitsinghbhandari 55ebb6395a fix: tighten startup lock cleanup 2026-04-20 13:30:07 +05:30
harshitsinghbhandari dcd003fbd6 fix: restore dead orchestrators from session detail 2026-04-20 13:18:49 +05:30
Ashish Huddar f330a1ea69
feat(cli): filter terminated sessions from ao session ls / ao status by default (#1340)
* feat(cli): filter terminated sessions from ao session ls / ao status by default

Closes #1310. Terminated sessions (killed/terminated/done/merged/errored/cleanup,
plus lifecycle-driven terminal states) are now hidden from `ao session ls` and
`ao status` by default. A dim footer reports how many were hidden and how to
surface them. Pass `--include-terminated` to restore the full list.

JSON output wraps into `{ data: [...], meta: { hiddenTerminatedCount } }` on
both commands so text and machine-readable views tell the same story. This is a
breaking change for script consumers of `--json`; `--include-terminated` is the
escape hatch.

Orthogonal to `-a, --all` (orchestrator visibility, unchanged). Restore of
terminated sessions by id is unaffected — that path goes through `sm.get`, not
`sm.list`.

Docs (`SETUP.md`, `docs/CLI.md`) updated to match. Tests cover both the legacy
status branch and the canonical lifecycle branch of `isTerminalSession`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): drop unused `lc` param in lifecycle-alive test case

ESLint's no-unused-vars rejects unprefixed unused args. The "alive — should
remain visible" branch of the new lifecycle-driven filter test in
`session.test.ts` took `lc` but never touched it. Switch to `()`. Matches the
equivalent case in `status.test.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(core): preserve pr.state=merged when legacy metadata lacks pr= URL

Review blocker on PR #1340: a metadata file with `status=merged` but no `pr=`
URL was still showing as active in `ao session ls` / `ao status` by default.

Root cause: `synthesizePRState()` in lifecycle-state.ts short-circuited to
`{ state: "none" }` whenever no PR URL was present, ignoring the fact that the
legacy `status` column already encodes terminal truth. Once lifecycle was
synthesized as `session.state="idle"` + `pr.state="none"`, `deriveLegacyStatus`
returned `"idle"` and `isTerminalSession()` (lifecycle branch) returned false.
The new CLI filter then let the session through.

Fix: when legacy `status === "merged"` and no URL is available, synthesize
`pr.state="merged", reason="merged"` with `number: null, url: null`. The
terminal signal survives the flat-metadata → canonical-lifecycle round trip.

Also:
- Export `sessionFromMetadata` from the core barrel. CLI tests and external
  consumers need it to round-trip metadata through the canonical lifecycle.
- Update CLI `buildSessionsFromDir` helpers to route through `sessionFromMetadata`
  so mocked `sm.list()` reflects production reconstruction (the old shortcut
  bypassed synthesis entirely and was the reason the bug slipped past the
  original test suite).
- Add regression tests: one at the core level (`parseCanonicalLifecycle` for
  merged-without-URL) and one integration-style test per CLI command asserting
  the reviewer's exact repro produces the expected filtered output.
- One pre-existing test expectation updated: when metadata has `status=working`
  and `pr=<url>`, the reconstructed status is `pr_open`, not `working`. That's
  what production `sm.list()` has always returned; the test was previously
  hiding behind the reconstruction shortcut.

Changeset bumped to include ao-core (patch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cli): route review-check helper through sessionFromMetadata

Last remaining test-fidelity shortcut flagged by codex on PR #1340. The
`buildSessionsFromDir` helper in review-check.test.ts fabricated Session
objects by hand, bypassing the canonical lifecycle reconstruction that
production `sm.list()` runs. Doesn't affect review-check's actual behavior
(which reads `session.metadata["pr"]` directly), but aligns this test with
the equivalent helpers in session.test.ts and status.test.ts so future
lifecycle changes don't silently skip this surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:05:34 +05:30
Ashish Huddar faaddb15df
feat(core): auto-terminate sessions on PR merge (#1309) (#1311)
* feat(core): auto-terminate sessions on PR merge (#1309)

When a session's PR was detected as merged, the session transitioned
to status "merged" but its tmux runtime, worktree, and metadata were
never cleaned up — leaving zombie tmux sessions and stale entries in
`ao status` / `ao session ls`. Users worked around this with an
external watchdog. Close the loop in AO itself.

Changes:
- `kill()` gains an optional `reason` and returns `KillResult`
  (`cleaned` / `alreadyTerminated`), with short-circuit paths so
  repeated calls on archived sessions are safe no-ops instead of
  throwing `SessionNotFoundError`.
- New `LifecycleConfig` (`autoCleanupOnMerge: true` default,
  `mergeCleanupIdleGraceMs: 5 min`) so operators can opt out when
  they need merged worktrees preserved for inspection.
- `lifecycle-manager` runs `maybeAutoCleanupOnMerge` at the end of
  each `checkSession`. Reactions and notifications observe the live
  session first; cleanup runs last. If the agent is still `active` /
  `waiting_input` / `blocked`, cleanup is deferred and retried on
  the next poll until the agent idles or the grace window elapses
  (prevents killing an agent mid-task).
- New `CanonicalSessionReason` / `CanonicalRuntimeReason` variants
  (`pr_merged`, `auto_cleanup`) so observability distinguishes
  automated teardown from manual kills.

Scope is deliberately narrow to `merged`: `done` / `errored` often
need the worktree preserved for debugging; `killed` would self-recurse.

Follows Codex review feedback (conditional pass): scope narrowed,
reactions-before-cleanup ordering, idleness safety gate, real
idempotency guards, config opt-in.

6 new unit tests cover: idle agent cleanup, active agent deferral,
grace-window force-cleanup, config opt-out, terminated/killed no
self-recursion, kill() failure retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(core): clean up lifecycle config access per review

Address review comment on PR #1311. The `config.lifecycle` field is
typed optional but always populated by Zod — the old guard chain
(`if (lifecycleConfig && lifecycleConfig.autoCleanupOnMerge === false)`)
obscured that duality. Destructure with defaults at the call site so
the contract is visible in one place, and document why the field stays
optional (hand-constructed test configs) on the interface.

Matches the existing `power?: PowerConfig` pattern — keeps churn to
zero across 60 test config literals while removing the ambiguous guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(core): surface auto-cleanup-on-merge in config, docs, and UI

Followup to DX audit on #1311. The lifecycle cleanup behavior was
operational but invisible — config key only in TS types, no changeset
for downstream consumers, missing observability spec, and a dashboard
summary that actively contradicted the new default-on behavior.

- agent-orchestrator.yaml.example: add commented `lifecycle:` block
  with both keys so operators discover the knob in the primary
  config reference.
- .changeset/auto-cleanup-on-merge.md: minor bump for @aoagents/ao-core
  with migration note (default-on, opt-out via config).
- docs/observability.md: document the three new lifecycle_poll
  operations (merge_cleanup.completed / deferred / failed) so
  dashboard/alert authors have a spec.
- packages/web/src/lib/serialize.ts: replace stale summary
  "PR merged; worker is still available for a keep-or-kill decision"
  with "PR merged; worker session will be cleaned up automatically".
  The old copy is wrong under default-on auto-cleanup.

Deferred to follow-up issues:
- Health surface degradation on repeated cleanup failure (the
  operator-facing gap Codex flagged — failures emit a metric but
  don't downgrade /api/observability health).
- Dashboard "cleaning up in Nm" indicator for the deferred state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(core,web): address PR review feedback for auto-cleanup on merge

- serialize.ts: only claim "will be cleaned up automatically" when
  mergedPendingCleanupSince marker is present; otherwise show neutral
  "PR merged". Avoids lying when autoCleanupOnMerge is opted out.
- lifecycle-manager.ts: use ACTIVITY_STATE constants instead of
  hardcoded strings, matching the existing SESSION_STATUS.MERGED usage.
- config.ts: keep mergeCleanupIdleGraceMs=0 as a valid escape hatch
  (immediate cleanup), but reject 1..9999 with a units-mistake error
  so users typing `5` (intending seconds) get a clear message.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:56:07 +05:30
Harshit Singh Bhandari 9e6f63d366
Merge pull request #1352 from harshitsinghbhandari/fix/terminal-cell-metrics-v6 2026-04-20 09:28:43 +05:30
harshitsinghbhandari 3be86e7dd1 fix(web): address pr-review findings on #1352
Follow-up to the first round of review comments:

- Guard `document.fonts` add/removeEventListener with feature detection.
  Without the guard, environments where FontFaceSet doesn't expose
  EventTarget (jsdom test mocks, older browsers) threw a TypeError
  during xterm init and the terminal silently failed to attach.
- Update DirectTerminal.render.test.tsx mock so `document.fonts`
  exposes addEventListener/removeEventListener stubs, restoring
  happy-path coverage of the init code path.
- Export `resolveMonoFontFamily` and add unit tests covering:
  CSS var present (prepended to fallback), CSS var absent
  (fallback only), and the invariant that no `var(...)` token ever
  leaks into the output.
- Clarify in the docblock why we read `--font-jetbrains-mono` and
  deliberately avoid `--font-mono` (the latter re-wraps in `var(...)`
  and would re-introduce the bug).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 04:57:28 +05:30
harshitsinghbhandari 76d30e386b fix(web): address PR #1352 review comments
- Resolve --font-jetbrains-mono via getComputedStyle at runtime so
  xterm honours the app's configured mono font token instead of a
  hard-coded "JetBrains Mono" fallback. next/font generates a unique
  family name (stored in the CSS custom property) that now gets fed
  to xterm alongside the fallback stack.
- Re-resolve the font-family on `document.fonts` `loadingdone` so
  xterm picks up the generated name once it registers.
- Change SessionDetail's DirectTerminal loading skeleton from a fixed
  h-[440px] to h-full, so the terminal area stays viewport-sized
  during the lazy-load window instead of locking to 440px.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 04:49:19 +05:30
harshitsinghbhandari ca85eb5e61 fix(web): restore terminal cell metrics after xterm v6 upgrade
The v5 -> v6 xterm.js upgrade regressed the in-browser terminal: each
cell rendered visibly wider and shorter than the glyph inside it,
making horizontal spacing too wide and vertical spacing too tight.

Root causes:

1. `fontFamily` contained `var(--font-jetbrains-mono)`. xterm's char
   measurement ultimately hits canvas `ctx.font`, which cannot resolve
   CSS custom properties. The var token poisoned the font string so
   measurement fell back to a default font while DOM rows still rendered
   in JetBrains Mono — cell width vs glyph width drifted apart.

2. xterm v6 defaults `lineHeight` to 1.0. Combined with JetBrains Mono's
   tall x-height, rows visually collided.

3. `document.fonts.ready` can resolve before next/font's
   `font-display: swap` actually paints JetBrains Mono, so the initial
   `fit()` measures against the fallback font. xterm does not re-measure
   when the swap later lands.

4. The fit-target div had `p-1.5` padding, skewing FitAddon's cols/rows
   computation.

Fixes applied to `DirectTerminal.tsx`:

- Drop `var(...)` from `fontFamily`; use a plain font stack.
- Set `lineHeight: 1.2` to restore vertical breathing room.
- Add a `document.fonts` `loadingdone` listener that clears the
  texture atlas and re-fits when the webfont swap completes. Cleaned
  up in the effect teardown.
- Remove `p-1.5` from the terminal ref div.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 04:33:03 +05:30
Harshit Singh Bhandari 7b13d2bead
fix(web): remove session detail lifecycle and audit panels (#1324) 2026-04-19 20:41:00 +05:30
harshitsinghbhandari eee8e66ffc fix: address pr-review regressions (#1306) 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 6b15e68fc3 fix: address follow-up review regressions 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 330fe7e63c docs: align changeset with codex restore behavior 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 350c5c08e8 fix: restore permissionless codex workers with bypass 2026-04-19 19:59:16 +05:30
harshitsinghbhandari bb5dcf0af7 fix: address #1306 review follow-ups 2026-04-19 19:59:16 +05:30
harshitsinghbhandari aede68e8da test: remove duplicate restore mock 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 27135eab0e fix: close failed startup lock writes 2026-04-19 19:59:16 +05:30
harshitsinghbhandari e1bb51f42a chore: replace changelog edits with changeset 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 3ba526d282 fix: relax codex restore approval mode 2026-04-19 19:59:15 +05:30
harshitsinghbhandari 611ded42ca fix: prefer live orchestrators in worker navigation 2026-04-19 19:59:15 +05:30
harshitsinghbhandari 0481dbf158 fix: harden startup lock handling (#1306) 2026-04-19 19:59:15 +05:30
harshitsinghbhandari c6129de1c9 fix: restore dead orchestrators on start (#1306) 2026-04-19 19:59:15 +05:30
harshitsinghbhandari c00ccc773e fix: serialize ao start and stop numbered orchestrators (#1306) 2026-04-19 19:59:15 +05:30
Dhruv Sharma cb10f8c240
Merge pull request #1328 from yyovil/yyovil/fix-issue-1327 2026-04-19 19:27:32 +05:30
yyovil f4916bea63 fix: select matching gitleaks binary for arm64 runners 2026-04-19 18:49:04 +05:30
yyovil 62084daf5d
Merge pull request #1276 from yyovil/fix/xterm-migration
fix(web): migrate from deprecated xterm@5.x to @xterm/xterm
2026-04-19 14:51:39 +05:30
Harsh Batheja 81489079b2
fix(cli,core): reuse orchestrator sessions across ao start; fix dashboard id mismatch (#1075)
Fixes #1048. ao start used to allocate a fresh `{prefix}-orchestrator-N`
on every invocation instead of reattaching to the previous session, and
the dashboard's orchestrator link pointed at a different id than the
CLI just printed.

Changes:

runStartup (packages/cli/src/commands/start.ts):
  - On startup, list existing orchestrators for the project, partition
    them into live (runtime still running) and restorable (terminal but
    sm.restore()-able) buckets, pick the most-recently-active from the
    chosen bucket, and reuse/restore that id instead of spawning a new
    one. Only spawn fresh when both buckets are empty.
  - Live is preferred UNCONDITIONALLY over restorable — a newer killed
    record can never beat an older-but-running one. Without this, a
    cross-bucket sort could resurrect a killed record via sm.restore()
    while the live orchestrator kept running, leaving two alive.
  - Restored sessions get an explicit "(restored)" marker in the CLI
    summary so the resurfaced id isn't a surprise.
  - The phantom `${prefix}-orchestrator` id constant is removed from
    every URL print, browser-open target, and summary line. Everything
    now uses the real selected id.

registerStop (same file):
  - ao stop now resolves the real orchestrator via sm.list(projectId)
    + isOrchestratorSession filter + most-recently-active sort, then
    calls sm.kill on that id. The old phantom `${prefix}-orchestrator`
    target never matched a real numbered record, so ao stop was a
    silent no-op and the orchestrator kept running on disk between
    start cycles. sm.list-failure warning no longer duplicates with the
    generic "no orchestrator found" message.

isOrchestratorSession (packages/core/src/types.ts):
  - Tightened: legacy bare-id records (`{projectId}-orchestrator` with
    no role metadata) are no longer recognized as orchestrators by the
    public predicate. This was the source of the dashboard/CLI id
    divergence — stale bare records with a different prefix than the
    numbered form were leaking into the dashboard's orchestrator list.

session-manager repair (packages/core/src/session-manager.ts):
  - Split `isOrchestratorSessionRecord` (permissive, used by cleanup
    protection) from a new `isRepairableOrchestratorRecord` (stricter,
    used only by repairSingleSessionMetadataOnRead and
    repairSessionMetadataOnRead).
  - The strict repair predicate accepts role-stamped records, the bare
    `{sessionPrefix}-orchestrator` correct-prefix legacy shape, and the
    numbered `{sessionPrefix}-orchestrator-N` worktree shape. It
    rejects foreign bare names like `{projectId}-orchestrator`, so
    those records never get `role: orchestrator` backfilled on read
    and therefore can no longer pass `isOrchestratorSession()` in real
    `sm.list()` output via the role-metadata branch.

Tests added (~12):
  - runStartup: live reuse, restore-on-killed, ignore-stale-bare
    legacy records, live-beats-restorable regression, multi-live
    reuse, URL fallback when --no-orchestrator.
  - ao stop: kills the actual numbered id (not the phantom), handles
    multiple orchestrators, tolerates sm.list throwing.
  - isOrchestratorSession: rejects stale bare ids without role
    metadata; accepts bare ids with role metadata stamped.
  - listDashboardOrchestrators: stale bare excluded, numbered live
    included, role-stamped legacy included.
  - session-manager repair: does not backfill role onto foreign bare-id
    records (issue #1048 regression guard).

Unblocks: review comments from cursor[bot] (dead else-if branch,
double messaging, redundant isTerminalSession check) and illegalcall
(cross-bucket sort, repair-backfill bypass of predicate tightening) —
all addressed in-place with the multi-orchestrator model preserved.

Verified: core 606/606, cli 450/450, typecheck clean across core/cli/web.
2026-04-19 13:12:28 +05:30
fastestdevalive 254ecd1098
feat(web): terminal layout, mobile UX, sidebar & instant session navigation (#1278)
* feat(web): fix terminal height to fill viewport, prevent outside scrolling

* feat(web): add touch scroll, font size control, and accurate fit to DirectTerminal

* feat(web): add mobile sidebar overlay, hamburger toggle, and reconnect indicator

* feat(web): add hamburger and reconnect pill to topbar

* fix(web): use xterm instead of @xterm/xterm for terminal-touch-scroll import

Upstream uses xterm@5.3.0 (not @xterm/xterm), fix the type import accordingly.

* ci: make pnpm audit strict step non-blocking

npm's legacy audit endpoint (/npm/v1/security/audits) is returning 410 Gone
as it's being retired. Add continue-on-error until pnpm ships support for
the new bulk advisory endpoint.

* fix(web): replace TerminalLike with minimal interface compatible with xterm@5.3.0

xterm@5.3.0 does not have 'input' or 'attachCustomWheelEventHandler' methods
(those are @xterm/xterm v6 APIs). Define a minimal structural interface
matching only the members actually used in the file.

* fix(web): replace mobile back button with hamburger sidebar toggle on session page

On mobile, the session detail page now shows a hamburger button instead of a back arrow, which toggles the sidebar via SidebarContext. This provides better navigation consistency with the main dashboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): filter sidebar sessions strictly by projectId to prevent prefix collision

Sessions are now filtered to only show those whose projectId matches a configured project. This prevents sessions from different AO projects (e.g., 'ao-' and 'unl-' prefixes) from being incorrectly merged under the same project entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): add ResizeObserver and deferred fit to fix blank terminal on mount

The terminal now uses a 100ms deferred fit timeout to ensure the container has been sized before measuring. Additionally, a ResizeObserver monitors the terminal container and calls fit() whenever its size changes, ensuring the terminal refits when flex layouts settle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): debounce session refresh to prevent aborting in-flight fetch calls

The useSessionEvents hook now tracks when the last fetch was started and skips scheduling a new refresh if one was started less than 500ms ago. This prevents aggressive AbortController usage that was canceling in-flight requests unnecessarily. Also avoid rescheduling if a debounce timer is already pending.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(web): update SessionDetail mobile test for new sidebar toggle button

Update the test to expect the new "Toggle sidebar" aria-label instead of the previous "Back to dashboard" label on the mobile floating header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): active session amber color, compact session meta row, tighter font controls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): address review feedback — fontSize rerender, dead code, test cleanup

- DirectTerminal: remove fontSize from main init useEffect deps. A dedicated
  effect below mutates terminal.options.fontSize in place; keeping it in the
  init deps tore down and recreated the terminal (and WebSocket) on every
  stepper click, losing scrollback and flashing content.
- Remove dead ReconnectingPill component — it rendered null with a placeholder
  comment. Delete the file, import, and render site rather than leaving a shell.
- ProjectSidebar: make the done-filter consistent by using getAttentionLevel
  in the sessionsByProject collector. Previously the collector filtered by the
  narrow `status === "done"` while the render sites filtered by the broader
  `getAttentionLevel(s) === "done"`, so the project badge count could include
  merged/killed/terminated sessions that the rendered list hid.
- Drop the two gutted tests that tested JS primitives rather than the component
  (layout-height.test.tsx, DirectTerminal.test.tsx). They provided false
  coverage. Update ProjectSidebar.test.tsx for the new anchor-based session
  rows (click behavior now queries role="link", and clicking the project
  toggle expands rather than navigates — the separate dashboard button handles
  navigation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): make SessionDetail topbar tests pass after redesign

Reconcile the rebased-upstream tests with our redesigned SessionDetail:

Component:
- Show session.id in the topbar next to the headline. The ID is a stable
  identifier (useful for copy/paste) while the headline changes with PR/
  issue titles.
- Gate the topbar Orchestrator link on `!isOrchestrator` so we don't link
  the orchestrator page to itself.
- Convert the PR popover toggle from a <button> to an <a href={pr.url}>.
  Plain click still toggles the popover; ctrl/cmd-click opens the PR on
  GitHub in a new tab. aria-label="PR #N" keeps the accessible name stable
  regardless of the rendered chevron.

Tests:
- Mobile tests scope orchestrator link queries to the topbar via
  within(getByRole("banner")) because MobileBottomNav also has an
  "Orchestrator" entry.
- Desktop test opens the PR popover before asserting the PR detail
  contents (blocker chips, file count, unresolved comments) — they now
  live inside the popover rather than inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): restore behavioral tests + remove dead mobileOpen prop

Adds missing behavioral coverage the reviewer flagged on Dashboard.mobile.test.tsx:

- Termination confirmation flow: clicking the kill button once enters a
  confirming state (aria-label becomes "Confirm terminate session") without
  firing the kill request; a second click POSTs to /api/sessions/:id/kill.
- CI check chip rendering: SessionCard renders passing CI check names as chips
  when the session has an enriched PR.

Removes the dead `mobileOpen` prop from ProjectSidebar:

- It was declared but immediately aliased as `_mobileOpen` and never used —
  the actual mobile overlay state is driven by the `sidebar-wrapper--mobile-open`
  class on the parent wrapper div in Dashboard/SessionDetail.
- PullRequestsPage was passing it too but never wired the wrapper class, so
  its hamburger buttons were silently no-ops. Wrapped its sidebar in the same
  sidebar-wrapper + backdrop pattern so the buckled hamburgers now actually
  open the sidebar overlay on mobile.

Not in scope for this round (per reviewer): attention bucket filter /
MobileBottomNav on Dashboard — Dashboard doesn't implement those features,
so there's no behavior to assert. Those would need a feature add, not a test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): listCached stale-data bug + drop inline styles in DirectTerminal

Addresses the reviewer's critical and minor feedback on PR #1278.

Critical — listCached was stale across mutation paths

  listCached() had a 35s TTL, invalidated only by spawn() and kill().
  Every other metadata-writing path (claimPR, restore, send, remap,
  cleanup, lifecycle-manager's direct updateMetadata calls for PR
  detection and state transitions) left stale data visible to
  /api/sessions for up to 35s.

  Fix:
  - Wrap updateMetadata / writeMetadata / deleteMetadata inside
    createSessionManager() so every in-file mutation auto-invalidates
    the cache. The raw imports are aliased as _rawXxx and only used
    by the wrappers. No call site needs to remember to invalidate.
  - Expose invalidateCache() on the SessionManager interface for
    callers that write metadata outside this module.
  - lifecycle-manager.ts: call sessionManager.invalidateCache() after
    its two direct updateMetadata sites (PR detection, state update)
    so polling-driven mutations are visible on the next poll.
  - Move the _cache / invalidateCache declarations to the top of the
    closure so the mutation wrappers can reference them.
  - Update createMockSessionManager + api-routes.test.ts mocks to
    satisfy the expanded interface.
  - New regression test: external mutation + sm.invalidateCache() →
    next listCached() re-reads disk.

C-02 — inline styles on the terminal-container div

  DirectTerminal.tsx was using style={{ overflow, display,
  flexDirection, flex, minHeight }} — all static values moved to
  Tailwind utilities: "w-full p-1.5 flex flex-col flex-1 min-h-0
  overflow-hidden".

Not addressed in this commit (noted as out-of-scope refactor):

  C-04 400-line cap on DirectTerminal.tsx. Splitting fontSizeControls
  and touch-scroll wiring into sub-components is worthwhile but
  mechanical — separate PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 00:15:57 +05:30
Harshit Singh Bhandari 509fb12b5e
Merge pull request #1300 from harshitsinghbhandari/sessions-redone 2026-04-18 20:01:27 +05:30
Harshit Singh Bhandari 390aff10f0
Merge pull request #151 from harshitsinghbhandari/fix/pr1300-test-failure
test: fix stale codex gh wrapper assertion
2026-04-18 18:08:48 +05:30
harshitsinghbhandari 3449e44d84 test: align codex gh wrapper assertion with dynamic PR parsing
The shared gh wrapper now extracts PR URLs with a regex instead of embedding a literal github URL, so the old codex assertion was stale and broke CI on PR #1300.
2026-04-18 18:02:31 +05:30
Harshit Singh Bhandari 8364fe883b
Merge pull request #150 from harshitsinghbhandari/aa-30-pr1300-followups
fix: close remaining pr1300 follow-ups
2026-04-18 17:45:50 +05:30
harshitsinghbhandari 7bc98aa387 fix: address pr review follow-ups 2026-04-18 17:43:11 +05:30
harshitsinghbhandari 4dac245b81 fix: deep clone runtime handle data 2026-04-18 17:38:55 +05:30
harshitsinghbhandari 51c3fd9b4b fix: close remaining pr1300 follow-ups 2026-04-18 15:18:24 +05:30
Harshit Singh Bhandari 7b543906e6
Merge pull request #148 from harshitsinghbhandari/aa-29-major-followups
test: add report watcher debounce regression coverage
2026-04-18 14:44:41 +05:30
harshitsinghbhandari 12a17c11d2 test: lock report watcher debounce coverage (#1300) 2026-04-18 14:31:42 +05:30
Harshit Singh Bhandari e75208aceb
Merge pull request #147 from harshitsinghbhandari/session/aa-28
fix: improve Claude/Codex plugin cost accounting and restore safety
2026-04-18 14:19:12 +05:30
harshitsinghbhandari bd7239257f fix: register codex web activity plugin 2026-04-18 14:15:29 +05:30
harshitsinghbhandari b0d0994efd fix: improve agent plugin cost accounting and restore safety 2026-04-18 13:56:59 +05:30
Harshit Singh Bhandari 365c149887
Merge pull request #146 from harshitsinghbhandari/aa-27-sessions-redone-fixes
fix: close remaining PR #1300 lifecycle regressions
2026-04-18 13:44:38 +05:30