Commit Graph
16103 Commits
Author SHA1 Message Date
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 4b753597fd docs(coding-agent): add grouped read changelog
(cherry picked from commit f23d576925906e6a39bfab44d421a52494a8f20b)
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 e24babd8e1 Merge PR #6941: fix(tui): stop loader spinner re-wrapping text every tick (@roboomp) 2026-07-29 23:08:40 +02:00
roboompandcan1357 beaafb9955 fix(tui): handled mixed-width spinner frames
Cached one stable layout representative per frame display width so same-width animation retains the fast path while width transitions rewrap before painting. Added coverage for wider custom frames remaining within terminal bounds.

(cherry picked from commit 3aa173a574538d754e68f8d10f7b88d437709884)
2026-07-29 23:08:40 +02:00
roboompandcan1357 b152f40134 fix(tui): stopped loader spinner re-wrapping text every tick
The Loader baked the advancing braille glyph into the underlying Text
via setText, so Text.render's wrap cache (keyed on text) missed every
80ms tick and re-ran wrapTextWithAnsi plus per-line width measurement
over the whole message even though only a 1-cell glyph changed. On a
single-subagent hub wait the direct-write render loop consumed ~48% of
a saturated core.

The wrapped text now carries a stable sentinel glyph (frames[0]); the
spinner advance only bumps the frame index and requests a repaint, and
render() swaps the visible glyph over the cached layout. wrapTextWithAnsi
and stringWidth now run only when the message actually changes.

Fixes #6940

(cherry picked from commit 6ec38053ebc1e33859358df3b66f71fea27532c7)
2026-07-29 23:08:40 +02:00
can1357 c6db470db5 test(coding-agent): cover drifted ACP write snapshot
(cherry picked from commit a8ef3465929e0ebdc026e0b6f94ea65d491946c8)
2026-07-29 23:08:39 +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
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 441f4e607c Merge PR #6926: fix(catalog): send string tool choice to LM Studio (@roboomp) 2026-07-29 23:08:36 +02:00
roboompandcan1357 10b5dcbca5 fix(ai): handled LM Studio Responses tool choices
Marked LM Studio as string-only for the Responses transport, narrowed the emitted tool catalogue to the forced function, and downgraded the named object to required.

Added wire regressions for the corrected openai-responses reproduction while preserving OpenAI's named-object behavior and the absent-tool fallback.

Fixes #6925

(cherry picked from commit a58ae400cf0e02d58f6344341caecc3eab9aebd2)
2026-07-29 23:08:36 +02:00
roboompandcan1357 b2be6458a1 fix(ai): narrowed tools to forced choice on string-only hosts
String-only OpenAI-compatible hosts (llama.cpp, LM Studio) degrade a named tool_choice to "required", which alone lets the host call any advertised tool. Narrow params.tools to the forced tool before the downgrade when it is present, mirroring the Ollama chat transport, so the named force is still enforced; keep the full list and drop the choice when the tool is absent.

Fixes #6925

(cherry picked from commit c34a366f3432113e9f42f4fd07b85dd427d517c1)
2026-07-29 23:08:36 +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
roboompandcan1357 6ba70e47f8 fix(ai): preserved forced-name fallback for string-only tool choice
Computed forcedToolName from the mapped named object before the named->required downgrade so the tool-absent guard still drops the forced choice for llama.cpp and LM Studio, falling back to an unforced turn instead of forcing an arbitrary tool.

Fixes #6925

(cherry picked from commit c31327ae9a69c857ce95d4af1becdaa2869a757b)
2026-07-29 23:08:35 +02:00
roboompandcan1357 ef936470d3 fix(catalog): sent string tool choice to LM Studio
Marked LM Studio as a string-only named-tool-choice host so forced turns serialize the supported required selector.

Extended the provider wire regression test and recorded the user-facing fix.

Fixes #6925

