Commit Graph
20 Commits
Author SHA1 Message Date
8121d0df6e feat(web): fixed session order display setting (#2033)
* feat(web): fixed session order display setting

Add a per-device "Fixed session order" toggle (default off, Settings →
Display → Sessions). When on, the session list freezes: sessions sort
by creation time only (newest first) instead of re-sorting on
activity, workspace groups stop jumping (ordered by the newest session
created inside them), and the pin-in-progress running buckets are
paused. Manual pins still float to the top; search-relevance ordering
is unchanged.

- shared: SessionSummary gains an optional createdAt (toSessionSummary
  copies it). Older hubs simply omit it and the web falls back to
  updatedAt, degrading to the legacy activity order.
- web: sessionOrder lib (stable key + comparator), useFixedSessionOrder
  localStorage hook, useSSE cache sort via ref (no EventSource teardown
  on toggle), SessionList in-group/group/machine sorts plus bucket
  gating, settings switch, en/zh-CN/ru strings.
- tests: sessionOrder unit, useFixedSessionOrder hook, useSSE
  fixed-order cache cases, SessionList grouping cases, settings page
  switch, shared createdAt passthrough.

Co-Authored-By: Claude Code <noreply@anthropic.com>
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): tie-break equal order keys on session id

Review follow-up: sessions created in the same instant compared as equal,
so their relative order came from the incoming array order — which the
hub may derive from activity, making tied sessions visibly swap after
every refetch. Tie-break on session id to make the comparator a total
order. Two tests cover the comparator and the refetch-stability view.

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

* fix(web): tie-break equal group creation instants on the group key

Review follow-up: the directory- and machine-level sorts compared only
the groups' latest creation instants, so groups created in the same
instant followed the incoming array order and could shuffle after every
refetch. Route both fixed-order group sorts through a shared
compareDescWithKeyTiebreak helper that falls back to the group's stable
key (directory groups: machine::path key; machine groups: machineId).
Test covers both incoming orders of two tied groups.

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

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: HAPI <noreply@hapi.run>
2026-10-26 10:27:52 +08:00
SSU-WEI HUANGandGitHub 68422ed6d3 fix(web): use conversation content for session reference eligibility (#1800)
* fix(web): base session reference eligibility on conversation content

* fix(hub): refresh replacement content after clear abort

* fix(hub): refresh content on idempotent clear aborts
2026-09-11 14:22:16 +08:00
SSU-WEI HUANGandGitHub be1ef2a2e4 feat(dsh): integrate DeepSeek Harness through ACP (#1632)
* feat(dsh): add DeepSeek Harness ACP flavor

* fix(dsh): update mobile flavor catalogs

* fix(dsh): keep mobile spawn policy managed

* fix(dsh): keep managed policy and prompt retry

* fix(dsh): suppress unsupported runner policy flags

* fix(dsh): align native managed-policy UX
2026-08-22 12:37:28 +08:00
3da9f7780a feat(web): support project and global pinned sessions (#1115)
* feat(web): support persistent pinned sessions

* fix(web): preserve project hierarchy for pinned sessions

* fix(web): preserve pins during session deduplication

* fix(hub): preserve pins when merging sessions

* fix(hub): migrate session pins from schema v15

* chore: retrigger PR review

* chore: retrigger PR review

* chore: retrigger PR review

* chore: retrigger PR review

* fix: align pinning with schema and sidebar filters

* fix(web): expose pinning in session header menu

* test(hub): expect schema v17 after pin migration

* fix(web): report pin action failures

* chore: retrigger PR review

* chore: retrigger PR review

* chore: retrigger PR review

* chore: retrigger PR review

* fix: address pinning review regressions

* fix(web): scope pin error toast to session rows

* test(hub): expect schema v20 after migration

* fix(hub): preserve latest source pin during merge

* test(hub): expect schema v22 after pin migration

* feat(web): add project and global session pin modes

Support mutually exclusive project vs global pins, surface both in the
session action menu, and render globally pinned sessions in a top-level
sidebar group with project path labels.

* fix(web): polish pinned section icon and divider alignment

* fix(web): tighten pinned section title icon alignment

* fix(web): restore original pinned section pin icon shape

* fix(web): rename pinned section to pinned sessions

* fix(hub): prefer stronger pin mode when merging sessions

Keep global pins over project pins during consolidation so a project-pinned source cannot downgrade an already or concurrently global-pinned target.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): keep inactive project-pinned groups expanded

Pinned sessions should stay easy to find after reload; do not hide them behind the default inactive-group collapse.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): keep project pins in directory groups

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 16:07:21 +01:00
b7f52f58ca feat(media): add audio and file display (#1405)
* feat(cli): cross-flavor inline image display via MCP and ACP

Share display_image prompt across MCP-bridge flavors (Cursor, Gemini,
Kimi, Codex, Claude, OpenCode), auto-approve the tool in
buildHapiMcpBridge, handle ACP image content blocks, and harden
generated-image registration with content sniffing.

Closes tiann/hapi#956

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): render generated-image cards reliably in chat

Keep object URLs stable across refetch, upscale tiny inline images,
fetch generated-image bytes with cache no-store (avoid empty 304 bodies),
and load hapiMcpUrl from per-session API in hapi-display-image tooling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli+web): display_video MCP for inline mp4/webm (#956)

Add display_video alongside display_image, video MIME sniffing with avif
guard, web GeneratedImageCard video player, and hapi-display-image auto-routing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(cli+web): cross-flavor display_video parity with images (#956)

Share display_video prompts across MCP-bridge flavors, auto-approve the
tool, register mp4/webm via path sniffing, render inline video in web on
the existing generated-image RPC path, and restore robust media card fetch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ACP image ordering and inline media source provenance

Flush buffered assistant text before async generated_image emit from ACP
image blocks (PR #958 review Major). Add optional source metadata on
generated-image wire messages (ingress, flavor, toolCallId, toolName) for
MCP, ACP, and Codex tool-result paths. Seeds artifact-event follow-up #966.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli+web): address PR #958 review Majors on media order and stale blobs

Queue ACP session updates and await async image registration before later
events; clear GeneratedImageCard blob state when imageId changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): await ACP queue after late-drain before turn_complete

Straggler session/update during drainLateBuffers can queue async image
registration; re-await sessionUpdateQueue so generated_image is not emitted
after turn_complete.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): route AVIF ftyp brands to display_image in helper

Match server-side detectImageMimeType so .avif files are not sent to
display_video and rejected as unsupported video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(#956): agent inline-media doctor and discovery fixes

- hapi doctor inline-media: probe bridges, print per-session inline commands
- Expose hapiMcpUrl on session list summaries (stops false "no MCP" scans)
- Helper script: match cursorSessionId prefixes; HAPI_SESSION_ID path-only mode
- ACP bridge prompt: shell fallback + HAPI session id vs agent id rule

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): allow immutable cache for generated media blobs

Drop cache: no-store on generated-image fetch so browser can reuse hub
immutable responses; on 304 re-read via force-cache (#927, PR review).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(#956): Cursor native MCP overlay; drop user-turn bridge prepend

Cursor ACP ignores session/new mcpServers. Write .cursor/mcp.json and
run agent mcp enable hapi instead. Remove HAPI_MCP_BRIDGE_PROMPT from
user turns on ACP remotes; enrich MCP tool descriptions for discovery.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): preserve non-HAPI mcp.json keys on Cursor overlay cleanup

Cleanup only removes or restores the hapi MCP entry instead of rewriting
the full pre-session snapshot, so concurrent edits to other servers survive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): handle generated_image in Grok ACP launcher switch

