575 Commits
Author SHA1 Message Date
cg33 930bb38415 Merge pull request #1661 from hi-neason/fix/provider-persist-errors
fix(core): surface provider persistence errors in management API
2026-10-25 05:00:26 +08:00
Lu ZOU 3e33c1b5fd fix(progress): recover card updates after transient failures 2026-10-23 03:02:37 -07:00
cg33 77e8bc7519 Merge pull request #895 from kevinWangSheng/fix/cron-session-mode-reuse-override
fix(cron): preserve explicit session_mode=reuse override over global default
2026-10-23 15:00:13 +08:00
cg33 6e8b481d9e Merge pull request #1861 from TongLing916/fix/pi-ps-streaming
fix(pi): deliver and acknowledge /ps and /btw RPC supplements
2026-10-23 14:59:50 +08:00
cg33 cd4a01256f Merge pull request #1041 from kevinWangSheng/fix/observer-advance-offset-on-scanner-error
fix(core/observer): advance tail offset on scanner error to prevent re-forwarding
2026-10-23 14:59:26 +08:00
cg33 a0abb8f6f0 Merge pull request #1049 from happyTonakai/fix/shell-no-args
fix(shell): show usage when /shell is called with no arguments
2026-10-23 14:59:02 +08:00
cg33 2c891e776b Merge pull request #1979 from xxb/fix/relay-full-response-upstream
fix(relay): preserve full response content in group echoes
2026-10-23 14:58:39 +08:00
cg33 04f946bc2e Merge pull request #2028 from kongh2021/fix/relay-timeout-tool-output
fix(core): stop the relay timeout path returning the target agent's tool output
2026-10-22 12:12:04 +08:00
cg33 cf636555ca Merge pull request #2048 from AaronZ345/codex/fix-codex-async-user-input
fix(codex): surface async questions and route user answers
2026-10-22 12:10:52 +08:00
cg33 b1210ee305 Merge pull request #2053 from WhyTan00/fix/reject-ambiguous-ccswitch-import-20261017
fix: reject ambiguous CC-Switch provider imports
2026-10-22 12:10:02 +08:00
cg33 85f271c171 Merge pull request #2054 from SunflowersLwtech/feat/claudecode-model-fallback-notice
feat(claudecode): tell the user when safeguards switch the session model
2026-10-22 12:09:39 +08:00
Tong Ling bbef5cbb59 Prevent Pi task supplements from being dropped during RPC turns
Pi rejects plain prompt commands while streaming unless streamingBehavior
is specified. Send prompts with streamingBehavior=steer so /ps and /btw
supplement an active run and still work across the remote turn-end boundary.
Keep in-flight attachments when a text-only supplement has no message ID.

Add protocol, subprocess, and user-journey regressions with generic fixtures,
and document Pi RPC behavior without changing the shared Send contract.

Constraint: Pi raw steer queues messages without starting an idle agent
Rejected: Core steering interface and raw steer RPC | unnecessary shared API and late-turn delivery gaps
Confidence: high
Scope-risk: narrow
Directive: Keep streamingBehavior inside the Pi adapter and preserve attachments on text-only sends
Tested: Regression failures on unmodified main; go build ./...; go vet ./...; incremental golangci-lint; full Pi race tests; core /ps and /btw race tests; complete CUJ suite
Not-tested: Live provider round trip; broad suites hit existing macOS launchd-status and CUJ A5 TempDir cleanup failures, both reproduced on main 757b4df0
2026-10-22 09:30:14 +08:00
cg33 725b9ef373 Merge pull request #2061 from SunflowersLwtech/fix/askq-stale-option-taps
fix(core): drop AskUserQuestion option taps that no longer match a question
2026-10-22 03:03:50 +08:00
cg33 1726296b89 Merge pull request #2069 from qvictl/fix/claudecode-resume-task-notification-result
fix(claudecode): drop empty task-notification result on --resume; flush partial unsolicited text on cancel
2026-10-22 03:03:04 +08:00
xinyuan0801 f690f0d7f3 test(core): wait for manual cron persistence before cleanup 2026-10-21 16:13:20 +08:00
xinyuan0801 63299ef46d fix(core): log compression turn completion before queued turns 2026-10-21 15:34:06 +08:00
qvictlandClaude Opus 5.5 0bfbd23ddd fix(core): keep flushed unsolicited partial ordered before next turn
flushPartial sends the flushed partial text from a goroutine, so after
stopUnsolicitedReader returned the next foreground turn's reply could
overtake or interleave with the flushed chunks.

