Commit Graph
483 Commits
Author SHA1 Message Date
e2631a553a feat(cli): ping-peer CLI + MCP ping_peer for peer messaging (#1195)
* feat(cli): add ping-peer CLI and MCP ping_peer for peer messaging

Promote resume-if-inactive + wait-active + POST message into a first-class
CLI command and session MCP tool so agents stop reinventing JWT+curl.

Fixes #1194

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

* fix(cli): do not auto-approve MCP ping_peer

Cross-session messaging can resume a peer and inject a prompt, so keep
permission-mode gating (Codex PR review Major on #1195).

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

* fix(cli): require approval for ping_peer in read-only mode

Read-only auto-approve treated non-write names as safe; ping_peer can still
resume a peer and inject prompts, so gate it like a write tool.

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

* fix(cli): keep ping_peer out of Claude --allowedTools

toolNames still registers the MCP tool, but Claude auto-allow must not
pre-approve cross-session resume+inject without a permission prompt.

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

* fix(cli): re-check session active before ping-peer send

List/get can race; POST /messages still 409s if the target flips inactive
before send. Resume+wait again (and re-gate pi) immediately before POST.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 08:36:56 +08:00
weishuandGitHub 909aaca09d fix(cli): gate main package release on platform packages being live (#1187)
- prepare-npm-packages: exit(1) instead of warn-and-continue when a
  platform binary is missing, so a broken build aborts the release
- release-all: after publishing platform packages, poll npm view for
  every @twsxtd/hapi-<platform> package until it matches the release
  version (10min timeout, 15s interval) before publishing the main
  package

Fixes #1149
2026-07-27 12:59:02 +08:00
e4cc3916c8 fix(cursor): CreatePlan Yes accepts and continues task (nested outcome + plan→execute) (#1097)
* fix(cursor): nest ACP extension outcome so plan approvals aren't cancelled

Cursor's ACP blocking extension methods (cursor/ask_question,
cursor/create_plan) expect the JSON-RPC result to nest the outcome under
an `outcome` key, e.g. `{ outcome: { outcome: "accepted" } }`. The
adapter returned it flat (`{ outcome: "accepted" }`), so Cursor read
`response.outcome.outcome` as undefined and fell back to a cancellation
— an approved plan was relayed to the agent as `User cancelled`, making
plan mode unusable over HAPI for Cursor sessions.

Wrap every ask_question / create_plan response in the nested envelope
and add regression tests asserting the exact wire shape for the
affirmative approval -> CreatePlan path (plus approved_for_session,
reject, and abort).

Fixes #79

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

* fix(cursor): continue task after CreatePlan Yes (plan→execute)

Nested ACP accept alone is not a complete fix: Yes unblocked create_plan
but the prompt turn still ended with the plan "done" and no execution.
Mirror Claude ExitPlanMode: on accept, leave plan/ask for an executable
mode and queue a continue prompt so the original user task keeps going.

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

* fix(cursor): sync runCursor enqueue mode after CreatePlan accept

Codex Major on #1097: setPermissionMode alone left runCursor's
currentPermissionMode stale, so the next user message could re-enter
plan/ask after Yes. Notify onPermissionModeChanged from CursorSession
so the enqueue source of truth stays aligned with the session.

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

---------

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 12:57:31 +08:00
weishu 8297383dc0 Release version 0.24.0 2026-07-27 10:22:58 +08:00
weishu 042ffced8d fix(kimi): sync native local session titles 2026-07-27 10:19:25 +08:00
SSU-WEI HUANGandGitHub 500407c6b1 fix(codex): show catalog-default Fast tier (#1179)
* fix(codex): show catalog-default Fast tier

* fix(web): show inherited Fast tier in header
2026-07-27 07:38:24 +08:00
Junmo KimandGitHub deb5d63695 fix(cli,web): group orphaned subagent trace by parentToolUseId (#1175) 2026-07-26 23:01:46 +08:00
SSU-WEI HUANGandGitHub 73bc45c1ae fix(codex): clear completed safety review prompt (#1156) 2026-07-26 15:07:24 +08:00
King StarandGitHub dbb063c112 fix(cli): preserve active Claude model on local handoff (#1168) 2026-07-26 15:04:12 +08:00
SSU-WEI HUANGandGitHub bd5e87898a feat(codex): support proactive /agent mode (#1172) 2026-07-26 15:01:53 +08:00
Haoqing WangandGitHub 6bddc9d044 fix(cli): stop internal agent events from leaking into chat as raw JSON (#1165)
* fix(cli): drop unknown SDK message types instead of passing them through

The SDK-to-log converter's switch had a fail-open default that stamped any
unrecognized SDK message with transcript base fields (parentUuid/sessionId/
userType) and forwarded it. Claude Code emits a tool_progress heartbeat every
30s for long-running tools, so a single slow Bash call flooded the chat: the
web normalizer matches no known shape for those records and falls back to
rendering the raw envelope as message text.

Gate the switch on an explicit allowlist instead, bailing before the uuid is
allocated so a dropped event cannot advance sidechain/parent tracking -- the
heartbeats share one parent_tool_use_id and were overwriting the pointer a
subagent's next real message parents to. This matches the local launcher,
which already enforces the same allowlist via RawJSONLinesSchema.safeParse.

The default branch stays as a fail-closed guard so adding a type to the
allowlist without a matching case drops the message rather than leaking it.

* fix(cli): re-check reassembled text for internal event JSON at flush boundary

isInternalEventJson was only applied per incoming chunk. In delta mode
(OpenCode) every chunk is a fragment, so none of them parses as JSON on its
own and the filter never fires; the pieces accumulate and flushText emits the
reassembled envelope verbatim. The dedupe path has the same hole whenever two
chunks share no overlap.

Check again in flushText, which is the first point the complete text exists,
and tolerate surrounding whitespace so an envelope preceded by a newline is
not waved through by the leading-'{' fast path.

Genuine assistant output that happens to be JSON is unaffected: the matcher
still requires the specific { type: 'output', data: { parentUuid, sessionId,
userType } } envelope shape.

* fix(cli): fail closed on unrecognized agent message in converter

convertAgentMessage's exhaustiveness default returned the message object
itself at runtime. The never binding makes the branch unreachable today, but
every caller forwards a non-null result straight into the chat stream, so the
failure mode if it were ever reached is a raw object on screen. Keep the
compile-time check, return null at runtime.

* test(cli): cover command_lifecycle, a second unknown type seen leaking

Observed in the same session after tool_progress. The allowlist already
covered it with no code change, which is the argument for gating on known
types rather than adding a case per offender.
2026-07-26 02:46:29 +08:00
weishu 8eac26726b Release version 0.23.4 2026-07-24 11:06:10 +08:00
NightWatcher314andGitHub 5300e1ee39 feat(codex): support file mentions (#774)
* feat(codex): support file mentions

* fix(codex): quote file mention paths with spaces

* fix(codex): keep punctuation outside file mentions

* fix(codex): avoid parsing literal at-mentions

* fix(codex): scope file mention autocomplete
2026-07-24 11:00:48 +08:00
SSU-WEI HUANGandGitHub 40c1789357 fix(acp): sync native agent session titles (#1028)
* fix(acp): sync native session titles

* fix(acp): keep title refresh off turn path

* fix: reconcile native ACP titles with skill lookup

* fix: preserve OpenCode image tool instruction
2026-07-24 10:59:47 +08:00
AnanovoandGitHub df36cec01e feat(web): sort file search results (#1109) 2026-07-24 10:58:07 +08:00
af8d160364 feat(cli): export HAPI_SESSION_ID into wrapped agent env (self-targeting) (#1121)
* feat(cli): export HAPI_SESSION_ID into wrapped agent env

Publish the hub session id into process.env at session bootstrap so every
downstream agent spawn inherits it. HAPI runs one hub session per CLI process
(the runner forks a fresh hapi child per session; local is 1:1) and every
flavor's agent spawn derives its child env from process.env, so a single seam
covers claude / codex / cursor / gemini / opencode / kimi / grok / pi -
runner-spawned and local - plus future flavors, without touching each launcher.

Agents can read HAPI_SESSION_ID to self-target their own hub session over REST
or shell helpers without listing /api/sessions. Prefer the MCP display_image
tool for inline media when available; HAPI_SESSION_ID is the deterministic
fallback for non-MCP tooling.

Closes #1119

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

* feat(scripts): self-target hapi-display-image via HAPI_SESSION_ID

Teach the in-tree shell helper to use $HAPI_SESSION_ID for path-only /
self invocations: GET /api/sessions/:id directly instead of listing
/api/sessions. Explicit session prefixes keep the previous list path.

Gives #1119 a tangible now benefit - the tool that forced the wasteful
list-and-reverse-lookup dance no longer needs it inside a wrapped session.

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

* fix(cli): defer HAPI_SESSION_ID export until lazy Codex materializes

The provisional lazy-session id was exported at bootstrap before the hub
row existed, so path-only self-targeting (GET /api/sessions/:id) could
404 while materialization was still pending. Export on onMaterialized
instead, and await materialize in buildHapiMcpBridge before starting the
MCP server / spawning Codex so the agent inherits an id the hub can
resolve (and so hapiMcpUrl is persisted, not only local pending state).

Addresses Codex review Major on #1121.

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

---------

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 10:56:59 +08:00
KorenKritaandGitHub 735ccda168 fix(pi): report authoritative context usage (#1106) 2026-07-24 10:56:03 +08:00
40314237ae fix(cli,hub): wire Cursor --existing-session-id for ACP remote resume (#991) (#1128)
Hub already passes access.sessionId on resume (#1088); Cursor CLI still ignored
it (Codex-only). Parse/pass the flag for cursor and lock in reuse-without-ready-wait tests.

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 10:52:55 +08:00
Fuyan YuanandGitHub fee853766a fix(codex): normalize resume args on local handoff (#1137) 2026-07-24 10:52:40 +08:00
SSU-WEI HUANGandGitHub aa5beb3af2 feat(codex): preserve native exploration actions (#1139) 2026-07-24 10:52:24 +08:00
7ca4e71fa0 fix(cli): buffer Pi prompts until RPC startup ready (#1146)
* fix(cli): buffer Pi prompts until RPC startup ready

A prompt POSTed immediately after spawn (a supported handoff pattern used
by hapi-ping-peer and intake scripts) could reach `pi --mode rpc` before
its `new_session`/`get_state` startup finished, wedging the turn:
`agent_start` then silence, no tool calls. The socket goes `active` (spawn
success) well before Pi's session is initialized, so `active` is not a
safe ready signal for Pi.

Gate outbound prompt/steer sends behind a startup ready gate on PiSession:
`runWhenReady()` delivers immediately once ready, else buffers FIFO;
`markReady()` fires on the first `get_state` response (the signal that
persists `metadata.piSessionId`, which working callers already wait for)
and drains the buffer in order. A 30s unref'd fallback timer force-drains
if `get_state` never lands, degrading to prior send-anyway behaviour
rather than swallowing the message forever.

Fixes #1143

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

* fix(cli): honor cancel-queued-message for buffered Pi prompts

Addresses the MAJOR review finding on the startup ready-buffer: while a
prompt is held behind runWhenReady, the hub can send cancel-queued-message
for its localId. Pi registered no onCancelQueuedMessage handler, so
ApiSessionClient acked removed:false, the hub marked the row invoked, yet
the buffered closure still drained on get_state and fired the cancelled
prompt.

Carry the localId with each buffered send and add
PiSession.cancelBufferedMessage, then register apiSession.onCancelQueuedMessage
so a cancel drops the still-buffered prompt (returns true) instead of
sending it. Once drained to Pi it cannot be recalled — returns false,
matching the other agents' queue.cancelByLocalId best-effort semantics.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 10:52:06 +08:00
weishu 74ad25ec57 feat(codex): refine tool activity display 2026-07-24 09:39:22 +08:00
SSU-WEI HUANGandGitHub 173f855b73 docs: remove sunset Gemini CLI launch references (#1132) 2026-07-23 08:42:11 +08:00
SSU-WEI HUANGandGitHub 6bedd0d924 feat(tooling): preserve native tool titles (#1133) 2026-07-23 08:41:33 +08:00
weishu 782b523fb0 Release version 0.23.3 2026-07-22 09:24:05 +08:00
weishu db1444fe2e Release version 0.23.2 2026-07-22 09:22:56 +08:00
weishu 3208f139b5 fix(codex): ignore subagents in transcript fallback 2026-07-22 09:20:00 +08:00
weishu 3dd425b15c fix(codex): restore transcript tool calls 2026-07-22 09:02:06 +08:00
af962fc61f fix(cli): stop prepending skill_lookup $name instruction onto user turns (#1096)
Cursor ACP (and other remotes) flagged the glued-on SKILL_LOOKUP_INSTRUCTION
as prompt injection. Keep discovery on the skill_lookup MCP tool description
and on system prompts (OpenCode/Grok); do not taint user messages.

Fixes #1095

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 18:29:29 +01:00
weishu b74a11ecc3 Release version 0.23.1 2026-07-19 14:21:14 +08:00
Junmo KimandGitHub 223be6d60f fix(cli): don't lose the queued message when a remote launch fails (#1058)
* fix(cli): cap and back off consecutive remote launch failures

claudeRemoteLauncher's respawn loop retried claudeRemote() immediately
on every throw with no backoff or limit. A deterministic launch
failure (bad auth, invalid model/args, spawn failure) respawned in a
tight loop instead of giving up, hammering the same failure forever.

Track whether onReady() fired at least once per attempt to tell an
immediate/deterministic failure apart from a failure after real
progress, back off between immediate-failure retries, and after 3
consecutive immediate failures drop the message that keeps triggering
them and reset the streak, instead of respawning forever. The session
keeps running so a later, unrelated message still gets its own budget.
The streak reset on a non-throwing attempt is itself gated on having
reached onReady, not applied unconditionally -- otherwise a message
that keeps getting parked and re-picked-up on alternating attempts
(e.g. an isolated command hitting the same deterministic failure)
would reset the streak every other attempt and the cap would never
fire.

* fix(cli): restore queued message when remote launch fails before delivery

MessageQueue2.collectBatch() acks a message (fires onBatchConsumed,
which the hub uses to mark it consumed) at dequeue time, before the
message ever reaches the SDK. If claudeRemote() then throws before
onReady -- e.g. the process dies right after picking up the message --
the catch block only logged and retried, so the message vanished: the
hub already thinks it was delivered, but the CLI never acted on it.

Track the message returned from nextMessage() (whether freshly
dequeued or held in `pending` across a mode change) as in-flight until
the next onReady confirms it was handled, and restore it to the front
of the queue (preserving isolation via unshiftIsolated when needed) if
the attempt throws and will be retried. Restoring happens even if the
throw races with a user-initiated switch/exit, so a message is not
silently dropped by that unrelated shutdown either.

When the immediate-failure cap from the previous commit is reached,
the in-flight message is dropped instead of restored: unshifting it
back would just feed it into another immediate failure on the very
next attempt, storming again. This mirrors
cursorLegacyRemoteLauncher's existing drop-and-reset policy on its own
consecutive-failure cap.

* fix(cli): preserve localId when restoring a failed message batch

MessageQueue2.collectBatch() already collects each queue item's
localId (it fires onBatchConsumed with the full list to ack them), but
only exposed the joined `message` string to callers, discarding the
per-item localIds and their original boundaries in the process.

When claudeRemoteLauncher restores a dequeued-but-undelivered batch
after a launch failure, it re-added the joined string as a single new
queue item with no localId, orphaning the retried prompt from the hub
row(s) it originated from (and from cancel-by-localId).

Expose the pre-join `items` breakdown (message + localId per item)
alongside the existing joined `message` field on
collectBatch()/waitForMessagesAndGetAsString() -- purely additive, so
the other callers of waitForMessagesAndGetAsString() (grok, kimi,
opencode, cursor, codex, runAgentSession) are unaffected. On restore,
unshift each original item individually in reverse order, so the
localId and relative order of a multi-message batch are both
preserved instead of just the first item's.

* fix(cli): reset immediate-failure streak on a delivered non-onReady success

claudeRemote.ts's /clear handling delivers the queued message to the
SDK, then calls onSessionReset()/onCompletionEvent() and returns
successfully without ever calling onReady(). The success-path streak
reset only cleared on reachedReadyThisAttempt, so a successful /clear
between two unrelated immediate launch failures did not reset the
streak: an unrelated message's very next failure could hit the
3-in-a-row cap after just 1 failure, and the resulting banner would
misreport "3 times in a row".

Track whether nextMessage() actually handed a message to the SDK this
attempt (deliveredMessageThisAttempt), separately from whether the
attempt reached onReady, and reset the streak on either signal. The
livelock-prone case this guards against (a message parked into
`pending` and the attempt returning without ever delivering anything)
leaves both flags false, so it still does not reset the streak.
2026-07-19 14:16:40 +08:00
bfd8c7e3fd fix(codex): use supported safe-yolo approval policy (#1079)
Co-authored-by: NPUlrk <21106497+NPUlrk@users.noreply.github.com>
2026-07-19 14:16:03 +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
Junmo KimandGitHub 289c9f2218 feat(cli,web): show Claude Code's away recap in local-mode chat (#1089)
* feat(shared,cli): whitelist away_summary so auto recap reaches the hub

Claude Code's local TUI writes an automatic away-summary recap to the
session transcript on window blur/focus (5min+ idle), but
VISIBLE_CLAUDE_SYSTEM_SUBTYPES dropped it before it ever reached the
hub. Add it to the whitelist so the local launcher forwards it like
the other system subtypes, and cover the forwarding + Zod passthrough
of the recap `content` field with tests.

* feat(web): render Claude Code's automatic away recap in the chat

Once away_summary reaches the hub (previous commit), the web chat
still dropped it silently: normalizeAgent had no branch for the
subtype, so it fell through to `return null`. Add a `recap` AgentEvent,
a normalizeAgent branch mirroring the existing turn_duration/compact
subtype branches, and a presentation entry that prefixes the text with
`recap:` so it reads distinctly from the manual /recap assistant
bubble (which already renders as a normal message). No new render
component needed: it flows through the existing generic system-event
row (SystemMessage.tsx + getEventPresentation) that every other system
subtype already uses.

* fix(web): drop inaccurate manual-/recap comparison from recap comments
2026-07-19 12:24:32 +08:00
AnanovoandGitHub f1b5ed5e5d fix(claude): preserve native titles and add a remote fallback (#1080)
* fix(claude): preserve native titles and add remote fallback

* fix(claude): write fallback titles as metadata only
2026-07-19 12:23:39 +08:00
Junmo KimandGitHub 95eee432d4 perf(claude): scan transcripts incrementally (#1081)
* perf(claude): scan transcripts incrementally

The claude session scanner re-read the entire transcript JSONL on every
scan, so the cost of each poll grew with the length of the conversation.
Track a byte offset per file instead and parse only the bytes appended
since the previous scan.

A trailing partial line — a write still in progress — is held back until
its newline arrives. A file that shrank resets the cursor to 0; the base
scanner's uuid dedup absorbs the re-sent events. A read that fails
returns no events and leaves the cursor where it was, so a transient
error is retried on the next scan rather than skipping content.

The codex scanner received this in #1031; this extends the same
improvement to the claude scanner. readSessionLog is exported for tests,
mirroring readTranscriptRange there.

* fix(claude): forward a complete final record with no trailing newline

The incremental reader consumed only through the last newline, so a final
JSONL record flushed without its terminating newline — at shutdown or on
import — was held back as if it were a partial write and never forwarded
until a later append supplied the newline. The previous whole-file reader
parsed such a record.

Consume a trailing segment when it already parses as a complete JSON value,
and keep holding back a genuinely partial line (which parses as incomplete).
2026-07-19 12:23:09 +08:00
weishu 2211888f04 Release version 0.23.0 2026-07-18 12:29:28 +08:00
DullJZandGitHub e737d67aaa fix(opencode): treat empty tool input as missing and recover late tool-calls (#1052)
* fix(opencode): stop treating empty tool input as final args

OpenCode emits input/rawInput as {} on tool start (and sometimes again
during permission), then fills real arguments on running/completed.
Treat empty objects as unusable so ACP and local hooks keep waiting for
real args, never clobber them, and ignore non-tool parts as fake results.

* fix(web): add exec timing fields to ToolCard test fixture

ChatToolCall now requires execStartedAt/execCompletedAt; update the
fixture so typecheck passes.

* fix(opencode): recover late tool-call after empty execute.before

Skip empty before under name-only queue pairing, emit tool-call on after when
still missing, and reject content JSON {} on ACP initial tool_call.
2026-07-18 12:26:23 +08:00
Junmo KimandGitHub 2c559373c9 fix(claude): preserve resume anchor and report real /compact outcome (#1056)
* fix(claude): consume the one-time --resume flag only once it is used or discarded

Reopening a remote Claude session and hitting a relaunch trigger (e.g.
/compact, or a mode/model/effort change) before the first turn is ever
processed loses the original --resume session id and starts a brand new
Claude session with no prior context. With /compact this also shows up as
the compaction ending immediately with "Not enough messages to compact.",
because the session it lands in is empty.

claudeRemoteLauncher called session.consumeOneTimeFlags() right after every
claudeRemote() call, including calls that returned before spawning Claude
(nextMessage() resolves null when the relaunch trigger arrives before any
turn was handled, so the message is parked as pending). That retired the
one-time --resume flag before the SDK ever had a chance to use it, so the
next launch started fresh instead of resuming.

Tie the flag's lifetime to the invariant it needs: retire it only once it
has been used, or once the context it points at has been explicitly
discarded. It is now consumed from onSessionFound (Claude reported a
session id back) and from onSessionReset (/clear dropped the context, and
/clear likewise returns before spawning Claude, so without this the flag
would outlive the reset and the next launch would resume the very session
the user just cleared). Attempts that reach neither outcome never touched
the anchor, so the flag survives for the next launch.

* fix(claude): report the actual /compact outcome instead of always success

When Claude cannot compact a session it says so on a system/status message
carrying compact_result: "failed" and a compact_error reason (for example
"Not enough messages to compact."), which arrives shortly before the result
message. The completion event was emitted from the result message alone, so
a compaction that did not happen was still surfaced to the user as
"Compaction completed".

Record the reported outcome when the status message arrives and use it when
the result message is handled, mirroring the wording the Codex launcher
already uses for the same situation ("Compaction failed: <reason>").

Only an explicitly reported failure is recorded: a status shape without
compact_result, or one reporting anything other than "failed", leaves the
existing success path untouched, so an unrecognised or unseen status can
never invent a failure.
2026-07-18 12:25:35 +08:00
weishu 22e9b38f70 fix(kimi): sync local sessions to web and adapt to new kimi-code architecture
hapi kimi local mode spawned the kimi TUI with no transcript sync, so
terminal conversations never reached the hub and the web UI stayed empty.
After the kimi-code rewrite (data moved from ~/.kimi to ~/.kimi-code),
model resolution also broke: hapi read the gone ~/.kimi/config.toml and
fell back to the invalid hardcoded default kimi-k2, and the KIMI_MODEL /
KIMI_PROJECT_DIR env vars it set no longer exist upstream.

Local sync (mirrors the codex transcript scanner):
- kimiWireLocator: derive the kimi-code workspace id
  (wd_<slug>_<sha256(cwd).12>, ported verbatim from upstream workdir-slug),
  poll for the session dir created by the just-spawned process, and watch
  its agents/main/wire.jsonl. Pre-existing sessions are snapshotted and
  excluded (awaited before spawn) so a retry cannot bind to a stale
  session; multiple fresh candidates are refused as ambiguous.
- kimiWireScanner: incrementally read wire.jsonl and convert events into
  hapi messages (user prompts/steers, assistant text/thinking, tool
  call/result incl. is_error, step.end usage with cached input summed
  into inputTokens).
- kimiLocalLauncher: attach locator+scanner, report kimiSessionId on
  discovery (enables web resume and local<->remote handoff).

Model handling:
- config.ts: read <KIMI_CODE_HOME|~/.kimi-code>/config.toml (legacy
  ~/.kimi fallback); drop the hardcoded kimi-k2 default and the dead
  KIMI_MODEL env source - when nothing is configured, omit --model so
  kimi-code uses its own default_model.
- kimiBackend/kimiLocal: stop setting KIMI_MODEL and KIMI_PROJECT_DIR
  (both unused by new kimi-code).
- kimiRemoteLauncher: apply the resolved model over ACP after session
  creation (session/set_model, falling back to the advertised model
  config option), and display the agent-reported current model instead
  of the env guess.

Verified against live kimi-code 0.26.0: ACP initialize/session-new/
prompt probes, locator discovery of a running session, and converter
robustness over a real 800-line wire.jsonl.
2026-07-18 12:18:15 +08:00
SSU-WEI HUANGandGitHub 6f5ecde2c4 fix(codex): expose HAPI threads to Codex Desktop (#1022)
* test(codex): reproduce desktop thread classification gap

* fix(codex): classify HAPI threads as user sessions

* test(codex): cover thread source creation paths
2026-07-16 12:35:04 +08:00
SSU-WEI HUANGandGitHub 520c3f511a fix: verify Cursor chat store before reopen (#1037)
* test: reproduce issue #841

* test: cover Cursor chat store discovery

* fix: verify Cursor chat store before resume (closes #841)

* test: preserve non-Cursor resume behavior

* test: cover conservative Cursor resume gating

* fix: gate Cursor reopen until store verification

* test: cover legacy Cursor drawer fallback

* fix: scan unique legacy Cursor store drawer

* test: preserve raw Cursor workspace path hashing

* fix: hash raw Cursor workspace path

* test: pin Cursor probe owner and machine

* fix: probe Cursor store on recorded owner

* test: normalize Cursor probe owner home

* fix: normalize Cursor probe owner home
2026-07-16 12:34:41 +08:00
SSU-WEI HUANGandGitHub f457156bd1 feat(cli): add skill_lookup MCP for non-native agents (#1035)
* test: reproduce issue #752

* fix: expose skill lookup MCP tool (closes #752)

* test: cover ACP skill lookup instructions

* fix: inject ACP skill lookup instruction

* test: narrow skill lookup auto-approval

* fix: restrict skill lookup auto-approval

* test: cover exact skill lookup tool names
2026-07-16 12:31:36 +08:00
SSU-WEI HUANGandGitHub 553b3492f1 fix(codex): wait for manual compaction to finish (#1038)
* test: reproduce issue #982

* fix: wait for Codex manual compaction (closes #982)

* test: cover Codex compaction turn completion
2026-07-16 12:30:04 +08:00
SSU-WEI HUANGandGitHub c87720ab4d fix(cli): load extra headers from settings (#1041)
* test: reproduce issue #786

* fix: load extra headers from settings (closes #786)

* test: cover extra header precedence and redaction

* fix: redact persisted extra headers in diagnostics

* test: cover runner extra header identity

* fix: restart runner when extra headers change
2026-07-16 12:27:50 +08:00
DullJZandGitHub e87e875d72 feat(opencode): accept -s/--session as resume aliases (#1042)
Map OpenCode-native session flags to resumeSessionId so
`hapi opencode -s <id>` restores the session like --resume.
2026-07-16 12:26:19 +08:00
87f5c78ab2 fix(codex): scan transcripts incrementally (#1031)
Co-authored-by: zj1123581321 <zj1123581321@users.noreply.github.com>
2026-07-13 15:51:40 +08:00
weishu 1c834607a2 Release version 0.22.3 2026-07-13 09:03:39 +08:00
8ee04500b9 fix(hub,cli): coerce null session activeAt so resume cannot 500 (#1026)
Legacy rows and inserts left sessions.active_at NULL while SessionSchema
required a number, so CLI GET /cli/sessions/:id failed Zod and resume
surfaced HTTP 500. Persist active_at on insert, harden hub read coerce,
and nullish-transform activeAt in SessionSchema (output stays number).

Fixes #1025

Co-authored-by: Debian <heavygee@oos-linux.in.lockhouse>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-13 09:00:17 +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