Upstream Grok launcher exhaustiveness broke after AgentMessage gained
generated_image for cross-flavor inline media.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): rebase fallout for display_video + OpenCode skill lookup

Gate display_video in the STDIO bridge, restore OpenCode first-prompt
TITLE_INSTRUCTION (skill_lookup), and update tool-list test expectations.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): leave user-owned hapi MCP entry alone on overlay cleanup

Only undo mcpServers.hapi when it still matches the exact entry this
session installed; concurrent Cursor/user edits of that key survive.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): exact-match auto-approve for display_image and display_video

Move media tools off substring name/id hints onto the exact-name set so
forged lookalike tools are not approved in default permission mode.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): drop dead Cursor bridge prompt; put guidance in MCP descriptions

Cursor must not get a user-turn media prepend (prompt-taint). Remove unused
HAPI_MCP_BRIDGE_PROMPT_CURSOR and embed DISPLAY_*_PROMPT_CURSOR in the
display_image/display_video MCP tool descriptions instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): require user approval for display_image and display_video

Those tools read arbitrary local paths into chat; keep them on MCP
approval_mode prompt and out of default-mode auto-approve exact names.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: re-trigger Codex PR review after provider 503

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ignore URI-only ACP image blocks that read local disk

Passive ACP agentMessageChunk handling must not load file:// or bare
paths; local media goes through prompt-gated display_image/display_video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): allowlist MP4 ftyp brands for video sniffing

Reject HEIC/HEIF and other non-video ISO-BMFF containers instead of
treating every non-AVIF ftyp as video/mp4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): keep Cursor ACP startup if MCP overlay fails

Wrap installCursorMcpOverlay so a malformed project .cursor/mcp.json
cannot abort the session; continue without inline media tools.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail doctor inline-media when checks fail

Exit non-zero whenever required checks fail, even if an active
hapiMcpUrl bridge is present.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): expect display_video when change_title is disabled

Native ACP title mode still exposes display_image and display_video;
update startHappyServer test after rebase onto 0.23.4.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): keep ACP title sync synchronous outside media queue

session_info_update title forwarding (#1028) must not wait on the
async message-handler queue used for inline media ordering.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): sniff media headers only; advertise OpenCode display_video

Read 16 bytes for detectMediaTool instead of the whole file, and include
hapi_display_video in OPENCODE_NATIVE_TOOL_INSTRUCTION for remote ACP.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): show Cursor generate_image inline in HAPI chat

cursor/generate_image only emitted a tool card; register filePath or
base64 imageData into generatedImages and emit generated_image so the
web chat card renders (issue #956 / swear01 report).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): ignore path-only Cursor generate_image reads

Path-only filePath registration bypassed permission-gated display_image /
display_video MCP tools. Keep base64 imageData only; local paths must go
through MCP approval.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): gate inline media base64 length before decode

Reject oversized ACP/Cursor base64 payloads by character count so the
CLI never allocates past the 25 MB generated-image cap.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): require EBML DocType webm for inline video sniff

Bare EBML magic matches Matroska/MKV too; only accept DocType webm.
Also restore annotated Playwright cursor in annotatedVideoUseOption.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): read 128-byte header for WebM DocType sniff

detectMediaTool only loaded 16 bytes, so EBML DocType webm was often
missing and valid WebM files fell through to display_image.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): compare generated-image source by value in reconcile

Wire normalization allocates a fresh source object each pass; reference
equality forced media-card recomputation on every reload/SSE refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): inject Cursor MCP enable for overlay unit tests

installCursorMcpOverlay always spawned `agent mcp enable`; tests now pass
a noop so the suite never shells out to a real Cursor binary.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): shell-quote doctor inline-media helper command

