Commit Graph
11295 Commits
Author SHA1 Message Date
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
Márton Danóczyandcan1357 5c0b52a141 test(coding-agent): use declared AgentToolResult type in ACP bridge test
Replaces `Awaited<ReturnType<typeof executeHashlineSingle>>` with the
function's declared `AgentToolResult<EditToolDetails, typeof
hashlineEditParamsSchema>` return type. AGENTS.md bans ReturnType<>.

Review: https://github.com/can1357/oh-my-pi/pull/6934
(cherry picked from commit 59dbc5b79f8857bab67559656eae96cf7dfb6ac0)
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
Márton Danóczyandcan1357 a5d01deb85 test(coding-agent): hoist hashline import to module scope in ACP bridge test
Replaces three inline `await import("@oh-my-pi/hashline")` calls with a
single top-level `computeFileHash` import. AGENTS.md forbids dynamic
imports.

Review: https://github.com/can1357/oh-my-pi/pull/6934
(cherry picked from commit 9ffda46c94de0146a8e77ec816640918a565a0ea)
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 a8c2feb6b1 docs(coding-agent): note extension provider model projection fix in changelog
(cherry picked from commit d82747b036515d3e6d9182ddccb519c59b766e94)
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 461971a49b test(session): cover model change event emission
(cherry picked from commit fd4b388c6947fb28539653ae56d61bf988f1b5ab)
2026-07-29 23:08:27 +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 e0415a9e90 test: cover legacy pi CLI exports
(cherry picked from commit a0e11651f72a69c80486dffbb8dbdd96d6a382f5)
2026-07-29 23:08:25 +02:00
Márton Danóczyandcan1357 25ace63676 docs(coding-agent): note ACP model status-sync fix in changelog
(cherry picked from commit 3be1a55298f67238b91a702abaa70c0fedc4df17)
2026-07-29 23:08:25 +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
Márton Danóczyandcan1357 0ca0739eeb fix(acp): sync model config option on internal model changes
Zed (and any other ACP client) never learned about a model switch that
happened from inside the agent loop — prewalk hand-offs, retry-fallback,
model cycling — because #pushConfigOptionUpdate was only ever wired to
the client-initiated setSessionConfigOption/setSessionMode RPCs and to
the thinking_level_changed lifetime event. The model itself did switch
correctly (subsequent requests used the new model), but the client's
model picker/status bar kept showing the session's starting model.

#handleLifetimeEvent now also reacts to the model_changed event added
in the previous commit and re-pushes config_option_update. Extend the
existing thinking-only subscription dedupe in setSessionConfigOption to
cover the model config id too, so a client-initiated model change still
produces exactly one notification once the lifetime subscription is
installed.

Regression tests mirror the existing thinking-level coverage:
- 'pushes config_option_update when the model changes internally'
- 'emits a single config_option_update per setSessionConfigOption(model) call'

Verified: bun test test/acp-agent.test.ts (57/57), plus
agent-session-prewalk.test.ts, agent-session-retry-fallback.test.ts,
retry-fallback.test.ts, model-resolver.test.ts, and the other acp-*.test.ts
files all still pass; tsgo --noEmit and biome check clean.

(cherry picked from commit f5c5081088e8cbac2650a9de89049731de1b2777)
2026-07-29 23:08:25 +02:00
Márton Danóczyandcan1357 bce1ff55a7 feat(session): emit model_changed event on every internal model switch
AgentSession#setModelWithProviderSessionReset is the single choke point
every model mutation runs through (explicit /model, prewalk hand-offs,
retry-fallback, model cycling). It previously changed agent.state.model
silently — no session event told subscribers (ACP, RPC, TUI) that the
active model moved.

Emit a new model_changed AgentSessionEvent from that choke point
whenever the model actually changes, and wire it into every consumer
that must exhaustively handle AgentSessionEvent: the TUI event
controller (invalidates the status line, same as thinking_level_changed)
and the RPC client's forwarded-event allowlist.

(cherry picked from commit f76325de2c7821dd7046ddb67546577c3575a263)
2026-07-29 23:08:25 +02:00
can1357 8e72d4778e test(ttsr): cover human-readable inference note
(cherry picked from commit 8867610c39cf302089b7a4203e3ff8886fb1af52)
2026-07-29 23:08:24 +02:00
can1357 ab48f43783 Merge PR #6888: fix(ttsr): flag text-source inference for unlisted file extensions (@roboomp) 2026-07-29 23:08:24 +02:00
can1357 b5ad903788 fix(ttsr): exclude deleted patch text from matcher digest
(cherry picked from commit b4812365368368a5706131c3ff1ecd52fd64662d)
2026-07-29 23:08:24 +02:00
can1357 3a61aa727f fix(coding-agent): keep Advisor retry sleep on Bun
(cherry picked from commit 31b9090f901b520b57d5dacec3f8c7dba271decc)
2026-07-29 23:08:23 +02:00
can1357 8c45df1cdd Merge PR #6883: fix(coding-agent): scope Advisor cost to the active session (@paolomazzitti) 2026-07-29 23:08:23 +02:00
can1357 7338be4d67 Merge PR #6845: fix(launch): isolate legacy xterm replay (@usr-bin-roygbiv) 2026-07-29 23:08:22 +02:00
usr-bin-roygbivandcan1357 363925dce4 style: follow Node import conventions
(cherry picked from commit b5c695605c13169c4d0a70101913476ead19dba8)
2026-07-29 23:08:22 +02:00
usr-bin-roygbivandcan1357 197fd2df02 test: restore complete Worker descriptors
(cherry picked from commit a7d227e6227b2c1d20ae4a9d5107be57980463a1)
2026-07-29 23:08:22 +02:00
usr-bin-roygbivandcan1357 856765ad51 docs: keep changes under unreleased
(cherry picked from commit 590690a7ffbda59768af4c5e60e5867b40188bce)
2026-07-29 23:08:22 +02:00
usr-bin-roygbivandcan1357 6772ef9487 test(launch): use shared promise gate
(cherry picked from commit e5ed202be829bff0bd4f10c2d3d33a996627b138)
2026-07-29 23:08:22 +02:00
can1357 2c99f2f2e8 fix(git): preserve effective character locale
(cherry picked from commit ef7abf60ad1b80642ba1731e043f8eeb82c6a6aa)
2026-07-29 23:08:21 +02:00
usr-bin-roygbivandcan1357 8b81b1c0a0 fix(launch): preserve logs on replay failure
(cherry picked from commit 2bebc32a05553e75edef16f71218fa2bfbfc1f77)
2026-07-29 23:08:21 +02:00
usr-bin-roygbivandcan1357 77e90e458d fix(launch): reject early replay worker exits
(cherry picked from commit d5bbebb22485a118c5efb690ec40033583a38d81)
2026-07-29 23:08:21 +02:00
usr-bin-roygbivandcan1357 4436038127 fix(launch): isolate legacy xterm replay
(cherry picked from commit 47baab894a224f3354085b4794337a211d3cf5c8)
2026-07-29 23:08:21 +02:00
can1357 3bce6527b3 Merge PR #6842: fix(git): force stable locale for non-interactive commands (@rolando439) 2026-07-29 23:08:20 +02:00
Rolando Diazandcan1357 e8bfd67d28 test(gh): bypass cached binary lookup
(cherry picked from commit 85d0ddca05640e95e7d750218c6533d6f936957d)
2026-07-29 23:08:20 +02:00
Rolando Diazandcan1357 b1c3ba8b8e fix(git): preserve UTF-8 locale for gh subprocesses
(cherry picked from commit e703aa00892b4589baa6c9dcb5f3ab2a3afb1662)
2026-07-29 23:08:20 +02:00