285 Commits
Author SHA1 Message Date
ahahahaandClaude Opus 4.7 de5ca264aa feat(core): relay unsolicited agent events between user turns (#608)
* feat(core): relay unsolicited agent events between user turns

Add a background goroutine per interactive session that consumes agent
events produced between user-initiated turns (e.g. Claude Code background
task completions via run_in_background). Previously these events piled up
in the buffered channel and were unconditionally discarded by drainEvents()
when the next user message arrived.

Key changes:
- Add startUnsolicitedReader/stopUnsolicitedReader with context-based
  lifecycle management and bounded handoff to foreground turns
- Replace unconditional drainEvents with conditional resync via
  eventsNeedResync flag (default true, cleared on clean EventResult)
- Integrate unsolicited reader teardown into all cleanup paths:
  cleanupInteractiveState, stopInteractiveSession, session recycle,
  /compress, and idle reaper
- Track BeginTurn/EndTurn for unsolicited turns so idle reaper does
  not kill workspaces with active background processing
- Make workspace pool idle timeout configurable via
  workspace_idle_timeout_mins in project config (was hardcoded 15min)
- Export SetWorkspaceIdleTimeout and DefaultWorkspaceIdleTimeout

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

* test(core): add unit tests for unsolicited events feature

Add 10 tests covering the unsolicited events implementation:
- TestUnsolicitedReader_RelaysEventResult: verifies EventResult relay
- TestUnsolicitedReader_StopsOnCancel: verifies clean goroutine shutdown
- TestUnsolicitedReader_SetsResyncOnChannelClose: abnormal exit path
- TestUnsolicitedReader_SetsResyncOnEventError: error event handling
- TestUnsolicitedReader_PermissionDeny: permission auto-deny when no user
- TestEventsNeedResync_DefaultTrue: constructor initialization
- TestEventsNeedResync_ClearedOnCleanResult: clean EventResult path
- TestCleanupInteractiveState_StopsUnsolicitedReader: cleanup teardown
- TestWorkspaceIdleTimeout_Configurable: timeout config API
- TestReapIdle_DisabledWhenZeroTimeout: zero timeout disables reaping

Tests use event-driven synchronization (channel waits with timeouts)
instead of fixed time.Sleep to avoid CI flakiness.

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

* fix(core): stop unsolicited reader in drainOrphanedQueue to prevent race

drainOrphanedQueue calls drainPendingMessages which reads from the
Events() channel. Without stopping the unsolicited reader first, two
goroutines can concurrently read from the same channel — a data race.

Also restart the unsolicited reader after draining completes so
background events continue to be relayed.

Found during self-review concurrency audit.

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

* test(integration): add end-to-end tests for unsolicited events

Add two integration tests covering the full flow:

TestIntegration_UnsolicitedEventsEndToEnd — verifies the happy path:
  1. User sends msg → agent turn completes (EventResult)
  2. Agent emits new events later (simulating run_in_background completion)
     → unsolicited reader relays them to platform
  3. User sends second msg → conditional drain skipped (clean exit) →
     new turn response delivered correctly

TestIntegration_StaleEventsDrainedAfterAbnormalExit — verifies the safety
net: after EventError sets eventsNeedResync=true, buffered leftover events
are drained on the next user message (not relayed as unsolicited, not
attributed to the new turn).

Uses a persistentEventsSession that keeps the Events() channel open across
turns (unlike FakeAgentSession which closes per-call). Synchronization is
event-driven via waitForMessage and waitForPromptCount — no fixed sleeps.

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

* fix(core): address PR #608 review comments

Addresses all five review comments from @chenhg5 on PR #608:

1. **[blocker]** Promote workspace_idle_timeout_mins to a global config.
   Adds top-level Config.WorkspaceIdleTimeoutMins; per-project
   ProjectConfig.WorkspaceIdleTimeoutMins retains precedence so existing
   configs keep working. main.go picks project > global > default.

2. Close the concurrent-reader window around stopUnsolicitedReader.
   After cancel + bounded 5s wait, the reader now double-checks ctx
   after reading an event — if cancellation happened between the outer
   select firing and the check, the event is dropped and resync is
   requested, preventing the old reader from silently consuming an event
   that belongs to the incoming foreground turn. The wait stays bounded
   because getOrCreateInteractiveStateWith and stopInteractiveSession
   call this while holding interactiveMu, and an unbounded wait there
   would stall unrelated sessions.

3. Notify the user on auto-deny. When approveAll=false, the reader now
   sends a localized MsgBackgroundAutoDenied message (EN/ZH/ZH-TW/JA/ES)
   identifying the requested tool so a silently blocked background task
   isn't invisible. RespondPermission is dispatched on a detached
   goroutine so the slow adapter call cannot block reader shutdown.

4. Accumulate and log tool events. EventToolUse / EventToolResult now
   record tool names and emit debug logs. If the channel closes mid-turn
   with buffered context, a warning log captures the tools/text that
   were lost — removes the misleading "accumulate silently" comments.

5. Document Session.AddHistory thread-safety. Session.AddHistory is
   already internally locked; stopUnsolicitedReader's bounded wait
   orders unsolicited-vs-foreground history writes. Added an explanatory
   comment so the invariant is explicit.

Verified with go test -race ./... and go test -race -tags=integration
./tests/integration/. Reviewed end-to-end by Codex (gpt-5.3-codex, high
reasoning effort) — no regressions flagged.

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

* fix(config): make workspace_idle_timeout_mins global-only

Addresses the remaining blocker on PR #608: the maintainer asked for
workspace_idle_timeout_mins to be a global config rather than per-project.

Changes:
- Remove ProjectConfig.WorkspaceIdleTimeoutMins as a supported per-project
  knob; the canonical setting is now the existing top-level
  Config.WorkspaceIdleTimeoutMins.
- Keep parsing the legacy per-project key (renamed to
  WorkspaceIdleTimeoutMinsLegacy in code) so existing configs continue to
  work. When the top-level field is unset, the legacy per-project value is
  honored as a fallback and a deprecation warning is logged at startup.
  This avoids silently breaking deployments that used `0` to disable
  reaping or a custom timeout.
- Clarify the comment on the global field to state that per-project
  configuration is intentionally not supported.

Also commits the unsolicited-reader test additions in core/engine_test.go
(437 lines covering reader relay, cancel/stop, channel close, error
handling, permission deny, resync flag defaults/clearing, cleanup, and
workspace idle timeout configurability) that landed alongside the prior
review-fix commit but were not previously committed.

Verified via `codex exec review --uncommitted` (gpt-5.3-codex). Build
clean, all relevant tests pass; the 3 known macOS-specific pre-existing
failures (TestProcessInteractiveEvents_AppendsReplyFooter*,
TestResolveLocalDirPath_AcceptsSubdir) are out of scope.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-30 17:25:33 +08:00
cg33andClaude b05f9fe6ae fix(core): guard workspacePool nil access in getOrCreateWorkspaceAgent (#754)
When a cron job has WorkDir set but multi-workspace mode is not enabled,
getOrCreateWorkspaceAgent() would dereference the nil workspacePool and
panic. Add a nil check that returns an error instead, allowing the cron
executor to fall back to the global agent gracefully.

Closes #748

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-27 14:43:32 +08:00
bingkxuandvibecoder e81cc1d568 fix(opencode): prevent empty response when agent process crashes (#720)
When the opencode process crashes (e.g. stale session ID causes
'Session not found'), readLoop sends a fallback EventResult before
the deferred cmd.Wait() can detect and emit an EventError. The
engine receives EventResult(Done:true) first, returns from the
event loop, and the subsequent EventError is never consumed.
The user sees an empty response with no error information.

Fix by checking stderrBuf immediately after the scanner loop exits,
before sending the fallback EventResult. If stderr contains errors
(including 'Session not found'), emit EventError instead.
cmd.Wait() remains in a defer for resource cleanup only.

Engine uses a pattern table (agentErrorHandlers) to match known
errors and show localized user-friendly messages with recovery hints.

Co-authored-by: vibecoder <16832299+vibecoder@user.noreply.gitee.com>
2026-04-27 14:30:30 +08:00
Claude fc79307142 fix(core): restore /btw alias, gate NO_REPLY done reaction, fix i18n gaps
1. Restore /btw as alias for /ps — prevents breaking change for existing users
2. Suppress done_emoji when agent reply is NO_REPLY (silent turn)
3. Handle NO_REPLY on channelClosed path — prevents leaking silent content
4. Add /show command to help text for zh-TW, ja, es (was only in en, zh)
5. Add TestBtwAlias_ResolvesToPs test

Made-with: Cursor
2026-04-24 07:09:48 +08:00
LyInfi e67915227b fix(core): suppress reply footer when only workdir is known (#701)
When the agent does not expose model/reasoning-effort/usage (e.g. the
claudecode agent) and the work directory equals $HOME,
compactReplyFooterPath returns "~" as the only part of the footer.
appendReplyFooter then wraps it as "*~*", which renders as a bare "~"
on Feishu/Weixin (both strip the surrounding italic markers).

A workdir alone isn't a useful status signal anyway, so skip the
footer entirely unless at least one status part (model, reasoning
effort, context remaining, or usage) is present.

Regression test covers an agent that only exposes GetWorkDir() and
runs in the user's home directory.
2026-04-24 06:32:40 +08:00
HanandClaude Opus 4.6 6e0a2e116e feat(shell): support ! prefix shortcut and --timeout for shell commands (#658)
Add `!` prefix as a shorthand for `/shell` (e.g., `!ls -la`), with the
same admin permission checks. Also add optional `--timeout <seconds>`
parameter at the beginning of shell commands to override the default
60s timeout (e.g., `/shell --timeout 300 npm install`).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 06:30:56 +08:00
CCandCigarrr dab7cc6d8e feat(core): replace /btw with /ps as builtin command (#620)
- Register /ps in builtinCommands with proper help card entry
- Implement cmdPs() following cmdStop() pattern using interactiveKeyForSessionKey
- Add MsgPsSent, MsgPsSendFailed, MsgPsEmpty, MsgPsNoSession i18n (5 languages)
- Remove isBtwCommand/matchBtwPrefix and mid-TryLock btw injection block
- Update MsgPreviousProcessing to reference /ps instead of /btw
- Update Slack app manifest slash command

Closes #619

Co-authored-by: Cigarrr <cigarrr@users.noreply.github.com>
2026-04-24 06:29:26 +08:00
ee81bb59e6 feat(core): suppress agent replies marked with NO_REPLY (#682)
* pr-no_reply: suppress NO_REPLY marker responses

Add Mode-aware NO_REPLY handling on top of PR 655 display.Mode enum.
Agent can return a response beginning with 'NO_REPLY' to silently
skip platform delivery while preserving history + context.

- isSilentReply / couldBeSilentPrefix helpers (regex + prefix test)
- EventText: hold streaming preview while segment could still resolve
  to a bare NO_REPLY marker; release (flush) once disambiguated
- silentHold reset at each segment boundary (thinking/tool/user)
- EventResult: detect isSilent on cleaned response; gate context
  indicator, reply footer, TTS, and final send; call sp.discard()
  to remove preview + skip done reaction via needsDoneReaction gate

* pr-no_reply: document NO_REPLY in injected system prompt

Agents cannot use the silent-reply feature unless they know about it.
Add a 'Silent reply' section to AgentSystemPrompt() describing when and
how to return the bare NO_REPLY marker.

* pr-no_reply: strip trailing NO_REPLY from mixed reasoning+marker responses

Extends NO_REPLY handling to match OpenClaw's stripSilentToken semantics.
Motivation: agents running cron/analysis tasks often narrate their
reasoning before appending NO_REPLY, defeating the bare-only detector.

- stripTrailingSilent helper (regex (?:^|\s+|\*+)NO_REPLY\s*$)
- EventResult: after bare-match check, try trailing strip;
  empty result -> fully silent, non-empty -> deliver stripped text
- System prompt clarified: agent may end reply with NO_REPLY line;
  stripped reasoning still delivers as normal reply

* test(core): add table-driven tests for NO_REPLY helpers

Cover isSilentReply, stripTrailingSilent, and couldBeSilentPrefix with
table-driven cases locking the contract these helpers expose to the
streaming preview and final-turn paths. Mirrors the coverage approach
used by PR #684 for its simpler strip helper.

isSilentReply: exact bare-marker match is case-insensitive and tolerates
surrounding whitespace/newlines; mixed content or tokens sharing the
prefix are rejected.

stripTrailingSilent: strips a trailing NO_REPLY preceded by whitespace
or markdown asterisks; leaves unchanged when the marker is absent,
mid-string, or glued to a longer token.

couldBeSilentPrefix: true while the trimmed input is a case-insensitive
prefix of NO_REPLY (including empty/whitespace-only), false once it
diverges or grows longer than the marker.

---------

Co-authored-by: Cigarrr <cigarrr@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 06:25:32 +08:00
Zeren ChenandClaude d01a9ccbb2 fix(telegram): show Telegram-safe skill commands in /skills (#571)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-24 06:24:32 +08:00
cg33andClaude Opus 4.6 39c0f9610a feat(core): make message queue depth configurable (#690)
* docs: add Homebrew as install option (brew install cc-connect)

Made-with: Cursor

* feat(core): make message queue depth configurable (#688)

Replace hardcoded maxQueuedMessages=5 with a configurable value via
[queue] max_depth in config.toml. When the queue is full, send a new
MsgQueueFull message with the current depth instead of the misleading
MsgPreviousProcessing.

- Add QueueConfig struct in config with max_depth field
- Add Engine.SetMaxQueuedMessages() setter, default remains 5
- Add MsgQueueFull i18n key with all 5 languages
- Wire config in main.go
- Update existing overflow test for new behavior

Closes #688

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-23 22:36:40 +08:00
Claude f3a729d7ab test(integration): add real agent adapter tests for filter_external_sessions
Integration tests using real codex.Agent and claudecode.Agent adapters
with JSONL fixture files on disk. Tests the full pipeline: agent reads
real session files → ListSessions → Engine applySessionFilter → command
output. Covers /list, /switch, /delete under both filter modes plus
dynamic runtime toggle, for both Codex and Claude Code agents.

Made-with: Cursor
2026-04-21 08:11:11 +08:00
Claude 7a3bad4815 feat(session): add filter_external_sessions config (default off)
Session filtering that hides external CLI sessions is now opt-in via
`filter_external_sessions = true` in project config. By default, /list,
/switch, /delete show all agent sessions regardless of origin.

Includes integration tests covering /list, /switch, /delete, card
rendering, and dynamic toggle under both modes, plus config parse tests.

Made-with: Cursor
2026-04-21 08:02:21 +08:00
Claude 668c9d3ba6 fix(core): add name mapping to relay paths + tests for all agent patterns
Complete the session name mapping coverage:
- Add name mapping to relay start (CompareAndSet without name)
- Add name mapping to relay EventText handler
- These paths were the last remaining gaps from the audit

Add end-to-end tests for all three agent SessionID patterns:
- Pattern A (ACP-like): CurrentSessionID() non-empty at start
  → name mapped in startOrResumeSession
- Pattern B (claudecode/gemini/cursor-like): ID in early EventText
  → name mapped in EventText handler
- Pattern C (codex/qoder/kimi-like): ID only in EventResult
  → name mapped in EventResult handler

All three tests simulate the full /new → message → reply → verify flow.

Made-with: Cursor
2026-04-20 23:06:04 +08:00
Claude 86ab722c3e fix(core): map session name in EventResult handler (codex compatibility)
Codex agent's CurrentSessionID() returns "" at startup; the thread ID
is only set after Send() triggers thread.started. The session ID reaches
the engine through EventResult (turn.completed), not EventText.

The previous fix only added name mapping in startOrResumeSession and
EventText handlers, but for codex:
- startOrResumeSession: CurrentSessionID()="" → no-op
- EventText: codex doesn't set SessionID on text events → no-op
- EventResult: was using SetAgentSessionID (unconditional) instead of
  CompareAndSetAgentSessionID, and had no name mapping at all

Now all three EventResult handlers (interactive, relay, drain) use
CompareAndSet and map the pending session name on first ID binding.

Added TestSessionName_CodexLikeFlow that simulates real codex behavior:
empty CurrentSessionID at startup, ID only in EventResult.

Made-with: Cursor
2026-04-20 22:57:22 +08:00
Claude b8a46c42b9 fix(core): map session name during startOrResumeSession
When /new creates a named session, the name was stored in Session.Name
but never mapped to the agent session ID in SessionManager.sessionNames.
The name mapping only happened in the EventText handler via
CompareAndSetAgentSessionID, but that returned false because
startOrResumeSession had already set the same ID moments earlier.

Now startOrResumeSession also calls SetSessionName when it first binds
the agent session ID to a session that has a pending custom name.

Made-with: Cursor
2026-04-20 22:49:09 +08:00
Claude 772e4388fe fix: preserve comments and config fields during provider switch
- Rewrite SaveActiveProvider, SaveAgentModel, SaveProviderModel,
  SaveLanguage, SaveDisplayConfig, SaveTTSMode to use surgical
  text-level TOML editing instead of full Unmarshal→Encode cycle.
  This preserves comments, unknown fields, and formatting.
- Add patchProjectAgentOption, patchTopLevelField, patchSectionField
  helpers for targeted config edits.
- Extend rawProjectSpan to track [projects.agent] and
  [projects.agent.options] sections.
- Add resetAllSessions to Engine; call it from management API
  provider activate endpoint so sessions reset properly.
- Update docs/feishu.md: add card.action.trigger to required event
  subscriptions; add FAQ for card button errors.
- Add startup log when feishu interactive card mode is enabled.
- Add tests for comment/field preservation in save functions.

Made-with: Cursor
2026-04-20 18:07:59 +08:00
Claude c982a61dc1 fix: correct copy-paste comment on SetHooks method
Made-with: Cursor
2026-04-19 20:37:50 +08:00
Claude 4d5cadd41b feat: add lifecycle event hooks (command/http) for message, session, cron, error events
- New HookManager with shell command and HTTP POST handlers
- 7 event types: message.received/sent, session.started/ended,
  cron.triggered, permission.requested, error
- Async by default (fail-open, non-blocking), configurable via [[hooks]] in config.toml
- Shell hooks receive context via CC_HOOK_* env vars; HTTP hooks POST JSON payload
- Supports wildcard (*) event matching
- 20 unit tests covering validation, command/http execution, async behavior, timeouts

Made-with: Cursor
2026-04-19 09:19:07 +08:00
Claude 345cdd59ac feat: add skill management page with local/recommended tabs
- Add /skills route with local skills browser and recommended presets
- Backend: ListSkills/SkillDirs/AgentTypeName on Engine, skill presets API
- Provider preset cards: pin register/add buttons to card bottom
- Fix layout footer overlap by removing min-h-0 from content wrapper
- Replace logo with styled text brand, remove temporary local JSON override
- Add i18n translations for skill-related strings (en/zh/zh-TW/ja/es)

Made-with: Cursor
2026-04-19 08:57:45 +08:00
Claude d02a5f1370 feat: provider switch fixes, agent type switching, integration tests & i18n
- Fix provider switch not taking effect: clear agent session ID on switch
  to force fresh session instead of resuming with stale config
- Fix ANTHROPIC_MODEL env leak from cc-switch: explicitly set model env
  when provider has custom BaseURL
- Add "clear provider" option in Feishu card for unsetting active provider
- Add debug logging for provider env vars and session startup commands
  (with secret redaction via RedactEnv/RedactArgs)
- Web UI: add agent type switcher in project settings with provider
  compatibility filtering and restart-required flow
- Web UI: filter global providers by agent_types when linking to project
- Web UI: fix heartbeat tab showing "running" for all projects (now shows
  "not configured" when heartbeat is not enabled)
- Web UI: fix page not refreshing after agent type change + restart
- Add comprehensive integration tests for both claudecode and codex
  provider switching (env vars, session args, config writing, send/receive)
- Complete i18n for all 5 languages: replyFooter, injectSender,
  thinkingMessages, toolMessages and their hints

Made-with: Cursor
2026-04-19 00:10:29 +08:00
Claude f8d2cad68f refactor: restructure provider presets with per-agent-type configuration
- Restructure provider-presets.json (v1→v2): replace flat endpoints/
  agent_models/models/agents/codex_config fields with a unified
  `agents` map keyed by agent type, each containing base_url, model,
  models list, and optional codex_config
- Update ProviderPreset Go struct with PresetAgentConfig and helper
  methods SupportsAgent()/AgentConfig()
- Add AgentModelLists to config.ProviderConfig for per-agent model
  lists; ResolveProviderRefs now resolves models per agent type
- Filter global provider linking by agent type in IM /provider add
  card and executeProviderLink to prevent incompatible associations
- Fix Codex 404 caused by shengsuanyun agent_types including "codex"
  with wrong base_url; remove codex from its agent_types
- Update web frontend ProviderPreset type and preset display/add logic
  to use the new agents map structure

Made-with: Cursor
2026-04-18 15:24:32 +08:00
Claude f7796be1fb fix: correct Shengsuanyun name (胜算云) and add invite link to provider add prompt
- Fix display_name from 省算云 to 胜算云 in provider-presets.json
- Show invite/register link in API key prompt card for preset providers
- Add MsgProviderAddInviteHint i18n key with all 5 languages

Made-with: Cursor
2026-04-17 22:25:34 +08:00
Claude b7304a2da9 feat: global provider management + card-based IM provider add
- Add global [[providers]] config with project-level provider_refs
- Add provider management page in web admin with preset support
- Implement card-based /provider add flow: preset selection card
  with sponsor presets, API key prompt, and manual "Other" option
- Add inline button support for Telegram-style platforms
- Add remote provider-presets.json with sponsor data
- Add REST API endpoints for global providers and provider refs
- Update project detail providers tab with unified list UI

Made-with: Cursor
2026-04-17 22:08:40 +08:00
Claude c660aff4e8 fix(core): always trigger done_emoji when agent finishes
Previously done_emoji only fired when stream preview had been updated
in-place at least once, which meant quick responses never triggered
the reaction. Remove the needsDoneReaction() gate so the platform's
AddDoneReaction is always called on EventResult.

Made-with: Cursor
2026-04-17 10:12:48 +08:00
Claude 00e6f7600e refactor(core): merge /agentsid into /status, add work_dir display
Remove the standalone /agentsid command and fold its output (Agent SID)
into /status. Also add work_dir to the status output so users can see
the current working directory at a glance.

Changes:
- Remove /agentsid command registration, handler, card button, help
  text, and all i18n entries (MsgAgentSIDWarning, MsgBuiltinCmdAgentSID)
- Add Agent SID line to /status (both plain-text and card variants),
  shown only when a session is active
- Add Work Dir line to /status output (i18n: 5 languages)
- Add MsgStatusAgentSID i18n key

Made-with: Cursor
2026-04-17 09:53:57 +08:00
Claude 3ebdbe9a6b fix(core): preserve old sessions when switching via /switch
Previously, /switch overwrote the active session's AgentSessionID,
causing the old agent session to disappear from KnownAgentSessionIDs
and thus from /list output.

Add SwitchToAgentSession() which finds an existing internal session
for the target agent session ID or creates a new one, preserving all
previous session references.

Made-with: Cursor
2026-04-17 08:14:27 +08:00
9db01586db fix(core): freeze preview card between text segments in quiet mode (#642)
When quiet mode is enabled (ThinkingMessages=false, ToolMessages=false),
EventThinking and EventToolUse events are suppressed but the stream
preview card is not frozen. This causes multiple Claude text outputs
to accumulate into a single preview card.

Fix: when thinking/tool events arrive in quiet mode and there is
accumulated text, freeze + detachPreview to preserve the current text
as a permanent message. When the preview is already degraded (after
the first freeze), send accumulated text directly as a regular message
instead of silently advancing segmentStart past it.

Co-authored-by: leoliang <leoliang@futunn.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:17:48 +08:00
Claude 9e118e631f fix: review fixes for pre-release stability (H1-H3, M1-M7)
H1: Kimi readLoop — move cmd.Wait() before EventResult to prevent
    out-of-order EventError after turn completion.
H2: Kimi tests — add skipUnlessKimiAvailable guard so CI without
    kimi binary skips gracefully instead of failing.
H3: Claude shellJoinArgs — add comprehensive unit tests for shell
    quoting (spaces, single quotes, backslashes, newlines).
M1: cli_path — document that paths with spaces are not supported.
M3: needsDoneReaction — add streaming preview tests covering
    UpdateMessage trigger, discard reset, and disabled state.
M5: /agentsid — add security warning about session ID sensitivity
    in the command response.
M7: i18n — localize reply_footer_remaining for ZH/ZH-TW/JA/ES;
    add MsgAgentSIDWarning translations for all 5 languages.
Made-with: Cursor
2026-04-16 15:13:33 +08:00
ahahahaandClaude Opus 4.6 66efe3256f feat: add agent-sid command to query agent session ID (#605)
* feat(cli): add `cc-connect agent-sid` command to query agent session ID

Reads the agent session ID (e.g. Claude Code / Codex / Gemini CLI session
ID used with --resume) from persisted session files. Auto-detects project
and session key from CC_PROJECT and CC_SESSION_KEY env vars, making it
usable directly inside agent subprocess sessions.

Handles all session file naming variants: plain, hashed, workspace (_ws_),
legacy paths, and .sessions.json. When multiple files contain the same key,
prefers the one with the newest UpdatedAt. Includes nil-safety, file modtime
fallback for zero-UpdatedAt entries, and proper error propagation.

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

* feat(core): add /agentsid chat command to show agent session ID

Registers /agentsid (alias: /agent-sid) as a builtin chat command so
users can query the agent session ID directly from Discord/Telegram/
Feishu etc. without asking the agent to run a Bash command.

- Added to builtinCommands registry, handleCommand switch, and help
  card (session group)
- Added to text-based /help output for all 5 languages (EN/ZH/ZH-TW/JA/ES)
- Added i18n description key MsgBuiltinCmdAgentSID with translations

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

* fix(lint): use strings.TrimPrefix instead of HasPrefix+slice

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:48:56 +08:00
Jerry d4addfb13f fix(codex): preserve workspace app-server options (#612)
* fix(codex): preserve workspace app-server options

* fix(core): tighten workspace agent option snapshots
2026-04-16 14:44:52 +08:00
HanandClaude Opus 4.6 3d641eb316 feat(feishu): add done_emoji reaction after streaming preview updates (#580)
When the agent's response is delivered via in-place card update (Feishu
streaming preview), the user gets no push notification. This adds a
configurable done_emoji option (e.g. "Done") that adds a reaction after
the "OnIt" reaction is removed, so the user knows the agent finished.

Only activates in quiet mode when the preview card was updated in-place
at least once (via UpdateMessage). Non-quiet mode is unaffected as
intermediate messages already trigger push notifications. Single-round
responses that only use SendPreviewStart are also unaffected, as the
initial send already triggers a push notification.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 14:44:23 +08:00
xxb e119d54b5d feat(codex): add runtime reply footer aligned with Codex CLI (#578)
* feat(core): add optional reply footer metadata

* feat(config): enable reply footer by default

* feat(codex): use runtime footer metadata from app-server

* feat(codex): resolve exec footer metadata at runtime

* feat(core): style reply footer and prefer context left

* feat(codex): use real runtime context for reply footer

* feat(core): use english left footer text

* fix(codex): use current context tokens for footer left

* fix(codex): match cli context left calculation

* test(codex): adapt footer tests to session base url arg
2026-04-16 14:44:11 +08:00
q107580018 b8d75f73af fix(feishu): improve delete and /model card refresh flows (#645) 2026-04-16 14:43:53 +08:00
Zeren Chen 41e7470ef5 (#597)
* fix(telegram): omit skill menu commands on overflow

* fix(telegram): clamp bot command descriptions
2026-04-16 14:41:24 +08:00
Claude bd9dc515f4 fix: address high/medium priority issues from PR review (batch 3)
H1: Prevent workspace path traversal via ../
    Add resolveLocalDirPath() that validates resolved path stays under
    baseDir, expands ~/... via os.UserHomeDir, rejects escaping paths.

M2: Sanitize sender_name in inject_sender header
    Replace quotes with single quotes, strip newlines/CR to prevent
    prompt injection via display names.

M3: Warn on unset env var placeholders in config
    resolveEnvPlaceholders now logs slog.Warn when ${VAR} references
    an environment variable that is not set.

M4: Validate WeCom api_base_url
    Reject non-http(s) or malformed URLs at New() to prevent credential
    exfiltration via misconfigured base URL.

M5: Quote systemd Environment= values
    Wrap values in double quotes to handle spaces, #, and special chars
    in proxy URLs and paths.

M7: Discord table detection now requires separator row
    Only wrap pipe-delimited blocks that contain a markdown separator
    row (| --- | --- |), reducing false positives on decorative pipes.
Made-with: Cursor
2026-04-16 09:21:18 +08:00
cg33 9178b77fef Merge pull request #610 from g1f9/feat/workspace-init-local-dir
feat: support local directory paths in /workspace init
2026-04-16 07:45:36 +08:00
cg33 cfef710750 Merge pull request #586 from unimountltd/feat/sender-name-injection
feat(core): include sender display name in inject_sender header
2026-04-16 07:12:24 +08:00
Claude 7980a70a30 chore: fix golangci-lint errors flagged by CI
- management.go: check w.Write return value
- config_cmd.go: check fs.Parse return value
- web_manager.go: remove unused defaultMgmtPort/defaultBridgePort consts
- opencode.go: remove unused discoverModels wrapper
- engine.go: remove ineffectual resolvedWorkspace assignment

Made-with: Cursor
2026-04-15 20:49:13 +08:00
Claude 0d92d808a3 fix: address high/medium priority issues from PR review
H1: Prevent double-Close on concurrent cleanupInteractiveState calls
    by nil'ing state.agentSession under the lock before closing.

H2: Weixin StartTyping sends typingStatusStop on ctx.Done() and on
    repeat-failure exit, preventing stuck "typing" indicator.

M3: Feishu @mention resolution: downgrade PII log to Debug, HTML-escape
    display names in at tags, skip ambiguous (duplicate) names.

M4: Narrow segmentStart==0 condition in EventResult to only apply when
    tool messages are hidden (!e.display.ToolMessages).

M5: Log warning when cron workspace agent creation fails instead of
    silently falling back to global agent.

M7: Discord file download capped at 50 MiB via io.LimitReader.

M8: Weixin getConfig now checks ret/errcode fields and returns error
    on non-zero values.
Made-with: Cursor
2026-04-15 20:41:32 +08:00
cg33 2c46faf4bc Merge pull request #556 from Cigarrr/fix/cron-prompt-workspace-resolution
fix(core): resolve workspace for cron prompt-mode jobs in multi-workspace mode
2026-04-15 09:27:49 +08:00
cg33 cf4c73eb0e Merge pull request #550 from Cigarrr/fix/quiet-preview-overwrite
fix(core): use full accumulated text for stream preview finalization when tool messages hidden
2026-04-15 09:23:00 +08:00
cg33 c091c63922 Merge pull request #543 from leoliang1997/fix/cron-list-by-project
fix(core): /cron list shows all project cron jobs instead of session-…
2026-04-15 09:18:34 +08:00
chendaxin.tkandClaude Opus 4.6 0553192ad6 feat: support local directory paths in /workspace init
Previously `/workspace init` only accepted git URLs. Users with existing
local directories had no way to bind them to a channel without cloning.

Now `/workspace init <path>` accepts:
- Absolute paths: `/workspace init /data/code/my-project`
- Relative names: `/workspace init my-project` (resolved under base_dir)
- Home-relative: `/workspace init ~/code/my-project`

The conversational init flow also accepts directory paths — users can
send a directory path when prompted instead of only git URLs.

Adds looksLikeLocalDir() helper, MsgWsInitDirNotFound and
MsgWsInitInvalidTarget i18n keys with translations for all 5 languages,
and 4 new test cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:02:38 +08:00
sidney061212-aiandClaude Opus 4.6 7eb6f34b02 fix(core): persist agent_session_id using CurrentSessionID()
event.SessionID can be empty in some cases, causing the
agent_session_id to never be written to disk. When cc-connect
restarts, the session resumes without any agent_session_id and
Claude Code treats it as a brand new conversation, losing all
context.

Fix: use state.agentSession.CurrentSessionID() (interactive mode)
and agentSession.CurrentSessionID() (relay mode) instead, which
always returns the actual session ID held by the agent. Also call
sessions.Save() after SetAgentSessionID() to ensure the change
is flushed to disk.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 02:00:46 +08:00
Claude 5f27718a6b fix(core): prevent race condition during session cleanup
Two race conditions fixed:

1. cleanupInteractiveState timing (main issue):
   - Previously: state was deleted from interactiveStates map BEFORE
     closeAgentSessionWithTimeout was called synchronously
   - Problem: during the blocking close (up to 130s for Stop hooks),
     the map was empty but agent session was still shutting down
   - Result: concurrent requests saw conflicting state - some got
     "No execution in progress" while others saw blocking behavior
   - Fix: delete state from map AFTER closeAgentSessionWithTimeout
     completes, with re-check to avoid deleting replaced states

2. readLoop defer leak (secondary issue):
   - Previously: defer had early return on ctx.Done() that skipped
     closing cs.events and cs.done channels
   - Problem: if Close() timed out and called cancel(), the defer
     returned early without closing channels
   - Result: Close() waits forever on cs.done at line 633
   - Fix: remove early return from defer, always close channels

Root cause identified in issue #595:
- Session cleanup during /stop in cleanup window created conflicting
  replies ("Previous request still processing" vs "No execution in progress")
- MCP servers and Stop hook subprocesses with stdio:inherit could
  block scanner.Scan() indefinitely, preventing defer cleanup

Test plan:
- go build ./... passes
- go vet ./... passes
- go test ./... passes

Fixes #595

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 08:11:28 +00:00
Jim TangandClaude Opus 4.6 108ab64ccf feat(core): include sender display name in inject_sender header
When inject_sender is enabled, the agent now receives the sender's
human-readable display name alongside the opaque user ID. This lets
agents distinguish between users by name in shared channel sessions
(e.g. Slack channels where multiple people talk to the same bot).

Header format changes from:
  [cc-connect sender_id=U123 platform=slack chat_id=C789]
to:
  [cc-connect sender_id=U123 sender_name="Jim Tang" platform=slack chat_id=C789]

The name is quoted to handle spaces. When no display name is available
(e.g. platform didn't resolve it), the header falls back to the
existing format without sender_name.

Changes:
- Add userName param to buildSenderPrompt and thread it through all 3
  call sites (direct send, queued message drain, event-loop drain)
- Add userName field to queuedMessage so queued messages preserve the
  display name across turns
- Add tests for empty name fallback and names with spaces

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 20:50:00 +01:00
Claude d21cee5eb7 fix: pre-release fixes for high/medium priority issues from PR review
High priority:
- PR #420: fix Telegram reply context lost by alias resolution —
  resolveAlias now runs on user text before ExtraContent merge
- PR #567: fix message queue race — adoptPendingFromPlaceholder
  copies queued messages when placeholder state is replaced
- PR #437: harden /diff command — reject targets starting with '-',
  use '--' separator to prevent git option injection
- PR #560: add run_as_env dangerous variable blacklist (LD_PRELOAD,
  PATH, HOME, etc.); fix doc claiming PATH is in default allowlist

Medium priority:
- PR #497: ACP permission params log level Info→Debug to prevent
  sensitive data leaking; add toolInputByID cache size cap (1000)
- PR #559: observe new JSONL files start from EOF instead of offset 0,
  preventing full session history replay to Slack
- PR #510: add idempotent Close() test for claude session
- PR #514: bridge ReconstructReplyCtx already has structured JSON +
  backward compat; added decode helper + tests
- PR #440: add instance lock acquire/release unit tests
- PR #569: document ContinueSession behavioral change in code comments

All changes backward-compatible. Tests added for all fixes.

Made-with: Cursor
2026-04-12 20:40:02 +08:00
cg33andClaude 780a81af42 fix(core): filter /list and /switch to only show cc-connect owned sessions (#569)
External Claude CLI sessions in the same work_dir were appearing in /list,
/switch, /delete, and /search results because ListSessions() scans all
.jsonl files in the project directory without filtering.

- Add KnownAgentSessionIDs() to SessionManager to collect tracked agent IDs
- Add filterOwnedSessions() to exclude external CLI sessions from all
  session listing operations (9 call sites in engine.go)
- Remove dead --continue code path from claudecode/session.go (engine no
  longer passes ContinueSession sentinel)
- Add regression tests for the filtering logic

Closes #541

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 06:19:26 +08:00
cg33andClaude 4bb14d47d0 fix(core): queue messages during session startup instead of dropping (#565) (#567)
When a session lock is held and a new message arrives, the queue
mechanism requires an interactiveState entry to exist. Previously,
this entry was only created inside the async goroutine after the
agent process was spawned — a potentially slow operation. Messages
arriving during this startup window were silently dropped.

Fix:
- Create a placeholder interactiveState before launching the async
  processor so the queue entry point exists immediately
- Relax queueMessageForBusySession guard to allow queueing when
  agentSession is nil (startup phase), while still rejecting when
  the agent session was established but has died

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-12 06:19:20 +08:00
4cf879b2b9 feat: add run_as_user for per-project OS-user isolation (#560)
* feat: add run_as_user for per-project OS-user isolation

Implements #496. Adds an optional `run_as_user` field at the project
level that, when set, spawns the project's agent command under a
different Unix user via `sudo -n -iu <user> --`. This provides OS-level
file-system isolation from the supervisor user that runs cc-connect
itself: a misbehaving agent can no longer reach files outside the
target user's permission scope, even via python -c / shell redirection
/ other non-hooked code paths.

## What lands in this PR

### Config (config/)

- new fields `run_as_user *string` and `run_as_env []string` on each
  `[[projects]]` entry
- validation: rejects root, rejects invalid usernames, rejects use on
  Windows, enforces POSIX username charset to close a sudo argv
  injection vector

### Core primitives (core/runas*.go)

POSIX-only (Windows stubs provided for API compatibility).

- `BuildSpawnCommand(ctx, opts, name, args...)` — wraps exec.Cmd with
  `sudo -n -iu <user> --preserve-env=<allowlist> -- name args...` when
  opts.RunAsUser is set; passthrough otherwise
- `FilterEnvForSpawn(env, opts)` — strips supervisor env to a minimal
  allowlist (PATH, LANG, LC_*, TERM) plus any project-level extensions
- `VerifyRunAsUserCheap` — the two cheap sudo probes, suitable for
  per-spawn re-verification (catches sudoers edits between startup and
  spawn)
- `PreflightRunAsUser` — the three go/no-go startup gates from #496:
  (1) passwordless sudo to target works,
  (2) target cannot passwordless sudo (if escalation succeeds, captures
      `sudo -n -l` output in the error message), and
  (3) target can read AND write work_dir root. Plus a non-fatal
  descendant scan warning on unreadable/unwritable descendants with
  prune-paths, 10s timeout, and a 50-entry cap.
- `RunIsolationProbe` + embedded runas_probe.sh (//go:embed) — the
  leak-audit probe codex asked for: spawns a fixed shell script as the
  target user and parses its output into a structured IsolationReport.
  Reports effective id/groups/umask/pwd, target-home config presence,
  cross-user denial attempts for every other configured run_as_user,
  and supervisor-denial attempts. Never reads file contents, only
  existence and access.

All of these are testable without a real second user via an injectable
`SudoRunner` interface; production uses ExecSudoRunner.

### Failure policy

Per-spec: all checks are hard go/no-go, not warnings.

- Any preflight fatal in any project: cc-connect refuses to start
  entirely; no half-spawned processes.
- Any audit-level `CROSS_LEAKED` (target reads another project user's
  secrets) or `SUPERVISOR_LEAKED` (target reads the supervisor's
  secrets) or `WORKDIR_WRITABLE=no`: cc-connect refuses to start.
- Descendant-scan findings are warnings (inaccessibility inside a
  project tree is sometimes intentional).

### Startup wiring (cmd/cc-connect/)

- `runRunAsUserStartupChecks` runs preflight + isolation probe for
  every project with run_as_user set, in parallel, BEFORE any agent
  or engine is constructed. Logs every outcome via slog. On any fatal,
  prints the full list and `os.Exit(1)`.
- injects project-level run_as_user / run_as_env into the agent's
  options map before calling `core.CreateAgent`, so agents can opt in
  without new top-level plumbing.

### Doctor subcommand (cmd/cc-connect/doctor_runas.go)

New `cc-connect doctor user-isolation [--project NAME] [--out PATH]`
subcommand. Runs preflight + isolation probe for one or all
`run_as_user` projects, writes a JSON report per project, and prints a
human-readable summary. Default output is
`~/.cc-connect/audits/<timestamp>-<project>.json`.

Also supports `--print-script` to dump the embedded probe source for
inspection (and for running shellcheck against it).

### Claude Code wiring (agent/claudecode/)

- `Agent` struct gains `runAsUser` and `runAsEnv` fields
- `New()` parses them from the opts map and skips the
  `exec.LookPath("claude")` check when run_as_user is set (the target
  user's PATH is what matters, not the supervisor's)
- `newClaudeSession` takes `core.SpawnOptions`, runs the per-spawn
  cheap re-check immediately before building the command, uses
  `core.BuildSpawnCommand` instead of `exec.CommandContext`, and
  filters env via `core.FilterEnvForSpawn`

### Docs

- new "Running agents as a different Unix user" section in
  `docs/usage.md` with setup steps, sudoers example, ACL example,
  environment-propagation migration checklist (PGSSL*, ~/.claude/
  settings.json, ~/.pgpass, ~/keys/, ~/.ssh/, MCP binaries, shell
  profile, language toolchains), opt-out instructions, and
  failure-mode debug guide
- README section with a short configuration example and a pointer to
  the full docs
- `config.example.toml` commented example with setup recipe

### Tests

- config: parse run_as_user/run_as_env, reject root, reject invalid
  chars, Windows rejection, username validator valid/invalid cases
- core: BuildSpawnCommand legacy vs. sudo form, FilterEnvForSpawn
  filtering, VerifyRunAsUserCheap success/no-sudo/escalation-possible/
  empty-user, PreflightRunAsUser all-pass/no-sudo/escalation-fatal/
  workdir-fatal/descendant-warnings/descendant-cap, probe parser
  clean/cross-leak-fatal/supervisor-leak-fatal/workdir-not-writable-
  fatal, shell quoting, other-users filtering, embedded probe script
  sanity
- agent/claudecode: New() parses run_as_user/run_as_env, run_as_user
  skips LookPath
- cmd/cc-connect: default audit dir suffix, printHumanReport doesn't
  panic on populated report

Uses stubbed SudoRunners throughout — no tests require a real second
Unix user or real sudo binary.

## Scope

Only Claude Code is wired up in this PR. Each non-claudecode agent has
its own session/spawn lifecycle (codex re-spawns per turn, cursor has
its own state model, etc.) so the sweep needs per-agent reasoning and
is deferred to a follow-up. The core primitives land here so each
future agent migration is a small, self-contained PR.

Follow-up issue tracks: codex, cursor, gemini, iflow, opencode, qoder,
acp, pi.

Refs: #496
Credit: issue framing via user + codex adversarial review
        (go/no-go gates, leak-audit probe, env propagation docs,
        sudo -iu rationale pinning)

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

* refactor(run_as_user): simplify after code review

Addresses findings from a parallel code-reuse / code-quality / efficiency
review of #498. Behavior unchanged; tests still pass.

Highlights:

- cache VerifyRunAsUserCheap results for 30s keyed by runAsUser, so
  rapid-fire messages from the same Slack user don't pay the ~100ms
  per-spawn sudo tax. Cache evicts on failure so the next spawn
  re-verifies fresh. Adds ResetVerifyCache for tests + operators.
- doctor user-isolation now fans out across projects in parallel
  (mirroring runas_startup.go) instead of iterating serially. Per-project
  output is buffered and printed in order so the report stays grouped.
- collapse Agent.runAsUser + Agent.runAsEnv into a single
  core.SpawnOptions field — eliminates the reassemble-at-call-site
  pattern.
- fold core/runas_username.go into core/runas.go (one 17-line file →
  one helper near its only caller).
- IsolationReport.RawOutput is now only populated when the audit had a
  fatal — clean reports are smaller on disk.
- introduce typed string constants for the probe wire format
  (tagBegin / tagWorkDirReadable / tagCrossLeaked / ...) so the parser
  and runas_probe.sh share a discoverable contract.
- itoa() in tests → strconv.Itoa
- contains() helper in tests → slices.Contains
- mass comment trim across runas.go / runas_check.go / runas_audit.go:
  delete WHAT-restates-name comments (~70 lines) while keeping the
  load-bearing WHY blocks (sudo -n -iu rationale, env propagation
  reasoning, find expression format note, scan timeout justification).

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

* fix(runas): drop PATH from default env allowlist

Preserving PATH across the sudo boundary defeats the whole point of
-i: it overrides the target user's login-profile PATH with the
supervisor's, and leaks supervisor work directories (e.g. the
cc-connect source tree on the supervisor's PATH) into the isolated
session.

sudo -i now rebuilds PATH from /etc/profile + the target user's
~/.profile. Binaries the isolated agent needs (like the claude CLI)
should be installed system-wide (e.g. /usr/local/bin symlinks) or put
on the target user's own shell profile.

Caught by Q6/Q8 daemon-spawn verification in data-worklog Phase 0a:
partseeker-coder sessions were running with leigh's PATH intact.

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

* fix(engine): propagate run_as_user to per-workspace agents

In multi-workspace mode, getOrCreateWorkspaceAgent constructs a fresh
opts map for each per-workspace agent instead of inheriting the
project-level opts that main.go decorated at startup. As a result,
run_as_user (and run_as_env) silently dropped out of the per-workspace
spawn path — the project-level config said partseeker-coder but every
spawn actually ran as the supervisor.

Fix: expose GetRunAsUser/GetRunAsEnv accessors on *claudecode.Agent
and copy them across in getOrCreateWorkspaceAgent via interface
assertion, mirroring the existing GetModel/GetMode pattern. Two
regression tests cover the propagation and the no-propagation case
so multi-workspace deployments that don't use run_as_user are
unaffected.

Caught by Q6/Q8 daemon-spawn verification in data-worklog Phase 0a.

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

* docs: add OAuth credential sharing guidance for run_as_user setups

OAuth access tokens expire after a few hours and only get refreshed by
active Claude CLI sessions. When the target user has no active session
between cc-connect spawns, its token expires and auth fails with 401.

Document the symlink + ACL pattern: target user's credentials.json
symlinks to the supervisor's copy so both share one auto-refreshed token.

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

* fix: graceful session shutdown — close stdin before signals

The old Close() called context.Cancel() which sends SIGKILL immediately
via Go's exec.CommandContext, giving Claude Code zero time to run Stop
hooks. This broke claude-mem session summaries (120s timeout) and any
other cleanup that depends on a clean exit.

New three-phase shutdown:
1. Close stdin (EOF) — Claude Code exits cleanly, running Stop hooks
2. SIGTERM after 30s grace period if still alive
3. SIGKILL after 5s more as last resort

Also increases engine-level close timeout from 10s to 45s to accommodate
the longer graceful window.

Tested: all agent/claudecode and core tests pass. Verified Claude Code
exits cleanly on stdin EOF (exit code 0).

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

* fix: 120s graceful close + hold message during session transition

Increases the default graceful stop timeout from 30s to 120s to match
claude-mem's Stop hook timeout. The wait ends as soon as the process
exits — typical shutdowns take seconds, not the full 120s.

When a user messages during an idle-reset that triggers a graceful
close, they now see "Wrapping up your previous session (usually a few
seconds, up to 2 minutes). Your new session will start automatically."
instead of a silent delay. The hold message is sent before the blocking
close, and the new session starts immediately when the old one finishes.

Engine-level close timeout raised to 130s (120s grace + 5s SIGTERM +
5s buffer).

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

---------

Co-authored-by: Leigh Stillard <leigh@stillard.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 19:45:08 +08:00