Paths and session prefixes with spaces/metacharacters broke the copied
snippet; JSON.stringify each interpolated argument.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(cli): fix doctor inline-media quote path expectation

Repo root from scriptPath is three levels up (cli/), not the parent of cli.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli/web): per-session Cursor MCP overlay id and bound tiny-image scale

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): redact generate_image base64 from logs and fix doctor MCP ids

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): harden inline-media doctor for packaged installs and hub headers

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): lock Cursor mcp.json updates and bound ACP media filenames

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): preserve mcp.json mode and token-scoped overlay locks

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): publish Cursor MCP lock owners via link(2) and treat EPERM as alive

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail closed on stale MCP locks; keep concurrent mcp.json top-level keys

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): roll back Cursor MCP overlay when agent mcp enable fails

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): await ACP session queue in suppressUpdatesDuring tests

#958 queues handleUpdate for media registration; upstream compact tests
assumed sync delivery after restore.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): capture ACP handler at enqueue; keep display_video manual

Close two Major review findings on #958: suppress queue leak after
restore, and Claude --allowedTools auto-approving local-path video.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: write through symlinked mcp.json; lazy-load inline video

Preserve user Cursor MCP symlinks on atomic overlay writes, and require
explicit Load video before fetching large generated-video blobs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): use valid TerminalToolDisplayMode in media card test

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(scripts): require unique session prefix in display helper

Reject ambiguous prefix matches so images/videos cannot land in the
wrong HAPI chat when multiple agent session ids share a prefix.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): always cleanup Cursor MCP overlay on teardown

Run overlay cleanup in finally so cancelAll/disconnect failures cannot
leave a dead hapi-<sessionId> entry in .cursor/mcp.json.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): handle Copilot generated_image; refuse MCP symlinks

Unblock typecheck after Antigravity/Copilot merge, and fail closed when
.cursor/mcp.json or .cursor is a project-controlled symlink.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: abort restore deliveryMode; prune dead Cursor MCP overlays

Unblock web typecheck after steer merge, and recover orphaned hapi-*
mcp.json entries via HAPI_MCP_OVERLAY_PID ownership stamps.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): recover dead-PID Cursor MCP overlay locks

Token-matched unlock so a crash mid-lock no longer permanently disables
inline media; keep live-owner waits identity-safe.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): serialize Cursor MCP stale-lock recovery

Acquire an exclusive recovery lock before token-matched unlink so two
recoverers cannot remove a successor's live mcp.json lock.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): fail closed on stale Cursor MCP overlay locks

Withdraw racy auto-recovery: pathname check-then-unlink/rename can steal
a successor lock. Stale locks throw with an explicit rm hint instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): drop duplicate deliveryMode on abort restore

Merge left both steer and queue; keep queue so retries after abort
do not re-bind to a later Pi turn.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): bound inline media reads on open fd

Close TOCTOU between pathname size check and readFile for display_image /
display_video and registerGeneratedImageFromPath. Also preserve non-PID
env edits on Cursor MCP overlay cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): repair happyMcpStdioBridge test syntax after merge

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): write Cursor MCP overlay to ~/.cursor, not the project

Keep ephemeral hapi-<sessionId> bridges out of the checked-out tree so
agents cannot git-add a live loopback URL. Tests inject mcpConfigDir.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): point Cursor MCP diagnostics at ~/.cursor/mcp.json

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(media): add audio and file display

---------

Co-authored-by: HeavyGee <133152184+heavygee@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
2026-08-08 13:46:59 +08:00
8e34e7599b perf(hub,web): emit structured patches for session todos/teamState/metadata/agentState writes (closes #895, second half of #884) (#897)
* perf(hub,web): emit structured patches for session todos/teamState/metadata/agentState writes (#895, closes second half of #884)

Today the four CLI handlers in `sessionHandlers.ts` that write session-scoped
state (TodoWrite messages -> setSessionTodos; team-state deltas ->
setSessionTeamState; update-metadata RPC; update-state RPC) emit
`session-updated` events with no `data` payload. `syncEngine.handleRealtimeEvent`
intercepts each one, re-reads the row from SQLite, and broadcasts the entire
~5KB Session via SSE. That works (the web client's `isSessionRecord` shortcut
keeps the cache patched), but it costs a DB read and a full-payload SSE
fan-out per write, and any failure mode that drops the broadcast data falls
through to `useSSE.ts:509-512` and triggers per-session REST refetches - the
storm vector documented in #884.

This is the architectural follow-up to #885. #885 added `staleTime` on the
detail query (eliminates focus / mount refetches inside a 30s window). This
PR removes the structural reason these four writes touch the REST path at all.

`SessionPatchSchema` learns four optional structured fields:
- `todos` (array)
- `teamState` (object)
- `metadata` (versioned `{ version, value }` wrapper)
- `agentState` (versioned `{ version, value }` wrapper)

`.strict()` preserved so unknown keys still throw. The versioned wrappers
mirror the existing socket.io `update-session` broadcast at lines 211 / 259 so
metadata and agentState always travel as an atomic (version, value) pair -
caches need the version to reject stale patches.

Each of the four emit-sites now carries a structured `data` payload with the
delta it just wrote. `syncEngine.handleRealtimeEvent` for `session-updated`
events with non-empty patch data: applies the patch to the in-memory Session
in place via the new `sessionCache.applySessionPatch`, then forwards the event
as-is. Empty patches, no-data events, and patches against uncached sessions
all fall back to the legacy `refreshSession` path so behavior for other
emitters (e.g. `cursor/codexDesktop.ts`) is unchanged. Dedup hook against
agent-session-id changes preserved on the fast path.

`patchSessionDetail` is no longer a blanket spread - it enumerates each field
explicitly so the versioned metadata / agentState patches can be unwrapped
into the Session's flat (metadata, metadataVersion) and (agentState,
agentStateVersion) pairs. Spreading the patch wholesale would have written a
`{ version, value }` object into `session.metadata` and corrupted the cache.

`patchSessionSummary` recomputes the touched derivations - `todoProgress` from
todos, `pendingRequestsCount` / `pendingRequestKinds` from agentState,
SessionSummaryMetadata from metadata - via three new pure helpers exposed
from `shared/src/sessionSummary.ts` (`computeTodoProgress`,
`computePendingRequestKinds`, `toSessionSummaryMetadata`). `toSessionSummary`
is refactored to use these helpers - identical output, single source of
truth.

- shared: `SessionPatchSchema` parses each new patch shape, stays strict,
  rejects empty metadata without `version`, rejects full Session payloads
  (those go through `isSessionRecord`).
- shared: summary derivation helpers covered against bare AgentState /
  Metadata inputs (the shape the SSE patch path provides).
- hub: each emit-site asserted to carry the expected structured payload.
- hub: `applySessionPatch` unit-tests cover todos / metadata / agentState
  application, empty-patch rejection (forces caller back to refreshSession),
  cross-namespace guard, and missing-session fallback.

Empirical wire round-trip verifies each patch shape survives `JSON.stringify`
intact and routes the web client through `getSessionPatch` (non-empty result)
instead of the REST invalidation fallback.

Per #884 expectation: with this fix on top of #885, idle GET /api/sessions/<id>
rate is expected to drop to near-zero on the reporter's 100+ session install.
Operator (heavygee) will attach the live-measured before / after to the PR
post-merge.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): snapshot metadata reference before applySessionPatch mutation so dedup-on-id-change fires

