Commit Graph
9801 Commits
Author SHA1 Message Date
can1357 f62526f111 fix(coding-agent): preserve VCS cache refresh semantics
(cherry picked from commit 3f3e475109deb7b9e7690720827876fc982b5b8d)
2026-07-29 23:08:59 +02:00
can1357 1d41b269f7 Merge PR #6997: perf(coding-agent): keep reftable branch resolution off the render path (@metaphorics) 2026-07-29 23:08:59 +02:00
robomp-botandcan1357 65707c7f4c fix(coding-agent): close VCS cache lifecycle gaps
(cherry picked from commit b88851334b07eb1da6743b51b542ea5d3222ccca)
2026-07-29 23:08:58 +02:00
robomp-botandcan1357 c5b0348150 fix(coding-agent): harden async reftable resolution
(cherry picked from commit e451f1d6f3537d4b58dfe479a6daf8919d169397)
2026-07-29 23:08:58 +02:00
robomp-botandcan1357 d966b3f8a0 perf(coding-agent): keep reftable branch resolution off the render path
(cherry picked from commit 5724c30ff66e2036ed03a2ce3514a9237a72a657)
2026-07-29 23:08:58 +02:00
can1357 650a8f0faf fix(collab): detach reconciled loader on idle
(cherry picked from commit d675de815644a5b02358fa90d83e5cd5c78141d2)
2026-07-29 23:08:57 +02:00
can1357 872a931795 Merge PR #6996: fix(collab): start the guest loader when the host reports streaming (@metaphorics) 2026-07-29 23:08:57 +02:00
robomp-botandcan1357 4557a4bb3d fix(collab): preserve maintenance loaders during state reconciliation
(cherry picked from commit ad611afa4885a71bbf8e8ad6c00328fb0be37d35)
2026-07-29 23:08:56 +02:00
robomp-botandcan1357 d6dc8f5d14 fix(collab): start the guest loader when the host reports streaming
(cherry picked from commit 0bc0e38d865815b70c848bd00be3dc1fcd228ba4)
2026-07-29 23:08:56 +02:00
can1357 eb8f3e6c2c Merge PR #6993: fix(coding-agent): require web_search_call in codex search (@roboomp) 2026-07-29 23:08:55 +02:00
roboompandcan1357 e5ea31b22c fix(coding-agent): require web_search_call in codex search
GPT-5.6 Responses-Lite models receive tool_choice "auto" (the forced
hosted choice is invalid under the lite shape, #5771/#5772), so the model
may answer without invoking the hosted web_search tool. The codex search
parser accepted any non-empty answer, returning a stale completion with
zero sources as a successful search.

callCodexSearch now tracks response.web_search_call.* events (and
web_search_call output items) and throws CodexNoWebSearchError when none
occurred. The candidate chain treats that error as retryable, advancing
default lite models to a non-lite model that forces web_search, and
surfaces a clear failure when the model was explicitly configured.

Fixes #6988

(cherry picked from commit a276cd0b3df1d0d041faf0a63fabcbb884e36a91)
2026-07-29 23:08:54 +02:00
can1357 832d4f1506 Merge PR #6987: fix(coding-agent): treat streamed visible text as replay-unsafe in turn recovery (@metaphorics) 2026-07-29 23:08:51 +02:00
metaphoricsandcan1357 93819dbec4 fix(coding-agent): gate refusal retries on replay safety
(cherry picked from commit 531b25beffa2096c76adff6692f3697e649f0e95)
2026-07-29 23:08:51 +02:00
metaphoricsandcan1357 9733d13827 fix(coding-agent): make replay safety authoritative
(cherry picked from commit 118ecabb22f406afbd56864ff0fbafc8f4f1fa4c)
2026-07-29 23:08:50 +02:00
metaphoricsandcan1357 c806a9b482 fix(coding-agent): guard Fireworks fallback after visible output
- reuse the centralized replay-unsafe predicate in Fast fallback
- exercise visible-text and replay-safe Fireworks branches

(cherry picked from commit f596392b891bb8ec82171a25c6651ed3cc8e0d61)
2026-07-29 23:08:50 +02:00
metaphoricsandcan1357 b5602ddfc1 fix(coding-agent): treat streamed visible text as replay-unsafe in turn recovery
(cherry picked from commit 3aeac1a4afa756e9c7d579246d8f334cd0731cda)
2026-07-29 23:08:50 +02:00
can1357 4bad9b481d Merge PR #6973: fix(coding-agent): share parent local:// root with /tan clone (@roboomp) 2026-07-29 23:08:44 +02:00
roboompandcan1357 bbf3d78c7e fix(coding-agent): keyed tan local root on session-manager id
Snapshot this.ctx.sessionManager.getSessionId() for the tan clone's local://
mapping instead of session.sessionId. The two diverge after /fresh or a
provider session override, and the Windows short-root fallback keys
%TEMP%/omp-local/<id> off the session-manager id used by the parent's
large-paste writes and '/data/workspaces/can1357__oh-my-pi__6971/.omp-session/2026-07-29T06-08-45-283Z_019fac7d-5ee3-7000-a7aa-16fe9394fdc9/local' reads, so the mismatched id left attachments
unreachable.

Diverge the mocked session id from the manager id in the regression test so
it pins the session-manager id.

Fixes #6971

(cherry picked from commit 1efcd22326d76fdb8b50c0977a836c892e80ab76)
2026-07-29 23:08:44 +02:00
roboompandcan1357 dd4985f31e fix(coding-agent): scoped subagent local root overrides
Keep subagent localProtocolOptions on their ToolSession instead of installing
them as the process-global LocalProtocolHandler override. No-context URL
consumers therefore retain the active top-level session's mapping while tan
and task subagents continue to resolve through their caller context.

Add SDK regression coverage proving subagent creation preserves an existing
global mapping.

Fixes #6971

(cherry picked from commit a02eef174b03036dc960c842a0901d22333ad9cd)
2026-07-29 23:08:44 +02:00
roboompandcan1357 fb4393ae16 fix(coding-agent): snapshotted tan parent local root
Capture the parent artifacts directory and session ID when /tan dispatches
instead of resolving them through the mutable interactive SessionManager.
This keeps background tan '/data/workspaces/can1357__oh-my-pi__6971/.omp-session/2026-07-29T06-08-45-283Z_019fac7d-5ee3-7000-a7aa-16fe9394fdc9/local' reads pinned to the dispatching transcript
after the user switches or resumes another session.

Extend the regression test to switch the mocked interactive session before
the background job starts and assert the original local mapping is retained.

Fixes #6971

(cherry picked from commit e05db428eabd5087e4b2b4a462f47927c0628e72)
2026-07-29 23:08:43 +02:00
roboompandcan1357 d933cfbe05 fix(coding-agent): shared parent local root with tan clone
TanCommandController.start nests the tan clone at
<parent-artifacts>/Tan-<id>.jsonl, so the clone's session manager derived
its own artifacts dir and hence local root <parent-artifacts>/Tan-<id>/local.
Its sdk.createAgentSession call omitted localProtocolOptions, unlike the
task-subagent path which inherits the parent's mapping, so parent-session
'/data/workspaces/can1357__oh-my-pi__6971/.omp-session/2026-07-29T06-08-45-283Z_019fac7d-5ee3-7000-a7aa-16fe9394fdc9/local' attachments (pasted files, generated references) were unreadable.

Thread the parent session manager's localProtocolOptions into the tan clone
so local:// resolves against <parent-artifacts>/local.

Fixes #6971

(cherry picked from commit 1ded46e182fc24f9f57d8e9a907aaad58f790783)
2026-07-29 23:08:43 +02:00
can1357 756e872f64 Merge PR #6946: fix(tui): nest usage metrics in grouped reads (@joshrzemien) 2026-07-29 23:08:42 +02:00
joshrzemienandcan1357 daf1557b3d fix(tui): seal mixed read groups before usage
(cherry picked from commit 576b950b80583d8fd028edd08967b537f4eb18f0)
2026-07-29 23:08:42 +02:00
joshrzemienandcan1357 57cb290fef fix(tui): preserve grouped read request order
(cherry picked from commit eb6767e3248ad0fe3ea371e5226a4af6e0b8f97b)
2026-07-29 23:08:42 +02:00
joshrzemienandcan1357 a3e90b3d95 fix(tui): nest usage metrics in read groups
(cherry picked from commit 101268adc632e80bb3f8304b95497ac7bd884131)
2026-07-29 23:08:41 +02:00
can1357 4cc259f6cf Merge PR #6934: fix(hashline,coding-agent): key snapshot tag on actually-persisted content after ACP bridge writes (@marton78) 2026-07-29 23:08:39 +02:00
can1357 d2283dd301 fix(model-registry): isolate modifier record mutations
(cherry picked from commit 3941103443576a6c8ab91a52e7da4ba1d8521d69)
2026-07-29 23:08:38 +02:00
can1357 75b77c08b5 Merge PR #6930: fix(model-registry): preserve oauth.modifyModels projection across reloads (@abhishekbiyala) 2026-07-29 23:08:38 +02:00
Márton Danóczyandcan1357 7708f372b5 fix(hashline,coding-agent): key snapshot tag on actually-persisted content after ACP bridge writes
Root cause of the reported "edit tool silently reformats the whole
file" corruption: fs/write_text_file has no verbatim guarantee. When
an ACP client (e.g. Zed with format_on_save: on) reformats a buffer
on save, routeWriteThroughBridge reported the pre-write content as
successfully written, and Patcher.commit keyed the returned snapshot
tag on that same pre-write text instead of what actually landed on
disk. The next edit anchored on that tag then resolved hunks against
a baseline the file had already drifted away from, which is what
produced whole-file "corruption" from single-line hunks -- reproduced
live in this session against real Swift/JSON/TypeScript files with
Zed as the ACP client.

- routeWriteThroughBridge reads the file back after the bridge write
  and returns the verified content plus a drift flag (best-effort:
  ACP defines no ordering between the client acking the write and its
  own async format-on-save settling, so this degrades gracefully to
  the old stale-tag-on-next-read failure mode, never to corruption).
- HashlineFilesystem.writeText propagates that verified content in
  view-space (the same space readText returns -- e.g. a notebook's
  editable cell text, not its raw JSON), not storage-space, so tag
  validation on the next edit compares like with like.
- Patcher.commit keys fileHash/header/snapshot on the verified
  post-write content (normalized, so BOM/line-ending restoration never
  produces a false "drift") when it diverges from what was sent, and
  appends a warning naming the drift -- but deliberately leaves the
  returned `after` (and therefore the model-visible diff) scoped to
  the intended hunk. Diffing against the full drifted file would
  balloon the tool response to span every reformatted line (measured
  ~6.8x inflation on a 245-line file with one touched line); the
  warning is the correct O(1) channel for "your editor reformatted
  this," not an O(file-size) diff.
- write.ts keys its own snapshot header on the verified bridge content
  too (no diff-size concern there since write always replaces the
  whole file).

Caught via code review (dispatched against the first pass of this
fix): a naive "just use the verified content everywhere" fix broke
.ipynb editing outright (write-space vs read-space content mismatch,
tag invalid on every notebook edit) and would have inflated every
drifted edit response by ~6.8x. Both are now covered by regression
tests that fail against the pre-fix code and pass against this one.

(cherry picked from commit 35ab80e43be5800b2f48728e4400eb9fd7f7f7d2)
2026-07-29 23:08:38 +02:00
Abhishek Sharmaandcan1357 c7a113c9cb refactor(model-registry): derive projected catalog from an unprojected snapshot
The previous commits patched each rebuild path individually to avoid feeding a
modifyModels hook its own output. That left the invariant implicit and the
provider-scoped path applying only a subset of hooks, which is wrong for a hook
that inspects or suppresses another provider's models.

Keep #unprojectedModels as the canonical pre-projection catalog and derive
#models from it at every mutation point, so projections are always a pure
function of the unprojected base:

- #composeUnprojectedStaticModels builds the catalog; #composeStaticModels
  projects it. A scoped lookup with modifiers registered composes and projects
  the whole catalog before narrowing, matching getAll() followed by a filter.
  Providers without modifiers keep the cheap filtered path.
- Discovery completion, registerProvider, and runtime transport overrides
  update the unprojected snapshot and reproject, instead of mutating an
  already-projected array.
- Runtime metadata patches apply to the unprojected model, then reproject, so
  a later registration cannot discard them.
- Provider lookup snapshots are invalidated wherever the projection changes.

Hooks no longer take a providerFilter: a modifier is a whole-catalog transform
and every rebuild now runs the full ordered set exactly once.

(cherry picked from commit e5d2e9eac7c371cc196e9b362f77d3a5d7bdf507)
2026-07-29 23:08:37 +02:00
Abhishek Sharmaandcan1357 e73ab518a0 fix(model-registry): scope registration-time projection to the new provider
registerProvider composed nextModels from the already-projected #models,
stripping only the incoming provider, then reran every stored modifier over
it. Loaders drain registrations one at a time, so the previously registered
provider's projection was fed back into its own hook — an append-style hook
compounded on each subsequent registration.

Apply only the incoming provider's hook. Every other provider's projection is
already present exactly once, and full rebuilds still go through
#composeStaticModels.

(cherry picked from commit b16db642b08cc223b062c0c556f00d46fda2520a)
2026-07-29 23:08:37 +02:00
Abhishek Sharmaandcan1357 88c8c13b6a fix(model-registry): log projection failures and rebuild unprojected before rerunning hooks
Review follow-up on two defects in the original change:

- The throwing-hook fallback wrote to #lastDiscoveryWarnings, which is only
  ever read to dedup a logger.warn inside #warnProviderDiscoveryFailure. No
  log line was emitted, so a broken extension degraded invisibly, and the
  shared key could mask a later discovery failure for the same provider. Log
  via logger.warn with its own dedup map.

- #refreshRuntimeDiscoveries starts from the already-projected #models, and
  the overlay merge only replaces matching provider+id pairs, so a hook's
  projection-only entries survived and were fed back into it. An append-style
  hook duplicated its output on every refresh. Drop each modifier provider
  before the merge so it re-seeds from the unprojected overlays.

(cherry picked from commit b6f841e080d4882a08b8d713de009461b6acc6fe)
2026-07-29 23:08:37 +02:00
Abhishek Sharmaandcan1357 d10906c9a8 fix(model-registry): preserve oauth.modifyModels projection across reloads
`registerProvider` applies `oauth.modifyModels` once and assigns the result
straight to `#models`, but only the pre-projection definitions are persisted
in `#runtimeModelOverlays`. Any subsequent static reload rebuilds `#models`
from those overlays and silently drops the projection.

The model selector reloads on every open (`refresh("offline")`), so an
extension provider that projects a credential-aware catalog shows its
correct models everywhere except the picker — the one place users look.
`refreshProvider()` and online discovery completion had the same hole.

Persist the hook per provider and re-apply it wherever `#models` is
recomposed, honouring the `providerFilter` used by scoped lookups. A hook
that throws now degrades to that provider's unprojected catalog instead of
failing the whole composition, so one broken extension cannot empty the
registry.

(cherry picked from commit 33b7c72f225b4253b68bb71ecb3a9186151b18b3)
2026-07-29 23:08:37 +02:00
can1357 7d29a65b1c Merge PR #6923: fix(coding-agent): sync WezTerm theme changes on macOS (@Sairen777) 2026-07-29 23:08:35 +02:00
can1357 d5457eba24 fix(session): reset announced mounts after changed reload
(cherry picked from commit f7ec56c1c8b73476322904666d560492a6027ef7)
2026-07-29 23:08:34 +02:00
Alex Jadenandcan1357 070ff41f4c fix(coding-agent): sync WezTerm theme changes on macOS
(cherry picked from commit fa70be267daa6f98cf18e2f257041f4e6e68a61d)
2026-07-29 23:08:34 +02:00
can1357 a73cd40c8f Merge PR #6922: fix(session): gate xd:// mount notices against announced history (@roboomp) 2026-07-29 23:08:33 +02:00
roboompandcan1357 359dfb5b8f fix(session): keep pending xd mount delta on transcript reset
The previous reset dropped #pendingXdevMountDelta alongside the announced
baseline. Unlike /new and different-session switchSession, branch() does
not rebuild the base system prompt afterward, and because the device is
already in mountedNames no later refresh re-queues an add delta. Dropping
the undelivered delta therefore left the branched transcript unaware of a
still-mounted discoverable device.

Only the announced baseline is reset now; pending adds (still-live mounts
awaiting delivery) survive and announce on the next prompt in the new
transcript. Redundant on /new (the rebuilt prompt lists them too) but
harmless, and correct for branch.

Fixes #6921

(cherry picked from commit 5866440f27c15a320657e25fbfa0d2d65aad1fa2)
2026-07-29 23:08:33 +02:00
roboompandcan1357 13b1077d3e fix(session): reset announced xd mounts on transcript replace
The announced-mount baseline persisted across /new, switchSession, and
branch, which replace agent.state.messages but only clear session-scoped
tool state. A device announced in the old transcript stayed in the cache,
so reconnecting it into the fresh history was filtered as already known
and never announced, leaving the new conversation unaware of the device.

Reset the announced baseline (and any undelivered pending delta) from
#clearSessionScopedToolState, so the next notice re-seeds from the new
transcript and a reconnecting device announces again.

Fixes #6921

(cherry picked from commit d06dde02b9de4aacacd7aea5ee51edc7e524e3fb)
2026-07-29 23:08:32 +02:00
roboompandcan1357 82dc0d43ae fix(session): migrated legacy xd mount notices on resume
Replayed the stable added and removed inventory sections from legacy
xdev-mount-notice content when structured details are absent. This keeps
the first post-upgrade resume from re-announcing devices that persisted
history already introduced.

Covered both structured and legacy resume histories, including removed
devices and inline docs that must not be interpreted as inventory.

Fixes #6921

(cherry picked from commit 634a4c2de75f99e219f408c56bed83c04fe1290a)
2026-07-29 23:08:32 +02:00
roboompandcan1357 ac67548bc1 fix(session): gated xd:// mount notices against announced history
Mount-notice injection diff-gated only against the in-memory mountedNames
set, which is reseeded on every process resume / host reconnect. Dynamic
devices (MCP / RPC host) already announced in persisted history therefore
re-announced, splicing a redundant developer message that busts the
provider prompt-cache prefix and re-bills the whole suffix at full price
on metered providers.

Notices now persist a structured { added, removed } payload. On the first
consumption after resume the announced-device baseline is reconstructed
from history, and only a net change relative to what the model already
knows is announced, so a resume re-establishing the same inventory emits
nothing.

Fixes #6921

(cherry picked from commit 03c2ed5189510f41431bd164fa80187a69ed8de9)
2026-07-29 23:08:32 +02:00
can1357 84af4d3f3c Merge PR #6912: fix(coding-agent): surface MCP resource templates in resource summary (@roboomp) 2026-07-29 23:08:29 +02:00
roboompandcan1357 c88c59db84 fix(coding-agent): surfaced MCP resource templates in resource summary
formatAvailableResources only listed concrete resources, so the
assistant-facing summary shown on a failed mcp:// read understated the
server's contract even though template routing and /mcp resources
already handle them. Now templates are listed alongside concrete
resources.

Fixes #6911

(cherry picked from commit 254f35bb04cda8a615bbe1291d73b13cac0eabaa)
2026-07-29 23:08:28 +02:00
can1357 0e7ae1d6ac Merge PR #6908: fix(acp): sync model status when the agent switches models internally (@marton78) 2026-07-29 23:08:27 +02:00
Márton Danóczyandcan1357 9aaa455708 fix(session): defer rollback's model_changed emit until after thinking is restored
#emit's listeners (ACP's #handleLifetimeEvent -> #pushConfigOptionUpdate
-> #buildConfigOptions) run synchronously up to their first await, and
#buildConfigOptions is evaluated as a synchronous argument expression
before that await. Emitting model_changed immediately after
agent.setModel(previousModel) but before #models.restoreThinkingSnapshot
ran meant ACP could push a { previousModel, target-session-thinking }
config that was never an actual session state -- neither the failed
target nor the restored previous session.

Move the emit after restoreThinkingSnapshot/restoreServiceTiers so it
observes fully-restored state, same as every other rollback consumer
in this catch block already does implicitly by running after both.

Found by Codex on PR #6908 (pullrequestreview-4801303428), against
a33aa07df from this same branch.

bun test test/acp-agent.test.ts (57) + agent-session-switch-prev-context,
agent-session-model-persistence, agent-session-model-switch-auth,
agent-session-openai-completions-model-switch, nonvision-model-switch
-- 90 pass, 0 fail. Workspace typecheck clean.

(cherry picked from commit 0ac190a39a1687ebf85849dde5e1af9c2f9147fc)
2026-07-29 23:08:26 +02:00
Márton Danóczyandcan1357 65f4702485 fix(collab): push footer state to guests on model_changed
STATE_TRIGGER_EVENTS (host.ts) gates the debounced state broadcast
that carries CollabSessionState.model to collab guests. model_changed
was absent, so a guest's model display went stale on the same
internal switches (prewalk hand-off, retry-fallback, model cycling)
this PR fixes for ACP/RPC/TUI, until an unrelated trigger
(agent_start/message_end/...) or the 2s streaming-interval tick
happened to fire first.

#buildState() already reads session.model live, so this is purely a
trigger-registration gap -- no schema change.

Extends the PR's Unreleased changelog entry to cover the TUI render
fix, the collab fix, and the rollback corrective-event fix landed in
this branch.

(cherry picked from commit 066ed2b7c729b7d8b3f4424b2c19f4b65d631f08)
2026-07-29 23:08:26 +02:00
Márton Danóczyandcan1357 80c32cf16a fix(tui): request a render on model_changed, not just cache invalidation
handleEvent has no blanket pre-render (removed for issue #4353), so
each handler must explicitly schedule one when it changes something
visible -- every other statusLine.invalidate() call site in this file
pairs it with ui.requestRender(). The new model_changed handler only
invalidated the cache, so an internal model switch (prewalk hand-off,
retry-fallback) while the TUI was otherwise idle left the status line
showing the stale model until an unrelated event happened to render.

Flagged by @chatgpt-codex-connector on PR #6908.

(cherry picked from commit f565e3a4956bda467b6679a3c3f1e48379395a78)
2026-07-29 23:08:26 +02:00
Márton Danóczyandcan1357 3c0aa19820 fix(session): emit model_changed when switchSession rollback restores the model
switchSession's success path may already have called
#setModelWithProviderSessionReset for the target session's model,
which emits model_changed for it. If a later step in the try block
then throws, the catch restores previousModel via a direct
agent.setModel(...) that bypasses that method entirely and emitted
nothing — ACP/RPC/TUI kept advertising the target model that was
never actually committed.

Emit model_changed from the rollback path too, but only when the
restore actually changes the model back (guards the common case where
switchSession never touched the model or restores the same one).

Flagged independently by @roboomp and @chatgpt-codex-connector on
PR #6908.

bun test test/acp-agent.test.ts (57), agent-session-switch-prev-context.test.ts
(3), agent-session-model-persistence.test.ts (10 files) -- 80 pass, 0 fail.

(cherry picked from commit a33aa07df6fd61508956d73cc5b5284051bbfa86)
2026-07-29 23:08:26 +02:00
Márton Danóczyandcan1357 986b1a1e70 refactor(session): emit model_changed via sync #emit, not #emitSessionEvent
model_changed has no extension-facing hook (#emitExtensionEvent never
maps it), unlike message_start/tool_execution_end/etc. Routing it
through #emitSessionEvent added an await on extension delivery plus
the FIFO subscriber gate inside every model switch, including
retry-fallback on the hot error-recovery path, for zero benefit.

Match the sibling thinking_level_changed event, which already goes
through the plain synchronous #emit. ACP/RPC/TUI still receive it
identically since they subscribe via #eventListeners either way.

No observable behavior change: bun test test/acp-agent.test.ts stays
at 57 pass, dedup logic for client-initiated model changes unaffected
(push still lands during the awaited #setModelById call).

(cherry picked from commit 3edc9f8495b1b06fd990a64756d92e60354451fc)
2026-07-29 23:08:26 +02:00
can1357 a97d1bdd7c Merge PR #6907: fix: forward parseArgs and CONFIG_DIR_NAME from the legacy pi shim (@Gy-Hu) 2026-07-29 23:08:25 +02:00