(cherry picked from commit 9b90153f0ece928d7926ad27e9173f873db29443)
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 028912ef01 fix(tui): suppress appearance probes while stopped
(cherry picked from commit a53b38a8b1b6f0a6d5e45fe46b84857c93e56bd2)
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
can1357 f925f9f38a test(utils): cover quit without stdout drain
(cherry picked from commit 59a8002411726d8686c187a6d031a1f54577093b)
2026-07-29 23:08:32 +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 897b0c8d6c Merge PR #6920: fix(tui): prevent Windows pane-close deadlock (@roboomp) 2026-07-29 23:08:31 +02:00
roboompandcan1357 313f3fa1c4 fix(tui): prevented Windows pane-close deadlock
Skipped stdout draining after ProcessTerminal observes a native Windows terminal disconnect, while preserving normal postmortem cleanup and drain behavior for every other shutdown path.

Fixes #6917

(cherry picked from commit b45fc00ab4b5075ccb9650584947dde061beea20)
2026-07-29 23:08:31 +02:00
can1357 cfb82436a1 Merge PR #6918: fix(ai): escape harmony control tokens in codex request input (@roboomp) 2026-07-29 23:08:30 +02:00
roboompandcan1357 baa9a8282d fix(ai): escape replayed messages that omit the type field
Address PR #6918 review: EasyInputMessage may be persisted as
`{ role, content }` with no `type`, which the responses server accepts
verbatim into providerPayload. escapeReplayedClientText only matched
`type === "message"`, so a typeless replayed user/developer message
sent raw markers to gpt-5.x. Treat a missing `type` with a client role
as a message too, narrowing via `in` guards so non-message items stay
untouched.

Fixes #6913

(cherry picked from commit 04d83dfa3efbd56f5963d77dd7f4d89813a6116c)
2026-07-29 23:08:30 +02:00
roboompandcan1357 8c3b316be5 fix(ai): harden harmony escaping for wire aliases and history replay
Address PR #6918 review:
- Gate escaping on the wire model id via isHarmonyDialectModel
  (requestModelId ?? id), so deployment/catalog aliases whose local id
  is opaque but whose wire id is gpt-5.x are still escaped.
- Escape client-boundary text of replayed native-history items
  (user/developer/system message text and tool-result output) before
  they hit the wire; the replay branch previously bypassed
  convertResponsesInputContent entirely, sending stored markers raw.

Applies to both the shared Responses builder and the codex builder.
Assistant output, reasoning, and tool-call arguments stay untouched.

Fixes #6913

(cherry picked from commit 2c5cf286c447122fdb7633eaf3d9371370648966)
2026-07-29 23:08:30 +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 9f37b1864d fix(ai): escaped harmony control tokens in codex request input
Harmony-server models (gpt-5.x / openai-codex) reject any Responses
request whose input reproduces reserved control-token spellings such as
`<|channel|>analysis`, failing before generation with invalid_prompt /
"Request blocked". The live agent-loop builders serialized user and
tool-result text verbatim into input_text / function_call_output, so a
tool result carrying the marker (e.g. a broad `omp://` grep) poisoned
the persisted session — every later turn replayed it and failed.

Escape reserved harmony tokens on the transport copy of untrusted
user/tool text in both the shared Responses builder and the codex
builder, gated on `preferredDialect === "harmony"`. The persisted
transcript is untouched and non-harmony models are unaffected. Extends
the existing compaction-payload escaping to the request boundary and
routes both through a shared `escapeHarmonyControlTokens` helper.

Fixes #6913

(cherry picked from commit dfea56588e7aa1c35971c4684221f4a86142e04e)
2026-07-29 23:08:29 +02:00
can1357 a2442ab929 Merge PR #6910: perf(utils): avoid Winston runtime dispatch (@usr-bin-roygbiv) 2026-07-29 23:08:28 +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
usr-bin-roygbivandcan1357 574f400b0f fix(utils): pin logger rotation entrypoint
(cherry picked from commit 6dc31019848a20053e66df947c48e087cce8566a)
2026-07-29 23:08:28 +02:00