The structured-patch fast path added in 05147a6a (closes #884 second half)
broke the dedup-on-metadata-change trigger in handleRealtimeEvent.

Root cause: applySessionPatch MUTATES the cached Session in place
(reassigns session.metadata = patch.metadata.value). The dedup check
compares before vs after agent session IDs, but `before = getSession(id)`
and `after = getSession(id)` returned the SAME object reference, so
before.metadata had already been overwritten by the time the check ran.
hasSameAgentSessionIds always returned true and dedup silently never
fired on the fast path.

The legacy refreshSession path got dedup for free because it REPLACES
the cache map entry with a new Session object, leaving the pre-refresh
reference intact for the comparator.

Fix: capture beforeMetadata before applySessionPatch runs; use it for
both branches so the comparison contract is identical.

Adds syncEngineHandleRealtimeEvent.test.ts with three regression guards:
- structured metadata patch with changed cursorSessionId fires dedup
- todos-only patch does NOT fire dedup (no false positives)
- legacy refresh path (no patch data) still fires dedup

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(schemas): reorder SessionPatchSchema fields so soup-merge with codex-usage layer conflicts cleanly

Pure reorder (no semantic change). feat/codex-usage-indicator-rebased adds
a flat `metadata: MetadataSchema.nullable().optional()` + `metadataVersion`
to SessionPatchSchema in the same line range upstream/main has the model/
modelReasoningEffort fields. My branch added the versioned `metadata` field
at the END of the object, so git 3-way merge silently auto-merged both,
producing an invalid object literal with duplicate `metadata` keys.

By placing my `metadata` / `agentState` / `todos` / `teamState` insertions
in the SAME line range codex inserts (between updatedAt and model), git
now raises an explicit CONFLICT during the soup merge, which can be
resolved correctly once and replayed by rerere. No behavior change on a
clean upstream/main merge.

Pure cosmetic; no test or runtime impact.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sse): propagate TeamDelete clear through structured patch path

Closes PR #897 Major review (HAPI Bot, 2026-06-13): TeamDelete events
drove `applyTeamStateDelta` to return `null`, but the emit-site
coalesced that to `undefined`. JSON serialization then dropped the key,
the hub cache skipped its assignment branch (`patch.teamState !==
undefined` was false), and the web client saw an empty patch and fell
back to REST invalidation — exactly the storm path this PR was supposed
to close. Sidebar / NotificationHub / dedup all served stale team state
until the next full refresh.

Fix in four coordinated places (wire ↔ cache contract):

- shared/src/schemas.ts: `teamState: TeamStateSchema.nullable().optional()`
  so `null` is a valid wire shape meaning "cleared". Comment documents
  the discriminator contract for consumers.
- hub/src/socket/handlers/cli/sessionHandlers.ts: drop the
  `?? undefined` coalesce so `null` survives JSON serialization.
- hub/src/sync/sessionCache.ts (applySessionPatch): use
  `Object.prototype.hasOwnProperty.call(patch, 'teamState')` to
  discriminate "field absent" from "field is null", then map null →
  undefined to match the cached `Session.teamState` type.
- web/src/hooks/useSSE.ts (patchSessionDetail): same
  hasOwnProperty discriminator + null → undefined mapping.

Regression tests:

- schemas.sessionPatch.test.ts: `{ teamState: null }` parses
  successfully (locks the wire contract).
- sessionCache.applySessionPatch.test.ts: TeamDelete clears cached
  teamState; todos-only patch leaves teamState untouched (guards the
  hasOwnProperty branch against a regression back to `!== undefined`).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sse): version-gate metadata/agentState patches against cache regression

Closes PR #897 follow-up Major review (HAPI Bot, 2026-06-16): the new
structured SSE patch path unwraps versioned metadata/agentState fields
without checking the cached metadataVersion/agentStateVersion. SSE
reconnects + the existing per-query invalidation can leave a detail
cache repopulated by a fresh REST refetch BEFORE a buffered older patch
replays. Without the gate the older patch overwrites the newer cache,
regressing resume / session-id / pending-requests state.