Publish a flush-done channel on interactiveState. The reader closes it
when the flush sender goroutine finishes (or on exit when nothing was
flushed), and stopUnsolicitedReader waits on it within the same
unsolicitedReaderStopTimeout budget it already uses for the reader
exit. On timeout it logs and continues; no Events resync is forced
since the reader has already exited. interactiveMu is never waited on
beyond the existing bound.

Add TestUnsolicitedReader_FlushOrderedBeforeNextTurn (slow platform,
multi-chunk flush, asserts exact send order) and
TestUnsolicitedReader_FlushWaitIsBounded, and update the safety note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 23:58:07 -06:00
qvictl 0fc8330440 test: check e.Stop error in unsolicited flush test (errcheck) 2026-10-20 22:51:44 -06:00
qvictlandClaude Opus 5.5 80bdf3041d fix(claudecode): drop empty task-notification result on --resume; flush partial unsolicited text on cancel
When a Claude Code session is resumed after the previous process exited
with a background shell task still running, the CLI first replays a task
notification ("Background shell command didn't finish before the previous
session ended") and closes it with a synthetic result: origin.kind ==
"task-notification", num_turns == 0, result == "". That result arrives
after cc-connect has written the user's prompt to stdin, so the engine
finished the user's turn with "(empty response)" and the real reply was
pushed into the unsolicited relay.

- agent/claudecode: drop result events that close an empty, model-less
  task-notification turn; they carry nothing to deliver.
- core: when the unsolicited reader is cancelled (or the agent channel
  closes) mid-turn, relay the text it already accumulated instead of
  silently discarding it.

Refs #1877

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:33:18 -06:00
LiuWei 0a4b461726 fix(core): drop AskUserQuestion option taps that no longer match a question
Platforms forward an AskUserQuestion option tap to the engine as a plain
"askq:<q>:<opt>" message. When that tap arrives after the question was
already answered (for example by a chat message), there is no pending
question, so the tap fell through to the normal message path: on a busy
session it was queued ("Message received — will process after the current
task finishes") and later sent to the agent as the literal text
"askq:0:2". A tap while a tool permission was pending got the
allow/deny hint, and a tap on an earlier question's card in a
multi-question prompt answered the current question with the wrong option.

The engine now recognises option taps and, when they do not match the
question being asked, tells the user the question is no longer active
instead of passing the tap on. Free text that merely starts with "askq:"
is unaffected.
2026-10-19 16:26:37 +08:00
LiuWei 582267bbed feat(claudecode): tell the user when safeguards switch the session model
When the primary model's safety classifiers decline a Claude Code request,
the CLI re-runs it on a fallback model and keeps the rest of the session
there (claude-opus-5-5 -> claude-opus-4-8 for the cyber category). It
reports this on stream-json as a system event with subtype
model_refusal_fallback, which cc-connect ignored: IM users got answers from
the fallback model with no notice, and the reply footer kept naming the
model from the init event.

- claudecode: parse model_refusal_fallback. For scope "session", or no
  scope from older CLIs, move the session's active model to the fallback
  model and drop the cached context window so the next result event
  reports the fallback model's own. Emit core.EventModelSwitch once per
  refused request and target, since the CLI can announce one fallback
  twice (provisional, then final).
- core: add EventModelSwitch carrying a ModelSwitch. The interactive and
  unsolicited event loops send a localized notice naming both models, the
  refusal category and the /model command that switches back.
2026-10-18 18:35:36 +08:00
Whytan b76abd6840 fix: localize ambiguous CC-Switch import errors 2026-10-18 12:26:50 +09:00
Whytan c7fbedc13d fix: reject ambiguous CC-Switch provider imports 2026-10-18 05:17:10 +09:00
zhangyu.34 314829adc0 test(core): check async question engine cleanup errors 2026-10-17 17:35:36 +08:00
zhangyu.34 0d4376e674 fix(codex): deliver async questions and steer user answers 2026-10-17 17:35:36 +08:00
cg33 2dec7173bd Merge pull request #1917 from rainyMorning2/fix/codex-large-history
fix(codex): read large session history records reliably
2026-10-16 16:57:25 +08:00
cg33 512c140698 Merge pull request #2026 from SunflowersLwtech/fix/idle-reset-queued-messages
fix(core): count queued user messages toward reset_on_idle_mins
2026-10-16 14:39:03 +08:00
kongh2021 5ae7b24fe3 fix(core): stop the relay timeout path returning the target agent's tool output
HandleRelay accumulated every EventToolResult into textParts, and the timeout
path hands textParts back to the caller verbatim. A relay whose target agent
ran past the deadline therefore returned its whole tool log as if it were the
answer: measured at 11932 chars in a single timed-out relay, against 165 to
2034 for turns that finished normally. That blob then sat in the caller's
context and was re-billed on every later turn of its session — the token cost
two /bind-ed bots were paying for nothing.

Tool output is intermediate state, not an answer, so it no longer reaches
textParts. The normal path is unaffected either way: both agent/claudecode and
agent/cursor populate EventResult.Content, so resp is the final answer and
textParts gets discarded.

The timeout path also stops returning a bare truncated fragment as a success.
relayPartialResponseOrError now trims to 2000 runes and wraps the fragment in
an explicit notice (MsgRelayPartial, all five languages), because a silent
fragment is indistinguishable from a real answer — the caller had no way to
tell it had been cut off. It becomes a method on *Engine to reach i18n.

TestHandleRelay_TimeoutExcludesToolResults is the regression test: a tool
result carrying a sentinel is not relayed, assistant text still is.
TestHandleRelay_ReturnsPartialOnTimeout keeps asserting the partial text
survives, now through the notice.
2026-10-15 22:45:35 +08:00
LiuWei 961655de14 fix(core): count queued user messages toward reset_on_idle_mins 2026-10-15 16:29:45 +08:00
82030615 8804501df8 fix(core): stop clearing history on card-driven /switch
The /list card button routes through executeCardAction instead of cmdSwitch,
and only the former cleared the session history after switching. cmdSwitch
documents why that must not happen: SwitchToAgentSession returns the existing
Session when the agent session ID is known, so wiping History makes /status
and /history report zero messages right after a successful switch. When no
prior session matches, the new Session already has nil History, so leaving it
alone is a no-op.

Reproduced as: /list shows a session with 97 messages, selecting it switches
successfully, then /status reports "messages: 0". Typing "/switch 1" instead of
tapping the row kept the history, so the behaviour depended on how the switch
was invoked.
2026-10-15 11:14:35 +08:00
cg33 7f4f2f5073 Merge pull request #1975 from timmyagentic/codex/preserve-reference-content
fix(core): preserve numbers, commands, and distinct references
2026-10-14 12:40:55 +08:00
82030615 4069baf1aa fix(core): keep workspace bindings scoped to the platform's thread
extractWorkspaceChannelKey rebuilt a workspace binding key from the
sessionKey alone. extractChannelID uses SplitN(key, ":", 4), so a Feishu
thread key "feishu:<chatID>:root:<rootID>" yielded just "<chatID>" and
every thread in a chat collapsed onto one binding. /status then reported
".", /list came back empty, /dir showed another conversation's directory,
and /new resolved an interactive key without the workspace prefix, so
cleanupInteractiveState missed the real state and the agent process leaked.

Platforms own their key layout, and core must not hardcode private markers
such as "root:", so this adds an optional ChannelScopedSessionKey interface
and asks the originating platform first. Feishu implements it by reusing
parseThreadRootID, matching what populateWorkspaceChannelKeys already
derives. Platforms without per-thread scoping implement nothing and keep
the previous behaviour, so this is purely additive: no interface signature
changes, and no other platform needs a line edited.

Two follow-on fixes the same key mismatch exposed:

- sessionContextForKey read the binding directly, but multi-workspace /dir
  records a per-interactive-key override rather than rebinding the channel,
  and only workspaceContext applies it. Resolving through workspaceContext
  keeps /status and /dir consistent with the directory the agent uses.

- The SessionManager is per-workspace (getOrCreateWorkspaceAgent), so topics
  sharing a directory shared every session in it. /list offered other
  topics' sessions and /switch could resume one. applySessionFilter now
  drops sessions owned by another sessionKey while keeping unowned ones,
  which are CLI-created sessions that have no conversation to belong to.

applySessionFilter is inert outside multi-workspace mode, and the tests
cover both that and the unclaimed-key fallbacks.

Refs #193, which adds a separate /path command and leaves /dir global.
2026-10-13 14:51:39 +08:00
VPS 2b7251b2f7 fix(relay): preserve full response content in group echoes 2026-10-08 14:40:22 +00:00
Timmy ZHOU dfce000009 fix(core): preserve numbers, commands, and distinct references 2026-10-07 21:39:52 +08:00
zhen dai 7c7c0e8a4a Revert "test(core): wait for cron persistence before cleanup"
This reverts commit d405d5973b.
2026-09-29 14:24:50 +08:00
zhen dai d405d5973b test(core): wait for cron persistence before cleanup 2026-09-29 11:47:51 +08:00
Niko WuandClaude Code 1d38af3fe3 fix(core): derive workspace session store from the engine store path (#1926)
getOrCreateWorkspaceAgent built the per-workspace session file as
filepath.Join(filepath.Dir(e.sessions.StorePath()), ...). With no store
path configured — which is how every engine in the test suite is built —
filepath.Dir("") is ".", so the path collapsed to a bare relative
"<engine>_ws_<hash>.json" and the session manager wrote it into the
process working directory. That is what littered core/ with test_ws_*.json
during `go test ./core/`.

An empty store path already means "no persistence" everywhere else in the
session manager (saveLocked returns early), so it now means the same here:
the workspace manager is built unpersisted instead of inventing a path.
A configured store path still derives the workspace file beside it, as
before.

Regressions: an engine without a store path yields a workspace manager
whose StorePath() is empty, and a configured one still derives
test_ws_<hash>.json in the store's directory.

Also ignores the stale files contributors already have on disk.

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-29 10:40:53 +08:00
f8acb2c6c6 fix(cron): render CRON_TZ schedules in their own timezone in /cron output (#1922)
robfig/cron accepts a leading CRON_TZ=<zone> (or TZ=<zone>) field, and
cc-connect stores such expressions as-is. Two display bugs followed:

- CronExprToHuman only understood 5 fields, so a CRON_TZ job fell back to
  echoing the raw expression instead of "Daily at 15:00".
- renderCronCard and cmdCronList formatted next/last run with the process
  local zone, so "CRON_TZ=Asia/Shanghai 0 15 * * *" showed up as 03:00 on
  a UTC-4 host with no hint that the job was pinned to another zone.

Strip the timezone field before humanizing, resolve the schedule's
*cron.SpecSchedule location, format next/last run in that location and
append the zone name to the human-readable schedule when it differs from
local time. Plain 5-field expressions render exactly as before.

Regression tests: TestCronExprToHuman_TimezonePrefix and
TestCronDisplay_UsesScheduleTimezone fail on the pre-fix code and pass
after the fix.

Co-authored-by: claw <claw@clawdeMac-mini.local>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-29 10:40:31 +08:00
zhen dai dac6160362 fix(codex): surface corrupt history at user entry points 2026-09-28 23:49:00 +08:00
shalk(xiao kun)andClaude Sonnet 5 dd93245b3d feat(web): manage multi-workspace mode and channel bindings from the dashboard (#1772)
The multi-workspace feature (per-project base_dir + channel-to-directory
routing) was previously only configurable via chat slash commands or by
hand-editing config.toml. Expose it through the management REST API
(GET/PATCH workspace_mode+workspace_base_dir, GET/POST/DELETE
/projects/{name}/workspaces) and a new "Workspaces" tab in the project
detail page, so operators can toggle the mode, set base_dir, and
bind/unbind channels entirely from the web dashboard.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 20:48:31 +08:00
Yu Zhang 82d8d5f1d8 feat(codex): extend GPT-5.6 reasoning and fallback support (#1815)
* feat(codex): support GPT-5.6 reasoning levels

* fix(codex): show all reasoning levels in usage

* fix(codex): use upstream dynamic model filter

* test(codex): drop duplicate GPT-5.6 API coverage
2026-09-27 21:55:13 +08:00
cg33 fc49da96dc fix(core): render Feishu AskUserQuestion as flat action rows (fixes #1658) (#1679)
The single-select AskUserQuestion card was rendered as a column_set
row per option, with the option description in a weighted column and
the click button in an 'auto' column. On Feishu mobile the button
column was clipped or non-interactive, and even on desktop the
column_set > column > button pattern was reported as unreliable —
Po1nt9's log during the 08:00-08:25 test window showed zero
card-action entries for ask_user_question cards while permission
cards (which use a flat action row, tag:"action") dispatched
normally.

Mirror the permission-card layout: each option becomes a markdown
description followed by a flat CardActions row with a single button.
The askq:qIdx:optIdx value plus askq_label/askq_question extras are
preserved so the Feishu callback handler still renders the
post-answer card. The Note line that already nudges users toward
numeric / text fallbacks stays in place.

Sibling fix to PR #1677 (issue #1659, pi 0.84.0+ tool-call drop) —
both reporters run pi through cc-connect on Feishu.
2026-09-27 21:01:25 +08:00
lingion 922e64ab11 chore(core): 对齐 stale-lock 测试注释 (#1883)
统一两条时间戳测试注释的空格对齐,保持新增测试文件通过 gofmt。\n\nverify: go test ./core -run TestStaleLockDumpNamePadding\|TestPruneStaleLockDumps\|TestPruneStaleLockDumpsNoOps\|TestDumpStaleLockStacksEndToEnd\|TestSession_TryLockUnlock\|TestSession_Busy\$\|TestSession_LateUnlockDropped\|TestSession_BreakStaleLockThreshold\|TestSession_ForceUnlock -count=1 (ok); go vet ./core ./cmd/... ./agent/claudecode (ok); git diff --check (ok)
2026-09-27 19:30:39 +08:00
刘朝 Zhao Liu e0b45eaa1f fix(qqbot): forward quoted image attachments to agents (#1806) 2026-09-27 19:12:23 +08:00
hi-neasonandClaude 73ea597230 test(core): cover DirHistory move-to-front, trim, persistence, bounds (#1663)
DirHistory's exported methods (Add, List, Get, Previous, Contains,
SetMaxSize) had no direct tests; engine tests only used NewDirHistory
as setup. The methods carry non-trivial logic — move-to-front dedup,
maxSize trimming, 1-based Get with bounds check, Previous at index 2,
defensive-copy List, JSON persistence across instances — so a
regression in any of them would have gone unnoticed.

Add table-driven tests covering each behavior plus the corrupt-file
load path.

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-27 18:00:27 +08:00
3ecaf7a58c test: fix flaky t.TempDir cleanup race in media pipeline and cron tests (#1911)
TestQueuedMessagePreservesFiles returned as soon as the queued prompt reached
the agent, while the still-running turn goroutine persisted the session store
under t.TempDir(). TestHandleCronExec_TriggersJob returned as soon as the cron
run's messages were sent, before the background goroutine's store.MarkRun save.
Both writes raced t.TempDir RemoveAll ("directory not empty"). Wait for the
queued turn's reply / for MarkRun to be persisted before returning. Test-side
only; assertions unchanged.

Co-authored-by: wen_guoxing <wen_guoxing@itrus.com.cn>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-27 17:56:39 +08:00
aaronandaaron.yang 22ef60fe2e fix(core): invalidate stale workspace sessions (#1699) (#1699)
Co-authored-by: aaron.yang <aaron.yang@idreamsky.com>
2026-09-27 14:23:39 +08:00
sundaymangoandsunday 4127c3d330 fix(core): persist late session ID on turn error (#1901)
Co-authored-by: sunday <sunda@protonbase.io>
2026-09-27 14:23:08 +08:00
hi-neasonandClaude 44d2808264 fix(core): lock CommandRegistry.agentDirs in SetAgentDirs and Resolve (#1662)
SetAgentDirs wrote r.agentDirs with no lock held, while Resolve read the
same field after releasing r.mu.RUnlock(), even though ListAll accessed
it under RLock. Today the write happens at engine construction and does
not overlap with Resolve, but the inconsistent locking meant any future
runtime re-bind raced on the slice header under -race.

- Take r.mu.Lock in SetAgentDirs and copy the incoming slice so later
  caller mutations don't leak into the registry.
- Snapshot agentDirs under RLock in Resolve before doing file IO, so
  the lock is not held across disk reads but the slice header is read
  safely.
- Add a -race regression test that concurrently swaps agent dirs while
  other goroutines resolve config and agent commands.

Co-authored-by: Claude <noreply@anthropic.com>
2026-09-27 14:22:21 +08:00
LING Tong a5c93d9ce1 fix(core): never write temp files in target directory (fixes t.TempDir cleanup) (#1777)
AtomicWriteFile now always writes the initial temp file to the system
temp directory (os.TempDir()), avoiding orphan .tmp-* files in the
target directory when background goroutines (e.g. session manager's
saveLocked) call it concurrently during tests.

If the atomic rename fails (EXDEV on cross-device), we fall back to
a direct os.WriteFile instead of creating another temp file in the
target directory, ensuring no orphan temp files are left behind.
2026-09-21 11:50:44 +08:00