The public relay used to accept a shared auth key compiled into every
hub, so its bandwidth was open to anyone. The relay now issues a
per-hub credential it can meter and revoke, and hubs obtain one on
their own.
- --relay resolves an auth key at startup: HAPI_RELAY_AUTH env, then a
key persisted in settings.json, then a fresh key from the relay's
/issue endpoint. There is no shared-key fallback; if no key can be
obtained the tunnel does not start and the hub says why.
- A persisted key rejected by the relay (HTTP 403 after revocation or a
secret rotation) is discarded and replaced once, then the tunnel is
restarted, so a revoked hub recovers without manual edits. Keys given
explicitly through the environment are never overwritten.
- Issuance is rate-limited per public IP; HTTP 429 is reported with the
retry hint instead of being retried blindly, which matters for users
sharing a CGNAT or corporate egress address.
- The tunnel URL now comes from upstream tunwg's slog JSON on stderr
(msg="listener started"), replacing the fork's custom --json event,
and --log_level=0 keeps per-request logs out of the hub console.
Requires a relay running tunwg with TUNWG_AUTH_SECRET configured.
Implement two-stage content codec: truncate oversized agent messages
(48KB head + 12KB tail + marker, idempotent, never user messages) and
compress message content JSON ≥256 bytes via zstd with fallback.
- contentCodec.ts: new codec with truncation and compression routines
- messages.ts: wire codec into addMessage (truncate+compress),
copyMessageToSession (lossless), toStoredMessage (decode both formats)
- index.ts: schema version 15→16 with no-op migration (enforces
schema match on downgrade, no DDL change)
- codexDesktop.ts: pass content through idempotent truncation for
canonical comparison in transcript-import
- cleanup-sessions.ts: decode compressed rows, replace full-scan with
per-session batched scan (LIMIT 50) stopping at first user message
- compact-db.ts: new offline compactor that retroactively applies
truncate+compress+VACUUM to existing DBs; guards against schema
version mismatch, symlink aliasing, and handles re-runs
- Tests: contentCodec round-trip + truncation + idempotence + legacy
decode; messages integration (compressed/truncated round-trip);
migration tests updated to version 16
- README.md: document codec and both maintenance scripts
Measured on 2.26GB production DB: 2159MB→768MB (-64%) in 12s,
748k messages intact, integrity verified.
- @assistant-ui/react ^0.11.53 -> ^0.14.29, react-markdown ^0.11.9 -> ^0.14.7
- resolves @assistant-ui/tap 0.9.8, which ships the upstream fix for
bulk message prepends (per-scheduler MAX_UPDATE_DEPTH guard, PR
assistant-ui/assistant-ui#5370) that the local patch covered for 0.3.5
- API migration: useAssistantApi -> useAui, useAssistantState -> useAuiState
with s.* selector access; TextMessagePart type-guard for content.find;
portable DefaultComponentsMap annotation for memoizeMarkdownComponents
Verified: tsc clean, 1762 unit tests, history-load e2e 12/12 against the
unpatched upstream scheduler.
Remote sessions get an authoritative context_window injected into usage by
the CLI (from SDK result modelUsage), but local-mode sessions forward raw
transcript JSONL whose usage has no context_window, so the web fell back to
getContextBudgetTokens — which could not tell Fable's 1M window from its
bare id (only the "[1m]" suffix was recognized) and received session.model,
which is usually null for local sessions, defaulting to a 200k budget. A 1M
Fable session with ~256k of context showed 135% used with an over-limit
warning instead of ~26%.
- modelConfig: recognize Fable ids (fable, fable[1m], claude-fable-*) as 1M
- reducer: LatestUsage carries the usage-bearing message's own model
- StatusBar: new contextModel prop feeds the fallback heuristic only;
the model prop's other semantics are unchanged
- HappyComposer forwards it; SessionChat passes
latestUsage.model ?? session.model
Add permission mode tracking across the local→remote switch so mode changes
made inside the interactive Claude TUI (shift+tab) are inherited when
launching remote. Previously, a mode picked in local TUI was invisible to
remote sessions.
Implementation:
- generateHookSettings: add trackPermissionMode option to register UserPromptSubmit
and PreToolUse hooks (their payloads carry permission_mode; SessionStart's does
not). Export buildHookSettings for testing and make matcher optional.
- New hookPermissionMode.ts: normalizer for hook permission_mode → HAPI mode
('manual' → 'default'; unknown modes like 'dontAsk' → null/ignored).
- runClaude.ts: generate a second, local-TUI-only hook settings file with
trackPermissionMode enabled (remote SDK process keeps the SessionStart-only
file — these hooks block Claude per prompt/tool, and remote state is owned
by hub/RPC). Hook callback inherits mode when session.mode === 'local': updates
currentPermissionMode, syncs session, pushes keepalive, emits permission-mode-
changed event.
- session.ts, loop.ts, claudeLocalLauncher.ts: plumb localHookSettingsPath
(defaults to hookSettingsPath when not provided).
When a batch is parked after a mode switch (pending delivery), the next
attempt that resumes from the parked batch must still seed modeHash/mode
into the relaunch gate. Without this, the gate keeps modeHash=null, so the
NEXT mode switch fails the hash check and gets silently fed into a process
spawned with the old --permission-mode (e.g. 'auto' silently running as
'default'). Fix adds hash to the pending type and seeds modeHash/mode when
delivering a parked batch.
Replace the always-visible wrapping chip row below the md breakpoint with a
filter icon button in the session list header (right side, next to the new
session button). The button opens a radio menu with per-machine counts and
an inline health summary, shows an active-filter dot, clamps to the
remaining viewport/safe-area space, and supports Escape/Arrow-key
navigation with focus restore. Desktop keeps the one-tap chip bar.
Remove the refresh icon button from the session list toolbar and make
the list itself the refresh affordance via a touch pull gesture.
Gesture (SessionList):
- Touch listeners on the scrollable list container; pull engages only
at scrollTop 0, with 16px feedback / 64px trigger thresholds, and
fires on release past the trigger. Mirrors the established
pull-to-load-older pattern in HappyThread.
- Touch-only by design: desktop has no overscroll bounce, so a wheel
pull feels broken; desktop keeps relying on SSE live updates and
query focus refetch.
- onRefresh widened to () => Promise<unknown> | void so the indicator
tracks the in-flight refetch and ignores re-entrant pulls.
Feedback:
- Status pill over the list (role=status, aria-live) shows
pull/release/refreshing states with a spinner while refreshing; it
also covers the initial useSessions load (isLoading), which lost its
only busy indication when the toolbar button was removed.
- The success toast is dropped (the pill is the feedback); the failure
toast is kept. handleRefresh now returns its promise.
Empty states (review P2s):
- SessionsEmptyState and the no-results message move from the shrink-0
header container into the scroll container, so the gesture works on
the visible empty state (retry path after a failed initial fetch)
and short viewports scroll instead of crushing the gesture area.
- SessionsEmptyState is gated on !isLoading so a slow initial request
no longer flashes the final empty state with active actions.
i18n: add sessions.refresh.pull/release/refreshing (en + zh-CN),
remove now-unused button.refresh and sessions.refresh.success.*.
Desktop wheel pull was implemented and then reverted after review.
Move the session-list search out of its dedicated full-width row and
into the sidebar toolbar, so the sidebar gets one compact header row
instead of two stacked rows.
Behavior:
- Collapsed by default: a search icon sits at the left of the toolbar
row, on the same line as the existing tool icons (codex import,
refresh, browse, settings, new session).
- Clicking the icon expands the input to fill the entire row width
(tool icons hide) and focuses the input; when focus leaves the
search container the input collapses back to the icon.
- Query and date range stay applied while collapsed; the icon shows
an indicator dot whenever any filter is active.
- The date-range picker stays inside the expanded input.
Implementation:
- SessionList gains a headerActions prop and renders a single header
row: [collapsible search] [spacer] [optional renderHeader plus]
[headerActions].
- SessionsPage drops its own toolbar wrapper and passes its five
buttons through headerActions instead.
- The safe-area top inset moves to the router container wrapping both
the error banner and the list, so iOS PWA error text no longer sits
beneath the status bar/notch.
Focus handling (review follow-ups):
- Controls that unmount themselves on click (clear-query X, picker
backdrop, range-end selection, picker footer Clear) silently moved
focus to <body>, leaving the search expanded but no longer
collapsible via blur. Each now returns focus to the input; the
picker footer Clear is wired through a dedicated onClear prop.
- Header actions can no longer be suppressed by a stale expanded
state: they render whenever the search control itself is absent
(e.g. the session list empties via SSE), and the expansion resets
when the list becomes empty.
i18n: add sessions.search.open (en / zh-CN).
Tests: adapt existing search/date-filter tests to expand the search
before interacting; add regressions for collapse-on-blur query
persistence, focus restore after clearing the query and after
clearing the date range, and header actions surviving an emptied
session list.
Remove the top-right Loading messages pill shown while tail reconciliation runs with cached conversation content.
Keep background tail synchronization silent so it does not compete with older-history loading feedback or imply that the visible conversation is blocked. Cold-start skeletons, pull-to-load guidance, older-page loading state, unseen-message navigation, and global reconnect feedback remain unchanged.
Loading an older page prepends hundreds of messages in one flush. tap's
scheduler aborts after 50 dirty resources and drops the overflow, so the
thread never applied the merged page: the scroll-restore gate never
passed and the top sentinel kept re-triggering (loads everything at
once). Raise MAX_FLUSH_LIMIT 50->2000 via bun patchedDependencies.
Adds a Playwright regression spec driving the real message-window store
and HappyThread against a fake paginated API: one page per top
approach, scroll restored, no idle reloads.
Remove the redundant "Load older" button; the existing top sentinel
already auto-loads older pages when approaching the top. Loading state
moves to a floating pill overlay so prepends no longer shift layout.
- Raise message page size 50 -> 200 (hub max) so cold loads and load-older
show more than a handful of rendered bubbles
- Flush pending messages when the thread forces scroll-to-bottom on mount,
and re-read atBottom after the latest fetch instead of using the stale
pre-fetch snapshot, so new messages no longer stay invisible after
leaving and re-entering a session
- Base the cold-load backfill floor on rendered conversation identities:
skip non-rendering rows (token-count/ready events, un-normalizable
content) and collapse tool call/result pairs into one card identity
Hub: getOrCreateMachine now merges incoming machine-owned metadata over
the stored row (first-write-wins previously kept rows registered without
a host name nameless forever; hub-only fields like displayName survive).
Web: session-list machine labels are cached in localStorage so machines
whose row is gone or whose query has not loaded yet keep their last
known name instead of flickering to the 8-char id prefix.
useLongPress binds both touch and mouse handlers. After a tap, touch
browsers emit compatibility mouse events (~300ms later) that the page did
not preventDefault, so onClick fired twice: once from touchend, once from
the synthesized mouseup. On the wide tablet sidebar layout the list stays
under the finger, so the second onClick lands on whatever row slid into
that position and navigates to the wrong session.
preventDefault() on touchend for every handled tap, and additionally
swallow mouse events that arrive within 700ms of a touch so browsers that
still dispatch the compatibility sequence cannot re-trigger onClick.
Based on the fix by RiriAgent in the fork (commits 1bbfcc20, 5e3d135a).
Co-authored-by: RiriAgent <39219425+RiriAgent@users.noreply.github.com>
- 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
SessionChat fetched Codex models through the session-scoped endpoint,
so the CLI listed models in the session process cwd, where a missing
directory or project-level Codex config could skew or break the result.
Use the machine-scoped endpoint (already used by NewSession) and drop
the now-unused session route and RPC plumbing.
Fixes#1072
Searching the session list forced every directory group to expand all
sessions (expanded: isFiltering) and hid the Show more button, so the
user's per-group preview fold was ignored during filtering. Stop
overriding the preview state while filtering and keep the Show more /
Show less control available.
Fixes#1068
Single-machine users no longer expand a redundant machine layer; with
multiple machines a chip filter bar (persisted, with hover health popup)
replaces the collapsible machine headers. Directory groups now render
top-level with machine-name suffixes when unfiltered. Also removes the
redundant session/project count header text.
The Codex import button used the same circular-arrow SVG as the
session-list refresh button, making the two adjacent actions look
identical. Switch the import affordance to a download-into-tray icon
to match its 'import sessions' semantics.
Closes#1135
Use @lobehub/icons (already a dependency) for per-agent SVG logos in the
session list, session header, and new-session agent selector.
- Color variants for claude/codex/gemini; Mono (currentColor) for
cursor/grok/opencode so glyphs track the theme text color
- kimi uses Mono as well: KimiColor's main glyph is hard-coded #fff and
vanishes on the light theme
- pi and unknown flavors keep the letter-badge fallback (no logo shipped)
- Deep component imports avoid the package root's ./features re-export,
which pulls uninstalled peer deps (antd, @lobehub/ui)
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.
Add 'auto' as a first-class HAPI permission mode for claude-flavored sessions,
enforced by Claude's classifier rather than emulated in canCallTool. Includes
mode configuration, CLI respawn on auto transitions, plan-exit targeting, API
extensions, and documentation updates.
Bun's fetch and node:http honor HTTP_PROXY/HTTPS_PROXY env vars, which can
route loopback traffic through a configured proxy (e.g. Surge/Clash). When
NO_PROXY doesn't explicitly exclude localhost, this breaks loopback
communication: SessionStart hooks fail to arrive (transcripts don't sync, web
UI stays empty), runner control client times out, and MCP server connections
fail.
Normalize NO_PROXY at the CLI entrypoint to always cover loopback
(localhost, 127.0.0.1, ::1). Child processes inherit the patched env so their
loopback traffic is covered too. Non-loopback traffic continues using the
configured proxy. Supersedes the runner control client workaround from #563.