Mirrors the hub-side CLI room handler contract (`incoming.version >
currentVersion`, `web/src/hooks/useSSE.ts`):

- `patchSessionDetail`: gate metadata/agentState assignment behind
  `isNewerVersionedPatch(patch.version, nextSession.<field>Version)`.
  The pre-patch version is captured by `{ ...previous.session }` so
  the comparison is against the cache-at-write-time.
- `patchSessionSummary`: read the detail cache (via queryClient) for
  the canonical metadataVersion / agentStateVersion. Use `>=` (not `>`)
  because the callsite runs `patchSessionDetail` first — when detail
  accepts a newer patch the cache already holds the new version, so
  matching `>=` keeps summary aligned with detail's acceptance; when
  detail rejects, `>=` aligns summary with detail's rejection.
- Exported `isNewerVersionedPatch(patchVersion, currentVersion)` as a
  pure helper so the rule is unit-testable in isolation.
- Test: `useSSE.test.ts` pins the 4 cases (newer ✓ / older ✗ /
  same-version ✗ / first-write currentVersion=0 ✓).

Hub-side `applySessionPatch` does NOT need the same gate: in-process
events from `handleUpdateMetadata` / `handleUpdateState` are emitted
only AFTER the optimistic-concurrency check at the store layer
succeeds, and `syncEngine.handleRealtimeEvent` consumes them
synchronously in order. The vulnerability is the SSE
reconnect/replay window on the web client.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sse): include updatedAt in structured patches + pendingRequests summary

Closes PR #897 post-rebase bot review (HAPI Bot, 2026-06-18):

Major — structured patches dropped session.updatedAt. TodoWrite,
teamState, metadata, and agentState DB writes all touch sessions.updated_at,
but the fast path forwarded only field deltas. Hub/web caches and session
list ordering stayed stale until a full refresh. All four emit-sites in
sessionHandlers now reload the stored row after a successful write and
include updatedAt in the SSE patch payload (applySessionPatch already
applies it via Math.max).

Minor — agentState summary patches updated pendingRequestsCount/kinds but
left pendingRequests stale, so SessionAttentionIndicator tooltips showed
old request tools after an SSE patch. patchSessionSummary now uses
computePendingRequestsCount + computePendingRequests alongside the
existing kinds helper.

Tests: sessionHandlers.test.ts asserts updatedAt on todos/metadata/agentState
patches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web,hub): apply serviceTier in structured session patch path

Closes PR #897 bot Minor (2026-06-18): field-by-field patchSessionDetail
stopped copying serviceTier after the spread refactor, so Codex Fast/
Standard could show stale tier until a full refetch. Mirror nullable
hasOwnProperty handling in patchSessionDetail and hub applySessionPatch.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(hub): allow same-ms updatedAt on structured patch emit asserts

Date.now() resolution makes create+update land on the same millisecond in
unit tests; the store still touches updated_at. Use >= so CI is not flaky.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): refuse versioned summary SSE patches without detail version source

When session detail is not cached, defaulting metadata/agentState versions to
0 let stale buffered patches overwrite a freshly refetched list and suppress
list invalidation. Bail out so the list refetches instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): keep updatedAt monotonic when applying SSE session patches

Stale versioned metadata/agentState replays can still carry an older
updatedAt. Use Math.max on detail and summary paths so rejected replays
cannot rewind list/detail clocks while patched=true suppresses invalidation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retrigger Codex PR review after infra stream failure

Prior pr-review run died on reconnect (stream closed before
response.completed); no code findings. Empty commit to re-fire
pull_request_target.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): compare all summary metadata fields in keep-alive skip

isRenderIrrelevantPatch omitted path/machineId/flavor/worktree, so a
same-ms metadata patch could be dropped while summaryPatched stayed true
and list invalidation never repaired grouping/icon/path.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sse): version-wrap todos/teamState patches for dual-SSE races

Global + session EventSources can deliver out of order. Carry store
todos_updated_at / team_state_updated_at as patch versions, gate web
applies, and tighten keep-alive skip compares (metadata + request tool/kind).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): put SSE version watermarks on SessionSummary

Requiring a detail query to apply versioned list patches forced O(N)
/sessions invalidation on every global SSE write. Gate against summary
watermarks instead; skip no-op detail clones on duplicate deliveries.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(hub): ratchet todosUpdatedAt on rewind rebuild

replaceSessionTodos was stamping the remaining TodoWrite's older
createdAt, so a lagged pre-rewind structured SSE patch could resurrect
deleted todos. Advance the watermark on force-replace instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): apply copilotAgentMode in structured detail SSE patches

Field-by-field detail mapper dropped the new Copilot keep-alive field,
so detailPatched suppressed invalidation and SessionChat kept a stale mode.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
2026-08-04 10:59:35 +08:00
Junmo KimandGitHub e35c06b36a feat(agy): add Antigravity as an interactive PTY agent (#1320) 2026-08-04 10:50:03 +08:00
f10fbc7496 feat(cli): add GitHub Copilot CLI agent support via ACP (#1245)
* feat(cli): add GitHub Copilot CLI agent support via ACP

Wrap `copilot --acp --stdio` for remote sessions and spawn the native TUI locally, with full hub/web integration for spawn, resume, and permissions.

Fixes tiann/hapi#362

Co-Authored-By: HAPI <noreply@hapi.run>
Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(copilot): agent modes, models, slash/file UX, local session sync

Add Interactive/Plan/Autopilot (fleet is slash-only), subscription-aware
model discovery, web StatusBar/permission UX, @ file mentions, and fix
local Safe Yolo plus session-id locator for handoff/resume.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: re-trigger Codex PR review after auth outage

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: retry Codex PR review

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): preserve agent mode on resume and apply via ACP set_mode

Resume was dropping copilotAgentMode so Plan/Autopilot reset to interactive.
Also switch local/remote mode application to --mode / session set_mode instead of slash prompts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): wake remote loop when agent mode changes

Empty isolated queue tick lets setMode apply without inventing a user prompt.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): confirm mode changes before persisting

Await Copilot mode changes and expose discovered models so session state reflects backend acceptance.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): guard mode discovery and slash updates

Keep model probes within runner roots and preserve active sessions when mode switching is unavailable or rejected.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): preserve resume and auto semantics

Deduplicate Copilot resume rows, apply Auto explicitly, and fail closed on denied permissions.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): close permission and model discovery gaps

Keep write-capable commands pending in read-only mode, extend model probe RPCs, and preserve explicit model validation before session creation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): persist runtime model and agent mode

Fallback to ACP model options when direct model switching is unavailable and retain Copilot agent mode across hub restarts.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): normalize composer auto selection

Use the null session sentinel for Copilot Auto so the composer selects and resets default models consistently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(copilot): reject local permission mode changes

* style(copilot): remove trailing blank line

* fix(copilot): secure local config handoffs

* fix(copilot): reject local agent mode slashes

* fix(copilot): reject mode changes during turns

* fix(copilot): consume rejected slash updates

* fix(copilot): preserve thinking across slash handling

* fix(copilot): stabilize async config changes

* fix(copilot): roll back rejected startup model

* fix(copilot): preserve cancellation and file mentions

* fix(copilot): hide local permission controls

* fix(deps): support clean workspace installs

* test(copilot): account for spawn mode argument

* fix(copilot): attribute usage to active model

---------

Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 08:19:12 +08:00
KorenKritaandGitHub 1ca7af44d2 fix(pi): keep archived sessions visible (#1297) 2026-08-02 10:00:15 +08:00
64834467e3 feat(codex): import and resume sessions from runners (#1088)
* fix codex import resume flow

* fix hub restart session active state

* fix codex transcript workspace scoping

* Address Codex import review findings

* Fix Codex import machine selection

* Update Codex sessions error test

* Address Codex import review findings

* Preserve forked Codex session id on sync

* Make Codex duplicate cleanup source-aware

* Handle Codex archive failures

* Limit existing session flag to Codex

* Preserve Codex import machine binding

* fix: rebase runner Codex import onto current main

* fix: preserve runner-scoped Codex import behavior

---------

Co-authored-by: syy <815728149@qq.com>
2026-07-19 14:14:42 +08:00
SSU-WEI HUANGandGitHub b9eed7c071 feat: add Grok Build support (#1030)
* test: define Grok Build integration behavior

* feat: add Grok Build agent integration

* test: cover Grok permissions and resume paths

* docs: add Grok Build setup guide

* fix: scope Grok ACP discovery to session cwd

* fix: align Grok permission UI semantics

* docs: clarify Grok runner setup

* test: require Grok create model and effort options

* feat: add Grok create model and effort pickers

* test: define Grok runtime parity behavior

* feat: add Grok runtime ACP controls and discovery

* fix: tighten Grok runtime controls

* fix: suppress nonfatal Grok title quota errors

* feat: support Grok Auto permission mode

* feat: forward ACP native session titles for Grok

* fix: guard Grok Windows shell arguments
2026-07-13 08:41:30 +08:00
ce67823fc3 feat(web,hub): rich hover tooltips on session-list attention indicators (#941)
* feat(web): rich hover tooltips on session-list attention indicators

The session-row attention dots and the future-scheduled clock icon used
plain `title=""` attributes which gave only a one-word label ("Permission
required"). Replace those with hover/focus-revealed tooltips that name
*which* tools are blocking, count background tasks, surface the
"updated Nm ago" timestamp, and explain the pending schedule.

To make per-tool copy possible without an extra round trip,
`SessionSummary` now carries a structured slice of the pending tool
requests, capped at `PENDING_REQUEST_SUMMARY_CAP = 5` oldest-first:

  pendingRequests: Array<{ id; kind; tool; since }>

`pendingRequestsCount` remains the authoritative total;
`pendingRequestKinds` is still derived from the FULL request set so a
single `'input'` request beyond the cap still surfaces its kind on the
session row.

The tooltip primitive (`HoverTooltip`) is a CSS-driven reveal — no
portal, no positioning JS — so it composes cheaply inside the existing
session-row `<button>` and stays out of the way on touch devices, which
keep getting the same `aria-label` the old `title=""` attribute provided
to screen readers.

Test coverage: shared derivation + cap + tie-break + full-set kind
behaviour; web tooltip render across all four attention kinds plus
mixed-kind overflow suppression and aria-label exposure.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web): opaque tooltip surface; drop redundant 'updated Nm ago' body

Two operator-feedback fixes on the new session-list HoverTooltip:

1. Tooltip background was bg-[var(--app-bg)] - the same variable as the
   session row underneath - so the tooltip looked translucent and the row
   text bled through. Switch to bg-[var(--app-secondary-bg)] (#2C2C2E
   dark / #f3f4f6 light, both opaque) and bump shadow-md -> shadow-lg.
   Telegram-themed clients still pick up tg-theme-secondary-bg-color so
   the tooltip stays on-theme.

2. The 'unread' attention dot tooltip rendered 'New activity / Updated 5m
   ago', but the relative-time pill ('5m ago') is already on the right
   edge of the same session row. The tooltip body just duplicated info.
   Render only the title for the unread case; drop the
   session.tooltip.unread.body i18n key from en + zh-CN.

The other tooltip kinds (permission/input list tools, background lists
task count) keep their bodies - those facts are not visible elsewhere on
the row.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(web,hub): show scheduled fire time in session-list clock tooltip

The schedule clock tooltip previously said only "Will fire when due."
while the row already showed a relative updated-at pill. Extend the
session-list API with nextScheduledAt (MIN future scheduled_at per
session, same filter as futureScheduledMessageCount) and render:

- single scheduled: "Fires in 5m · Jun 16, 1:45 PM"
- multiple: "Next in 5m · Jun 16, 1:45 PM · +2 more"

Extract formatScheduledTime from QueuedMessagesBar into web/lib/
scheduledTime.ts alongside formatFutureRelativeTime and the tooltip
composer. SSE upsert preserves nextScheduledAt until the list refetch
that already runs on schedule-related events.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): wire session-row keyboard focus to HoverTooltip a11y

Address PR #941 Major review: aria-describedby and tooltip visibility
were on a non-focusable inner span, so keyboard users tabbing the session
row button never received the rich tooltip description and
group-focus-within never matched.

- Session row button owns aria-describedby (attention + schedule ids)
- Add group/session-row + SESSION_ROW_TOOLTIP_FOCUS_CLASS reveal on
  :focus-visible
- HoverTooltip takes required id; drop inner aria-label/describedby
- useSessionRowTooltipIds helper composes stable row tooltip ids
- Tests for id wiring and parent-focus reveal classes

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 10:15:22 +08:00
8094b500f3 fix(web): hide sidebar fake sessions for Cursor resume/archive (#836)
* fix(web): dedupe sidebar sessions by flavor resume id

Wire deduplicateSessionsByAgentId into SessionList and resolve cursor
threads via cursorSessionId so resume/archive no longer shows duplicate
inactive rows for the same ACP session.

Fixes #833

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): use SessionSummary.agentSessionId for sidebar dedup

SessionList only receives SessionSummary from the API; native ids like
cursorSessionId are already mapped into metadata.agentSessionId by
toSessionSummary. Drop resolveAgentSessionIdFromMetadata to fix typecheck.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): hide inactive empty session stubs in sidebar

Filter inactive rows with no agentSessionId and no title signal before
grouping sessions, and expose lifecycleState on SessionSummary for future
sidebar rules. Completes the #833 P0 follow-up alongside agent-id dedup.

Fixes #833

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): scope sidebar dedup key by flavor

Prevent cross-flavor collisions when flattened agentSessionId retains a
stale native id. Add regression test and relax claudeRemote CI timeout.

Fixes #833

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 13:31:09 +08:00
HeavyGeeandGitHub ec3722aba9 feat(web): session list status indicators (attention + scheduled) (#699) 2026-05-29 15:43:21 +08:00
763f45acdd feat: add support for Kimi Code CLI and fixed some bugs (#659)
* Add Kimi agent support via ACP protocol

Add full integration for the Kimi Code CLI agent using the standard
Agent Client Protocol (ACP). Includes:

- kimi command and CLI registry wiring
- Local launcher spawning kimi directly
- Remote launcher with ACP stdio transport via AcpSdkBackend
- Session management with resume support
- Permission handler supporting all Kimi permission modes
- Terminal UI display component
- Runtime config resolving model from env and ~/.kimi/config.toml

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* Fix Kimi ACP tool call input decoding on web

Kimi streams tool arguments as JSON text inside the content array
(e.g. {\"command\": \"df -h\"}) instead of rawInput/kind. The handler
now extracts input from three sources in priority order:

1. rawInput (Claude/Codex path)
2. kind + title fallback (Gemini path)
3. content JSON text (Kimi path)

Also handles:
- rawInput: null no longer blocks the kind+title fallback
- Title prefixes like \"Shell: free -h\" are stripped to extract args
- Stale placeholder inputs are re-derived when the title updates
- Normalized kind aliases (shell, run, read_file, write, etc.)

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* Add kimi support to web UI

* Fix some bugs

* fix(kimi): dedupe repeated tool_call display in terminal UI

* fix(web): keep tool block immutable so React detects input/state changes

* fix(web): recognise Kimi subagent titles like 'Agent: ...' as subagent tools

* fix(web): allow-for-session for ACP agents (kimi, cursor)

PermissionFooter treated all non-codex sessions as Claude, sending
Claude-specific acceptEdits/allowTools to ACP agents that don't
support them. Hub rejected acceptEdits for kimi, and the ACP
PermissionAdapter ignored allowTools.

- Only show 'allow all edits' for Claude sessions
- Send decision: approved_for_session for non-Claude ACP agents
- Update status display to check decision field

* fix(web): lookup subagent sidechains by tool-call id instead of msg id

* fix(web): don't trim newest messages when loading older history

fetchOlderMessages was using trimVisible(merged, 'prepend') which kept
the oldest 400 messages and dropped the newest ones. This caused:
1. Latest messages to disappear when user loaded older history
2. User to see no visible change when new old messages were drowned
   in the 400-message window.

Remove the incorrect trim so all fetched older messages are retained
alongside the current window. Subsequent ingestIncomingMessages
(append mode) will naturally keep the window bounded when new agent
messages arrive.

* fix(cli): route Kimi session resume to runKimi instead of runCursor

Kimi was present in AGENT_FLAVORS but dispatchLocalResume had no branch
for it, so resuming a Kimi session fell through to the Cursor launcher.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(cli): pass selected model to Kimi ACP backend via KIMI_MODEL env

createKimiBackend was ignoring opts.model and only setting KIMI_PROJECT_DIR.
Use buildKimiEnv so the selected model reaches the subprocess as KIMI_MODEL.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): bound message window on older loads with dedicated larger cap

fetchOlderMessages was keeping all messages unbounded, causing
sessionStorage bloat on repeated pagination. Reintroduce trimming
with OLDER_LOAD_WINDOW_SIZE (800) so growth is capped while the
newest messages are still preserved for far longer than before.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(web): revert sidechain lookup to message id, matching tracer/grouping pipeline

tracer.ts sets sidechainId to the parent message id, and reducer.ts groups
by sidechainId. A prior commit changed reducerTimeline.ts to look up by
tool-call id (c.id), which broke sidechain attachment. Revert to msg.id
so the lookup matches the actual grouping key end-to-end.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(cli): gate ACP title prefix stripping to known tool-kind labels

extractTitleArgument stripped at the first colon unconditionally,
corrupting commands/paths like curl http://localhost:3000 or
Windows paths. Now it only strips when the prefix normalizes to
the same tool kind as the event, verified via regex.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix(shared): include kimi in isCodexFamilyFlavor for ACP permission UI

Kimi is an ACP-style agent that supports the abort decision, but
isCodexFamilyFlavor excluded it, so PermissionFooter rendered the
non-Codex Allow/Deny UI without the Abort button.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: HAPI <noreply@hapi.run>
2026-05-22 10:08:14 +08:00
Haoqing WangandGitHub 7c6a7fa8ef fix(hub,web): deduplicate sessions by agent session ID (#448)
* fix(hub,web): deduplicate sessions by agent session ID

When multiple CLI wrappers independently resume the same Codex thread,
each generates a random tag, causing the hub to create duplicate session
records for a single underlying thread. This leads to duplicate
conversations in the web UI and messages routing to the wrong session.

Add two-layer deduplication:
- Hub: when a metadata update sets an agent session ID (codexSessionId,
  claudeSessionId, etc.) that already exists on another session in the
  same namespace, automatically merge the duplicate into the current
  session using the existing mergeSessions logic.
- Web: deduplicate the session list display by agentSessionId as a
  safety net, keeping the active/most-recent session visible.

Closes #446

* chore: add review-driven comments for dedup clarity

- Explain single-threaded assumption in before/after metadata comparison
- Document merge direction rationale (duplicate → active session)
- Document deduplicateInProgress guard as known limitation
- Add catch comment explaining web safety net fallback

* fix: address review feedback from bot, Opus, and Codex

- Skip active duplicates during hub-side dedup to avoid deleting
  sessions with live CLI sockets and pending agent state
- Pass selectedSessionId into web dedup sort to prevent hiding
  the session the user is currently viewing
- Add test for active-duplicate-not-merged case

* fix: retry dedup on session-end and preserve agentState in merge

- Trigger dedup when a session ends (handleSessionEnd), so active
  duplicates skipped during earlier dedup get merged once they disconnect
- Preserve agentState from old session during mergeSessions when the
  new session has no agentState (mirrors existing model/effort/todos
  preservation pattern)
- Extract triggerDedupIfNeeded helper for reuse across trigger points

* fix(web): prefer active session over selected in dedup sort

Active session always wins the dedup tie-break so the live connection
is never hidden in favor of a selected inactive duplicate. Among
inactive duplicates the selected one is still preferred.

* fix: dedup on inactivity timeout and deep-merge agentState

- expireInactive now returns expired session IDs so SyncEngine can
  trigger dedup for sessions that timed out (crash/network drop)
  instead of only on explicit session-end
- mergeSessions now deep-merges agentState requests/completedRequests
  from both sessions instead of only copying when new is null

* fix: exclude completed requests from merged pending set

Filter out request IDs that already appear in completedRequests when
merging agentState, preventing completed permission prompts from
resurrecting as pending after session dedup.

* fix: guard resume merge against prior auto-dedup

The automatic dedup (triggered when the spawned CLI sets its agent
session ID) can delete the old session before resumeSession reaches
its own explicit mergeSessions call. Skip the merge if the old session
no longer exists instead of failing the resume with a false error.

* test: add coverage for dedup retry paths and web dedup sort

Hub tests:
- session-end triggers dedup retry for previously-active duplicates
- inactivity timeout expiry triggers dedup retry
- agentState deep merge filters completed requests from pending set

Web tests:
- basic dedup by agentSessionId
- active session wins over inactive duplicate
- selected session preferred among inactive duplicates
- active always wins over selected inactive
- sessions without agentSessionId pass through
- independent dedup across different agentSessionIds

* fix: read latest agentState before merge write to avoid overwriting live updates

Re-read the target session's agentState right before writing the merged
result, with a version-mismatch retry loop, so concurrent update-state
events from the active CLI are not lost during dedup merge.

* fix: sort expired sessions by recency before dedup

When multiple duplicates for the same agent thread expire in a single
sweep, process the most recent one first so it becomes the merge target
and survives, rather than keeping the oldest by arbitrary iteration order.

* fix: select most recent session as merge target in dedup

deduplicateByAgentSessionId now collects all inactive candidates
(including the caller) and picks the one with the highest activeAt
(then updatedAt) as the merge target. This ensures the newest session
survives regardless of which trigger point or ordering calls the dedup.
2026-04-13 19:56:22 +08:00
a200fe9628 feat(claude): add effort setting parity with model across stack (#353)
Co-authored-by: Xiaoyi <xiaoyizhang@microsoft.com>
2026-03-24 21:15:48 +08:00
weishu 329d28a93c remove , using instead 2026-03-16 18:29:09 +08:00
weishu 02c8e12e80 unify model selection 2026-03-16 18:29:09 +08:00
weishu 21a64dfb49 refactor: extract session summary utilities to shared module 2026-01-05 13:00:48 +08:00