Skip to content

fix(telegram): harden durable topic authority on current dev - #31

Closed
twoimo wants to merge 1219 commits into
devfrom
feat/telegram-topic-authority-v13-recovered
Closed

fix(telegram): harden durable topic authority on current dev#31
twoimo wants to merge 1219 commits into
devfrom
feat/telegram-topic-authority-v13-recovered

Conversation

@twoimo

@twoimo twoimo commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Summary

Replacement for the closed Telegram topic-authority review, rebased onto current upstream/dev (3bddcc579).

  • Hardens durable topic authority and archive recovery.
  • Treats archive settlement as idempotent only for Telegram HTTP 400 responses with documented not-found/already-closed descriptions.
  • Keeps non-400 responses retryable.
  • Updates the generation manifest and focused adversarial coverage.

Verification

  • Darwin arm64 native addon build: passed
  • Focused Telegram suites: 611 passed, 0 failed
  • Telegram generation guard tests: 48 passed, 0 failed
  • Coding-agent typecheck: passed
  • Biome checks on changed Telegram/generated files: passed
  • Current-tree generation guard validation: passed
  • git diff --check: passed

The prior PR Yeachan-Heo#3700 is closed; this branch is published separately for fresh maintainer review. No merge is requested by this session.

probepark and others added 30 commits August 4, 2026 17:06
Slow SSH terminals can accumulate stale spinner frames faster than stdout drains. Pause shared decorative animation ticks while stdout is congested, then resume from live state without replaying skipped frames.

Lore-id: tui-animation-backpressure-v2
Constraint: content-bearing renders must remain ordered and unthrottled
Rejected: throttling terminal writes | would violate differential renderer ordering
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: animation scheduler congestion, resume, healthy sink, shared bucket, loader cadence
…#3818)

* fix(extensions): preserve live handler context accessors

Attaching timeout signals by spreading the extension context eagerly evaluated and froze its live model getter. It also moved accessor failures outside the runner error boundary, breaking SDK lifecycle containment.

Lore-id: c3817ded
Constraint: each handler must receive an isolated timeout AbortSignal without snapshotting live context accessors
Rejected: mutate the shared emit context | timed-out handlers could observe a later sibling signal
Rejected: prototype delegation | changes own-property enumeration semantics for extensions
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: extension runner, SDK model selection, SDK host lifecycle, coding-agent typecheck
Not-tested: complete shard under operator home because unrelated GC discovery exceeds its 20000-entry cap

* fix(extensions): keep handler signals writable

Descriptor cloning preserved live accessors but accidentally made the injected signal non-writable. ExtensionContext exposes a mutable signal property, so strict extensions must retain object-literal assignment behavior.

Lore-id: c3817sig
Constraint: preserve the signal descriptor semantics introduced by the extension timeout context
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: focused extension runner and SDK lifecycle/model suite; coding-agent check

---------

Co-authored-by: gaebal-gajae <gaebal-gajae@users.noreply.github.com>
…han-Heo#3813)

* fix(session): share one artifact store across the subagent tree

Nested subagents did not share the parent artifact store. A subagent's
SessionManager adopts the parent ArtifactManager, so getArtifactsDir()
returns null and the adopted manager's dir differs from the subagent's
own session-file-derived dir. The task tool only accepted the session
manager when those dirs matched, so a grandchild task got a private
directory: split artifact ID spaces, agent:// URIs the parent could not
resolve, and lost managedPersistence routing.

TaskTool#sharedArtifactStore now treats the session manager as
authoritative when ownership is provable (dir equality, or lexical
containment of the session file inside the manager dir). A manager
unrelated to either root is still rejected as foreign.

SessionManager also kept a write-only in-memory artifact map for
non-persistent sessions: content was retained for the session lifetime,
never read back, and the returned id was unresolvable. It is replaced by
a lazily created temp-directory ArtifactManager, exposed through
getArtifactManager() so artifact:// resolves, and discarded on
fresh-session transitions.

Fixes a pre-existing ArtifactManager race found while probing the new
path: #ensureDir did not memoize its ID scan, so concurrent first
writers both restarted the counter at 0 and collided on publish.

* chore(docs): refresh embedded docs index

The artifact-store architecture update changed source documentation, so the generated internal docs index must be refreshed after rebasing onto current dev.

Lore-id: 3813-docs-index

Confidence: high

Scope-risk: narrow

Reversibility: easy

Tested: bun run check:public-sync

* fix(session): harden shared artifact store lifecycle

Ephemeral roots could leak or be retired before a failed session transition rolled back, while lexical parent checks could accept foreign managers or reject the live ephemeral owner. Bind sharing to exact manager identity and drain owned roots on terminal close.

Lore-id: 3813-artifact-store-review

Constraint: preserve one artifact ID namespace across an authorized agent tree

Constraint: reject cross-session and foreign-manager authority

Rejected: pathname containment proof | lexical nesting does not prove session ownership

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 36 focused artifact lifecycle, authorization, task, and managed-persistence tests

Tested: coding-agent biome and TypeScript checks

* fix(session): claim artifact ids across managers

Independent ArtifactManager instances could scan the same root and publish different tool types under one numeric ID, making artifact resolution ambiguous. Atomic hidden claims serialize the numeric namespace across managers and processes while preserving exact manager authorization and lifecycle cleanup.

Lore-id: 3813-artifact-id-claims

Constraint: preserve prior ephemeral cleanup, transition rollback, and exact manager authorization repairs

Rejected: process-local shared counter | does not prevent cross-process collisions

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 163 focused tests across artifact lifecycle, authorization, task persistence, pruning, and tools

Tested: coding-agent check and public docs sync

* fix(session): retire artifacts after resume commit

Switching a no-session predecessor to an existing populated session committed the new session but retained the predecessor's ephemeral artifact root until shutdown. Retire that root immediately after the resident transition commits while leaving every pre-commit failure path untouched.

Lore-id: 3813-existing-resume-cleanup

Constraint: preserve failed-transition artifact ownership until commit

Constraint: resumed sessions must create and authorize their own artifact manager

Rejected: retire before transition commit | destroys predecessor outputs when resume preparation fails

Confidence: high

Scope-risk: narrow

Reversibility: easy

Tested: 37 focused artifact lifecycle, authorization, task output, read, and managed descendant tests

Tested: coding-agent Biome and TypeScript checks

* fix(task): adopt task-first artifact manager

A no-session parent could run TaskTool before saving its own artifact, leaving the task fallback manager visible only through ToolSession overrides. The parent SessionManager then created a second root and numeric namespace. Wire explicit fallback adoption into the concrete SessionManager and verify the exact relationship before publishing task output authority.

Lore-id: 3813-task-first-artifact-adoption

Constraint: task-first and parent-first ordering must share one manager, root, and numeric ID space

Constraint: preserve exact identity authorization and managed persistence routing

Rejected: ToolSession override only | parent SessionManager remains unaware and can create a second artifact root

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 160 focused artifact, lifecycle, task, pruning, and tool tests

Tested: coding-agent Biome and TypeScript checks

* fix(session): retire task artifacts on transitions

Task-owned fallback roots and ToolSession overrides previously survived logical session changes, unsafe numeric claims could stall allocation beyond JavaScript's safe integer range, and pruning reservation failures escaped the best-effort boundary. Release exact adopted authority and overrides only after committed transitions, reject unsafe scanned IDs, and omit pruning references when allocation is unavailable.

Lore-id: 3813-transition-artifact-hardening

Constraint: preserve task-first canonical manager adoption and atomic no-replace claims

Constraint: failed resume transitions must retain predecessor manager authority and artifact data

Constraint: persisted target artifacts must remain readable after resume and restart

Rejected: cleanup before final switch success | destroys predecessor artifacts on rollback

Rejected: clamp unsafe IDs | can reuse or repeat a conflicting filename

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 163 focused artifact, lifecycle, task, pruning, and tool tests

Tested: 65 logical transition and handoff tests

Tested: coding-agent Biome and TypeScript checks

* fix(session): scope logical transition cleanup

Logical session changes drained the final ToolSession disposal registry to retire task fallback artifacts. That also cancelled deferred MCP startup and disconnected long-lived tooling while the AgentSession remained active. Split session-transition cleanup from final disposal cleanup, route task fallback retirement through the transition registry, and keep deferred MCP teardown final-only.

Lore-id: 3813-scoped-transition-cleanup

Constraint: resume hooks must observe successor routing after stale task overrides are retired

Constraint: failed pre-commit resume must retain predecessor task artifacts and authority

Constraint: final disposal must drain both transition-scoped and final-only cleanup

Rejected: filter the shared cleanup set by callback identity | couples unrelated subsystems and cannot prove authority

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 163 focused artifact, lifecycle, task, pruning, and tool tests

Tested: 65 logical transition and handoff tests

Tested: 36 SDK MCP lifecycle tests

Tested: coding-agent Biome and TypeScript checks

* fix(session): defer ephemeral retirement on resume

SessionManager retired a no-session predecessor's ephemeral artifact root as soon as setSessionFile adopted the target. Later AgentSession readiness or restore failures could roll back metadata after the predecessor authority was cleared and its files were already scheduled for deletion. Defer retirement for outer resume transactions and settle it only at the final successor commit boundary.

Lore-id: 3813-resume-ephemeral-rollback

Constraint: failed resume must preserve predecessor artifact authority and readable payloads

Constraint: successful resume must retire predecessor artifacts before successor-visible hooks

Constraint: direct SessionManager setSessionFile callers retain immediate retirement semantics

Rejected: snapshot cleanup promises | filesystem deletion cannot be reliably cancelled after dispatch

Confidence: high

Scope-risk: narrow

Reversibility: easy

Tested: 164 focused artifact, lifecycle, task, pruning, and tool tests

Tested: 65 logical transition and handoff tests

Tested: isolated existing resume rollback regression

Tested: coding-agent Biome and TypeScript checks

* fix(session): stage fallback before local migration

A task-first fallback manager remained adopted while resume initialized a managed target's legacy local tree. SessionManager therefore exposed neither the target artifact directory nor its retained migration source, allowing an absent marker to hide valid legacy content. Temporarily release adopted authority after target loading and before local-root initialization; rollback restores the snapshotted fallback, while successful transition cleanup retires it at commit.

Lore-id: 3813-managed-local-staging

Constraint: managed legacy local migration must observe the successor session authority

Constraint: pre-commit failure must restore fallback manager, claims, payloads, and routing

Constraint: predecessor cleanup remains final-commit-only

Rejected: migrate after fallback deletion | loses rollback authority and exposes a cleanup gap

Confidence: high

Scope-risk: narrow

Reversibility: easy

Tested: 165 focused artifact, lifecycle, task, pruning, and tool tests

Tested: 65 logical transition and handoff tests

Tested: 73 managed session-directory tests with 3 platform skips

Tested: 36 SDK MCP lifecycle tests

Tested: coding-agent Biome and TypeScript checks

* fix(session): linearize task artifact initialization

Parent saveArtifact and first TaskTool execution could each observe no nonpersistent artifact manager and independently allocate roots before either installed authority. Route TaskTool through SessionManager's memoized ephemeral initialization transaction, then install only routing overrides around that canonical manager. Parent and task callers now await one promise and share one root and numeric namespace.

Lore-id: 3813-linearized-artifact-init

Constraint: concurrent parent-first and task-first initialization must produce one manager and root

Constraint: task authorization rollback must never delete SessionManager-owned artifacts

Constraint: transition cleanup restores ToolSession routing while SessionManager retains root ownership

Rejected: reconcile managers after allocation | duplicate ID 0 may already be published in separate roots

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 167 focused artifact, lifecycle, task, pruning, and tool tests

Tested: 65 logical transition and handoff tests

Tested: 36 SDK MCP lifecycle tests

Tested: coding-agent Biome and TypeScript checks

* fix(session): settle detached tasks before resume

switchSession aborted only the foreground run, then retired predecessor artifact authority while owner-scoped detached task jobs could still publish through captured managers and roots. Apply the owner shutdown lease, producer fencing, subagent cancellation proof, delivery drain, and job settlement protocol before disconnecting, flushing, or entering any artifact-retirement boundary.

Lore-id: 3813-resume-detached-settlement

Constraint: no predecessor task job may remain live when resume retires artifact authority

Constraint: failed settlement must retain predecessor identity, root, routing, and shutdown admission

Constraint: cancellation is owner-scoped and must not control foreign jobs

Rejected: ignore late publication errors | writes can recreate orphaned unauthorized roots

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 168 focused artifact, lifecycle, task, pruning, and tool tests

Tested: 65 logical transition and handoff tests

Tested: 30 owner shutdown and subagent cancellation tests

Tested: 36 SDK MCP lifecycle tests

Tested: coding-agent Biome and TypeScript checks

* fix(session): commit shutdown on copied resume

Owner shutdown finalization inferred transition success from session ID inequality. A successful switch to a copied transcript at a different path can preserve the same ID, causing the predecessor fence to release and replay queued deliveries/state into the successor. Track the irreversible different-path transition boundary explicitly and commit the lease from that fact.

Lore-id: 3813-copied-transcript-shutdown

Constraint: successful different-path switches commit owner shutdown regardless of copied session ID

Constraint: rollback and pre-commit failure continue to release the shutdown lease

Constraint: predecessor queued deliveries and subagent records must not enter the successor

Rejected: compare session file in finally | rollback can restore paths after partial mutation and obscures the explicit boundary

Confidence: high

Scope-risk: narrow

Reversibility: easy

Tested: 66 logical transition and handoff tests

Tested: 18 owner shutdown queue tests

Tested: 168 focused artifact, lifecycle, task, pruning, and tool tests

Tested: 36 SDK MCP lifecycle tests

Tested: coding-agent Biome and TypeScript checks

* fix(session): validate resume before owner teardown

switchSession cancelled owner jobs and consumed producer cleanup callbacks before the successor passed local-root, model, MCP, persistence, and state validation. Rollback could restore session metadata but not the predecessor's running work. Keep only the non-destructive owner lease during validation, then perform producer cleanup, cancellation proof, delivery drain, and job settlement at the irreversible transition boundary.

Lore-id: 3813-resume-validation-before-shutdown

Constraint: pre-commit resume failure must preserve owner jobs and producer callbacks

Constraint: owner deliveries remain fenced while successor validation is in progress

Constraint: destructive owner shutdown still completes before artifact retirement and successor hooks

Rejected: release after early cancellation | release cannot recreate cancelled jobs or consumed callbacks

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 67 logical transition and handoff tests

Tested: 168 focused artifact, lifecycle, task, pruning, and tool tests

Tested: 18 owner shutdown lifecycle tests

Tested: 36 SDK MCP lifecycle tests

Tested: coding-agent Biome and TypeScript checks

* fix(session): settle owners before artifact retirement

Resume shutdown timeouts occurred after successor validation but before reconnect and session_switch, leaving callers with an error while the successor was already authoritative. Complete that path forward, keep the unsettled owner fence active, preserve predecessor roots/routing cleanup, reconnect, publish the successor, and surface an explicit cleanup notice. Prepared fork, branch, and handoff transitions now prove owner subagent cancellation and await owner-job settlement before committing the successor and retiring predecessor artifacts.

Lore-id: 3813-transition-owner-settlement

Constraint: post-validation resume cleanup failure must publish one coherent successor state

Constraint: unsettled predecessor jobs keep their shutdown fence and artifact root

Constraint: fork branch and handoff must settle only their owner before artifact retirement

Rejected: unawaited cancel before prepared commit | detached publication can race root deletion

Confidence: high

Scope-risk: bounded

Reversibility: migration-free

Tested: 17 owner transition regressions with 81 assertions

Tested: 33 handoff regressions with 159 assertions

Tested: 71 logical transition tests before final parameter expansion

Tested: 62 async owner/task tests with 314 assertions

Tested: 143 artifact lifecycle tests with 617 assertions

Tested: 36 SDK MCP lifecycle tests with 227 assertions

Tested: coding-agent Biome and TypeScript checks

* fix(session): finalize deferred resume shutdown

Forward-only resume preserved the validated successor after owner shutdown timeout, but retained the owner lease and predecessor transition resources forever. Schedule bounded retry attempts under the retained fence; once producer cleanup, subagent proof, delivery drain, and owner-job settlement all succeed, retire predecessor artifacts, drain task transition cleanup, and commit the lease. Disposal joins or releases pending finalizers.

Lore-id: 3813-deferred-shutdown-finalization

Constraint: forward resume may remain fenced only while predecessor settlement is incomplete

Constraint: artifact and task authority retire only after proof and owner-job settlement

Constraint: later owner jobs and transitions must become available after finalization

Constraint: foreign-owner jobs and deliveries remain untouched

Rejected: permanent retained lease | wedges all future owner work and transitions

Confidence: high

Scope-risk: bounded

Reversibility: easy

Tested: 71 transition tests with 316 assertions

Tested: 63 async owner/task tests with 317 assertions

Tested: 143 artifact lifecycle tests with 617 assertions

Tested: 36 SDK MCP lifecycle tests with 227 assertions

Tested: coding-agent Biome and TypeScript checks

Directive: use Promise.withResolvers for abort and retry test gates

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
Co-authored-by: gaebal-gajae <gaebal-gajae@users.noreply.github.com>
…chan-Heo#3835)

DeepSeek V4 (and reasoning-capable siblings on any OpenAI-compatible proxy)
reject every follow-up turn with "The reasoning_content in the thinking mode
must be passed back to the API" once a prior assistant turn carried reasoning
the proxy stripped to an empty encrypted_content. Resending the identical
history re-triggers this deterministic 400, so naive session auto-retry just
burns the budget.

Add a bounded, strip-only circuit breaker mirroring the existing invalid_prompt
poisoned-history breaker: on the first such rejection of a run, strip the
unusable reasoning items from the Responses history payload in place (never
dropping text, tool-call, or tool-output items) and resend exactly once so the
model re-reasons. If nothing can be stripped, fail fast. Budget = one repaired
resend.

New shared utilities in @gajae-code/ai/utils:
- isReasoningContentReplayError: detects the error across message carrier shapes
- stripUnusableReasoningItems: removes only reasoning items with empty/missing
  encrypted_content, preserving all non-reasoning history

Lore-id: 4b3211d3
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: agent-loop circuit-breaker matrix (6 cases), classifier + strip unit
  suite (12 cases), existing invalid_prompt/recovery/deepseek suites green
Not-tested: live DeepSeek proxy replay (no credentials in CI)
Supersedes: none

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…entory (Yeachan-Heo#3837)

Yeachan-Heo#3813 added AgentSession.registerToolSessionTransitionCleanup for shared
artifact-manager ownership on session transitions. The method is an internal
lifecycle registration seam, parallel to registerToolSessionCleanup, but was
never added to LOCKED_EXCLUSIONS. Exact-head Dev CI then fails the generated
SDK operation inventory check on any PR based on current dev.

Classify the seam as a locked exclusion and regenerate the committed matrix.

Lore-id: 3813seam01
Constraint: do not expose as a public SDK control
Rejected: map to a new SDK operation | no user-facing control exists
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: sdk-operation-inventory.test.ts (17 pass); inventory --check
Not-tested: full coding-agent shard matrix on CI

Refs Yeachan-Heo#3813
…eachan-Heo#3804) (Yeachan-Heo#3836)

* fix(session): observe resume picker rejections without process kill (Yeachan-Heo#3804)

Managed-candidate preparation can reject before switchSession (identity fence).
The picker dispatched resume through a void onSelect, so that rejection escaped
as an unhandled promise rejection and could kill the process. Catch at the UI
dispatch boundary, surface via showError, and keep the active session usable.

Lore-id: 3804a1b2
Constraint: preserve strict identity fence -- no auto-retry of changed candidate
Constraint: keep current session usable after resume preparation failure
Rejected: weaken identity validation | would admit races into migration
Rejected: swallow all handleResumeSession errors | direct callers need propagation
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: picker race showError + no unhandledRejection; component async onSelect recovery; existing resume reentrancy suite
Not-tested: live interactive TUI end-to-end against a real filesystem race

* style(session): format resume-picker rejection tests for biome

Successor to closed Yeachan-Heo#3830. Exact-head review accepted the identity-fence
and unhandled-rejection recovery; the only blocker was biome formatter
wrapping of three expect() calls that fit the 120-col limit.

Lore-id: 3804fmt01
Constraint: no behavior change -- formatter-only
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: focused resume/selector suites; biome format clean
Not-tested: live interactive TUI resume race

Fixes Yeachan-Heo#3804
The defers-memory-startup test creates a full agent session, which can
exceed bun's default 5s per-test timeout on cold CI runners even though
the session creation is not faulty. Raise the explicit timeout to 30s so
the test asserts the deferral behavior without flaking on slow CI.

Lore-id: 8ca3d1f2
Confidence: high
Scope-risk: low
Reversibility: revert-commit
Tested: bun test packages/coding-agent/test/sdk-memory-startup.test.ts (passes)
Not-tested: full CI re-run
The existing MiniMax M3 policy forced every first-class route back to 512K despite official Claude Code and Codex Token Plan contracts documenting a 1M context window. Scope the correction to the four first-class regional MiniMax routes and add route and alias regression coverage.

Lore-id: issue-3770\nConstraint: preserve unrelated aliases and openai-codex\nConstraint: keep billing/provider boundaries route-specific\nRejected: blanket model-ID-only widening | aliases lack independent contract evidence\nConfidence: high\nScope-risk: narrow\nReversibility: straightforward\nTested: focused MiniMax/model-thinking tests; package check; repository build; CLI smoke\nNot-tested: live MiniMax billing/API behavior
Mirror the local qwen-deepseek preset as an Alibaba Token Plan built-in
profile so the Qwen-plus-DeepSeek role split works without a custom
models.yml. Alibaba vendors DeepSeek V4 Flash as deepseek-v4-flash-0731,
which only exposes low/high/max efforts, so the layofflabs planner
xhigh/executor medium map to the nearest valid high/low.

Lore-id: 8f2e1c9a
Confidence: high
Scope-risk: low
Reversibility: revert-profile
Tested: model-profiles-catalog/model-profile-activation/model-profiles-redteam
Not-tested: live activation against an Alibaba Token Plan credential
Use the literal layofflabs reasoning split on deepseek-v4-flash-0731.
Lore-id: 8f2e1c9a
Confidence: high
Scope-risk: low
Reversibility: revert-profile
Tested: model-profiles-catalog/model-profile-activation/model-profiles-redteam
Raise the qwen-deepseek profile's default role to high reasoning and the
planner role to max, matching the operator's preferred split.

Lore-id: 8f2e1c9a
Confidence: high
Scope-risk: low
Reversibility: revert-profile
Tested: model-profiles-catalog/model-profile-activation/model-profiles-redteam
Lore-id: 8f2e1c9a
Confidence: high
Scope-risk: low
Reversibility: revert-profile
Tested: model-profiles-catalog/model-profile-activation/model-profiles-redteam
Register qwen-3.8-max as the non-preview Qwen model in the Alibaba catalog
and provider preset, and add the glm-deepseek profile mirroring
qwen-deepseek's role split with GLM 5.2 as the expensive model.

- qwen-deepseek: Qwen 3.8 Max (default/architect/critic) + DeepSeek V4 Flash 0731 (planner max / executor high)
- glm-deepseek: GLM 5.2 (default high/architect xhigh/critic xhigh) + DeepSeek V4 Flash 0731 (planner max / executor high)

Lore-id: 8f2e1c9a
Confidence: high
Scope-risk: low
Reversibility: revert-profile
Tested: model-profiles-catalog/model-profile-activation/model-profiles-redteam/provider-onboarding; ai generate-models
Not-tested: live activation against an Alibaba Token Plan credential
… preset

The docs-index.generated.ts embeds a hash of models.md; adding the new
qwen-deepseek profile preset changed the models.md length, so the
embedded hash went stale and failed the check:public-sync gate.

Lore-id: 8f2e1c9a
Confidence: high
Scope-risk: low
Reversibility: revert-profile
Tested: check:public-sync
Not-tested: full test suite (worktree has no node_modules)
The affected-path check:@gajae-code/ai gate failed on a formatting
violation: Biome prefers the models.find(...) call on a single line.
Collapse it to satisfy the formatter.

Lore-id: 8f2e1c9a
Confidence: high
Scope-risk: low
Reversibility: revert-profile
Tested: biome check packages/ai/scripts/generate-models.ts
)

fix(memory): defer startup until model profiles settle
fix(ai): honor MiniMax M3 official 1M routes
…presets (Yeachan-Heo#3827)

feat(alibaba-token-plan): add qwen-deepseek profile preset
…puter suite (Yeachan-Heo#3767)

The SKILL says the computer-use red-team suite is "conditional, not
universal" and tells the agent to pick the surface that matches what the
change actually ships. The runtime is stricter than that: since Yeachan-Heo#3543 it
decides applicability from the computed change set and fails closed, so
any edit to a shared behavior registry demands the suite even when the
diff contains nothing computer-related.

Following the doc as written leads an agent to conclude the suite is
skippable, then hit COMPUTER_REDTEAM_CASE_MISSING at
`checkpoint --status complete` with no explanation of why -- and the
tempting way out is to invent the seven mandatory cases, which is
exactly what the gate exists to prevent.

Record the real rule instead: the suite is required for computer source,
the computer tool, the three shared behavior registries
(`config/settings-schema.ts`, `tools/index.ts`, `tools/renderers.ts`),
and any incomplete change-set capture; generated bindings, prompt/skill
docs and everything else do not trigger it on their own. Also state the
sanctioned way out -- supply a genuine suite or escalate for an
authorized override -- so the failure mode has a documented exit that is
not fabrication.

Path claims verified against `categorizeComputerChangePath` and
`isComputerControlSurfaceCategory` rather than transcribed by hand.

Docs only; no runtime behavior change.

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…xact unlink (Yeachan-Heo#3834)

* fix(notifications): resolve intermediate notifications dir only for exact unlink

Successor to closed Yeachan-Heo#3832. Multi-account layouts that share notifications/
via a directory symlink still need intermediate reparse points resolved
before native exact unlink can retire transition locks. Full-path realpath
followed final components and could race a final-component symlink swap
past native AT_SYMLINK_NOFOLLOW.

Canonicalize only the parent directory and rejoin the original basename so
native mutation still no-follows the final path component. Bump
DAEMON_GENERATION 49→50 and refresh the generation guard manifest for the
protected exactUnlinkNotificationFile change.

Lore-id: 3761act02
Constraint: final-component file symlink must remain reparse_point under TOCTOU
Constraint: do not claim full Yeachan-Heo#3761 closure -- bounded activation slice only
Rejected: full-file realpathSync | weakens final-component no-follow under race
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: issue-3761-symlinked-notifications-activation; generation-50 pin; generation-guard suite; coding-agent check
Not-tested: live Linux multi-account Telegram bot inbound round-trip

Refs Yeachan-Heo#3761

* fix(sdk): lock registerToolSessionTransitionCleanup out of public inventory

Yeachan-Heo#3813 added AgentSession.registerToolSessionTransitionCleanup for shared
artifact-manager ownership on session transitions. The method is an internal
lifecycle registration seam, parallel to registerToolSessionCleanup, but was
never added to LOCKED_EXCLUSIONS. Exact-head Dev CI then fails the generated
SDK operation inventory check on any PR based on current dev.

Classify the seam as a locked exclusion and regenerate the committed matrix.

Lore-id: 3813seam01
Constraint: do not expose as a public SDK control
Rejected: map to a new SDK operation | no user-facing control exists
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: sdk-operation-inventory.test.ts (17 pass); inventory --check
Not-tested: full coding-agent shard matrix on CI

Refs Yeachan-Heo#3813
…eo#3840)

A Darwin crash during exact replacement cleanup can leave an empty exchange placeholder at the canonical receipt name, causing every later append to fail closed. Reconcile that state only when the receipt quarantine identity proves the placeholder is stale, while continuing to reject substituted non-empty receipts. Also migrate version-one receipts left by interrupted upgrades.

Lore-id: 6f4a2c91
Constraint: never delete a canonical replacement receipt unless its detached identity is proven
Constraint: preserve live transcripts and quarantined receipt payloads during recovery
Rejected: trust the canonical placeholder by filename | an attacker or concurrent writer could substitute a live entry
Confidence: high
Scope-risk: focused
Reversibility: simple
Tested: Darwin native receipt recovery, legacy v1 receipt migration, managed replacement regressions, typecheck, Biome
Not-tested: concurrent multi-process replacement stress
…eachan-Heo#3831)

Configured legacy retry.* could re-issue a turn after partial assistant
text, thinking, or tool-call content had already crossed the public stream
boundary (e.g. proxy response.failed / upstream_stream_error). That path
only applied replay-safety checks to first-event timeouts and bare
defaults, so unclassified failures still entered bounded unknown retry.

Apply one universal automatic gate: once the failed attempt carries
observable assistant content, non-managed auto-retry returns false.
Content-free clean failures keep existing bounded/unbounded policy;
managed provisional discard, credential rotation, first-event scope
checks, and manual /retry remain unchanged.

Lore-id: 3791a1b2
Constraint: must not invent a parallel retry classifier -- extend existing content/replay helpers
Constraint: managed provisional discard and content-free credential rotation must remain retryable
Rejected: terminal special-case for upstream_stream_error only | same bug class under any code/message
Rejected: blanket hasCleanRetryReplaySafety for all legacy retries | over-blocks mid-turn after prior tool results
Confidence: high
Scope-risk: narrow
Reversibility: easy
Directive: do not auto-continue committed partial responses as retry
Tested: agent-session-resilient-retry (Yeachan-Heo#3791 cases) + retry-fallback + manual-retry + coding-agent check
Not-tested: live Responses proxy partial-then-failed e2e; extension-only current-attempt effects without content
Supersedes: none

Fixes Yeachan-Heo#3791
…nses/Azure setup (Yeachan-Heo#3829)

* fix(ai): delegate lazy stream watchdogs to transports

The lazy provider wrapper watched normalized assistant events while several providers already watched richer raw transport events. That second clock could expire after transport-only progress, replace a live response with a blank generic stall error, and race provider-specific failure handling.

Providers with raw watchdogs now own timeout decisions; the shared wrapper remains for providers that need it. OpenAI Completions now honors caller idle overrides internally, Azure shares the semantic Responses progress filter, and provider-owned paths preserve caller cancellation.

Lore-id: e4a32f9c
Constraint: providers without raw transport watchdogs retain the shared lazy watchdog
Rejected: raise the global timeout | masks watchdog ownership and delays genuine stalls
Confidence: high
Scope-risk: medium
Reversibility: code-only
Tested: packages/ai check; 2153 package tests and 10224 assertions; 115 timeout and concurrency tests
Not-tested: live provider outage recovery

* fix(ai): bound Responses/Azure setup to first-event timeout

Delegating lazy-stream watchdogs to transports removed the outer
first-event clock, but Responses and Azure only armed their idle
iterator after create() returned. A never-resolving pre-headers fetch
could then wait the SDK default (10 minutes) before any provider
watchdog existed.

Map streamFirstEventTimeoutMs into the OpenAI/Azure SDK request timeout
via a shared helper (Completions parity), and normalize pre-connect
Azure SDK timeouts to typed stream_first_event_timeout. Keep
transport-owned idle progress after the stream arms.

Lore-id: b7c4e19a
Constraint: keep provider-owned raw-event idle after create() returns
Constraint: Completions-style explicit-vs-fallback SDK timeout rules
Rejected: re-enable outer first-event for all provider-owned paths | dual clocks race transport progress
Confidence: high
Scope-risk: medium
Reversibility: code-only
Tested: packages/ai check; openai-first-event-timeout, register-builtins, stream-timeout-defaults
Not-tested: live Azure/Responses hung-header outage recovery
Slack's conversations.replies endpoint rejects JSON request bodies with
invalid_arguments even though equivalent form-encoded requests succeed.
Serialize all Slack Web API POST parameters with URLSearchParams, omit
undefined fields, and pin the request contract with a regression test.

Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: Slack provider and daemon 60/60; coding-agent check; CLI smoke

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
PR Yeachan-Heo#3806 flattened time-dependent loaders to 80 ms everywhere, making the working shimmer visibly step on direct terminals. Restore the 16 ms color cadence only for direct local sessions while keeping SSH and multiplexers on the bounded cadence and retaining congestion-based frame drops.

Lore-id: tui-local-shimmer-cadence
Constraint: SSH and multiplexed terminals must retain 80 ms decorative churn bounds
Rejected: revert Yeachan-Heo#3806 and Yeachan-Heo#3814 | restores slow-terminal stale-frame backlogs
Confidence: high
Scope-risk: narrow
Reversibility: clean-revert
Tested: loader cadence and scheduler congestion tests; TUI and coding-agent package checks
Not-tested: visual comparison across physical terminal emulators
Restores the 16 ms time-dependent loader cadence on direct local terminals while retaining 80 ms transport bounds and congestion dropping for SSH and multiplexers.
Successor to closed PR Yeachan-Heo#3700, rebased on current dev.

- Shared durable Telegram topic authority with generation-CAS convergence, durable pre-create claims, lease-fenced effects, and host-qualified file locks (owner_host_id) that fail closed for foreign hosts.
- Requires ok === false, error_code === 400, and an exact allowlisted Telegram description before treating an archive error as idempotently settled; identical TOPIC_NOT_FOUND text under 401/403/429/500 remains archive_pending with a durable bounded retry job.
- Remote archive closes daemon-created topics (closeForumTopic) without deleting retained records; user-created topics are never closed/removed.
- Crash-atomic topic registry persistence (fsync file+directory, native Windows exact write-through replacement); versionless legacy state is quarantined, never interpreted as empty.
- Isolated owner-backed validation-supergroup mode for bot-API testing that persists nothing.
- Generation 51 / serving epoch 5; exact-generation ownership compatibility (fail-closed).
`gjc update` fetched `https://registry.npmjs.org/@gajae-code/coding-agent/latest`
directly, and so did the interactive startup version check. The install that
follows shells out to bun/npm and therefore already honors whatever registry the
user configured, so on any network that mirrors or blocks the public registry the
two halves disagreed: the check died with `Failed to fetch release info:` — empty,
because an intercepting proxy returns a status with no statusText — while the
install it was gating would have succeeded.

Resolve the registry before the request, the way npm does, and attach the
credentials registered for it by walking the registry path up one segment at a
time like npm's nerf darts. A configured-but-unusable registry now throws instead
of quietly falling back to the public one, which would reintroduce the bug.

Lore-id: 7c3f9a1e
Constraint: the version check must reach the same registry the install shells out to
Constraint: a repository the user merely cloned must not choose the host this process talks to
Rejected: read <cwd>/.npmrc like npm does | a hostile repo could name the destination and supply an ${ENV}-expanded token to send there; npm skips project config in global mode anyway, and this check gates a global install
Rejected: read process.env directly | Bun merges cwd/.env into it, so the same repo-controlled path returns through the environment; $credentialEnv is the boundary this repo already established for exactly that
Rejected: shell out to `npm config get registry` | adds a process spawn to interactive startup
Rejected: fall back to registry.npmjs.org when the configured mirror fails | converts a deterministic misconfiguration into an intermittent one and leaks the request to a host the user excluded
Directive: do not promote a userinfo-only registry URL to a credential — `https://trusted.example.com@attacker.example.com` is that shape
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: bun test packages/coding-agent/test/npm-registry.test.ts
Tested: bun test packages/coding-agent/test/update-cli.test.ts packages/coding-agent/test/startup-update-contract.test.ts
Tested: bun run check:tools and tsc -p packages/coding-agent/tsconfig.json --noEmit
Tested: live resolution against a corporate network that returns HTTP 503 for registry.npmjs.org, reaching the Nexus mirror named in the user .npmrc
Not-tested: an authenticated mirror end to end; credential attachment is covered only by unit tests

Co-authored-by: Claude <noreply@anthropic.com>
Yeachan-Heo and others added 28 commits August 7, 2026 17:29
Lore-id: b3d7a218
Confidence: high
Scope-risk: narrow
Reversibility: trivial
…pt-guidence-autoroute-skill

feat: explicit-only skill routing — stop implicit workflow auto-invocation
The canonical replacement-receipt ENOENT guard had no regression at the exact readdir-to-open window, while the legacy test deleted its receipt before enumeration and never exercised the catch.

Inject disappearance from openSync so both canonical and legacy tests prove mutation continues only for benign absence.

Lore-id: canonical-receipt-preopen-enoent

Constraint: preserve fail-closed handling for identity mismatch and non-ENOENT failures

Confidence: high

Scope-risk: narrow

Reversibility: trivial

Tested: bun test packages/coding-agent/test/session-storage.test.ts -t replacement cleanup receipt reconcile TOCTOU resilience (4 pass)

Tested: bun --cwd=packages/coding-agent run check

Not-tested: full session-storage file clean; 4 unrelated writer-security failures remain in the local native build
Yeachan-Heo#3994)

A managed session scope is snapshotted in full on every session start, and
the snapshot fails closed once the tree exceeds the managed byte budget. The
scope is filled by GJC's own session records — tool logs, subagent
transcripts, artifacts — so a working directory in sustained use crosses the
budget without the operator doing anything unusual.

There is no signal before that happens. The first symptom is a launch that
aborts, and `gjc gc` (the command that already reports state the operator
cannot otherwise see) says nothing about it. On the machine this was found,
the scope reached 577 MiB against a 512 MiB budget with no prior warning.

Report scope usage from `gjc gc` once a scope is at or past 75% of the
budget. The probe only measures; nothing in the prune path acts on it, and
`gc` still reclaims no session records.

Deliberately conservative so existing runs are unaffected:
- Scopes below the threshold are omitted, so output is byte-identical for
  anyone not near the budget.
- Absent / non-directory / unreadable scopes report `unavailable` rather
  than failing the run.
- An unreadable subtree is skipped and the walk continues, because a partial
  total still answers "am I near the budget?".
- The walk is bounded, and a truncated walk is marked so the total is read
  as a floor rather than a measurement.

Refs Yeachan-Heo#3959. The capacity problem itself — no retention policy, and `gjc gc`
unable to reclaim session records — needs a maintainer decision and is left
out of this change.

Tests: packages/coding-agent/test/gc-session-scope.test.ts (6 new)
…Heo#3991)

* feat(ai): force Codex GPT-5.6 family context window to 372K

The OpenAI code backend raised its enforced usable prompt budget from
272K to 372K for the GPT-5.6 family (gpt-5.6 / -sol / -terra / -luna).
GJC still clamped these models to the old 272K cap, so status,
compaction, and the bundled catalog advertised a window 100K smaller
than the backend actually accepts, forcing premature compaction on
large sessions. Bump CODEX_GPT_5_6_CONTEXT_CAP fallback/ceiling to
372K, refresh the policy/discovery/manager tests, and update the
bundled openai-codex GPT-5.6 catalog entries so users without live
discovery see the same 372K window. Smaller observed live limits stay
authoritative; first-party OpenAI and non-5.6 codex variants are
untouched; the 272K long-context pricing threshold is unchanged.

Lore-id: 4f2c8e1a
Confidence: high
Scope-risk: low
Reversibility: config-only
Tested: ai policy/discovery/manager/thinking/cost/defaults suites; runtime probe of bundled openai-codex gpt-5.6-sol and discovery/final-cap paths
Not-tested: live OpenAI code discovery (no OAuth token in this environment); models.json regeneration (network-dependent, entry edit mirrors generator output under the new authority)

* test(ai): lock bundled Codex GPT-5.6 context at 372K

The policy unit tests cover the cap logic, but nothing asserted the
bundled openai-codex GPT-5.6 catalog entries themselves, so a stale
272K value in models.json could ship without any test noticing. Assert
the bundled gpt-5.6-sol/terra/luna entries advertise the 372K prompt
budget (128K max output) alongside the existing pricing coverage.

Lore-id: 7b1d90c3
Confidence: high
Scope-risk: low
Reversibility: config-only
Tested: openai-codex-default suite (3 pass)

* fix(ai): scope the 372K Codex fallback to the GPT-5.6 tier only

Raising CODEX_GPT_5_6_CONTEXT_CAP's shared fallback to 372K leaked the
new authority into non-5.6 Codex discovery rows: a gpt-5.5 or
gpt-5.6-codex row with absent/invalid context_window resolved to 372000
at the fetchCodexModels boundary, and the model-manager cache snapshot
is written before generated-catalog normalization, so a later failed
refresh could surface 372K for those models. Split the contracts:
CODEX_GENERIC_CONTEXT_WINDOW (272K) is the fallback for non-5.6 Codex
rows (live observations still pass through; the 272K pin stays in
model-thinking), while the 372K fallback/ceiling applies only to the
exact GPT-5.6 tier. model-thinking's three literal 272000 codex pins
now reference the shared constant instead of a second authority. Add
table-driven discovery coverage (all four 5.6 ids, absent/above/below
observations, non-5.6 controls) and lock the bundled 272K
long-context-pricing threshold.

Lore-id: 9c3e7d52
Confidence: high
Scope-risk: low
Reversibility: config-only
Tested: 7 targeted ai suites (56 pass); biome clean
Not-tested: live OpenAI code discovery

* feat(ai): force the Codex GPT-5.6 context window to 372K

Live OpenAI code discovery (verified with real OAuth credentials) still
reports context_window 272000 for gpt-5.6/-sol/-terra/-luna, so a
raised cap with "smaller observations authoritative" semantics would
leave the tier at 272K — a functional no-op for the brief's
"force-override ... to 372k". Change CODEX_GPT_5_6_CONTEXT_CAP to a
single enforced window (372K) applied as a hard override at all three
layers: discovery resolution, generated-catalog policy, and the final
model-manager cap. The generic 272K Codex budget (CODEX_GENERIC_
CONTEXT_WINDOW) still governs every non-5.6 codex row (gpt-5.5,
gpt-5.4-codex, gpt-5.6-codex, GPT-5.4 mini/nano); first-party OpenAI
and non-Codex transports are untouched; the 272K long-context pricing
threshold is unchanged. Canonical `bun run generate-models` reproduces
the committed 372K openai-codex entries (evidence recorded in the
ultragoal artifacts dir); the remaining generator diff is pre-existing
live-catalog drift and is intentionally not part of this PR.

Lore-id: 5f1a8b07
Confidence: high
Scope-risk: low
Reversibility: config-only
Tested: 7 targeted ai suites (56 pass); runtime probe (bundled + discovery + final cap); canonical regeneration reproduced the 372K entries
Not-tested: none material

* test(ai): lock invalid-observation and all-tier force coverage

The force-semantics suite covered representative numeric observations but
not the explicit acceptance matrix: invalid metadata (null, nonnumeric
string, zero, negative, NaN, Infinity) must resolve to the enforced 372K
window at every layer, and all four GPT-5.6 tier ids must be exercised at
the generated-policy and model-manager integration layers. Add the
invalid-observation matrix to the direct policy and discovery tests,
parameterize the generated-policy force test over all four tier ids x
{200K, 272K, 373K, 1.05M}, and add a manager-pipeline loop over every
tier id. Non-tier invalid metadata still falls back to the generic 272K
window.

Lore-id: 2a7c6f14
Confidence: high
Scope-risk: low
Reversibility: test-only
Tested: 7 targeted ai suites (59 pass); biome clean

---------

Co-authored-by: Yeachan-Heo <yeachan-heo@gajae.dev>
…dels (Yeachan-Heo#3988)

* feat(sdk): expose model profiles as synthetic gajae-code/<profile> models

External ACP/SDK clients (notably the paseo TUI) could only use presets via
the session-scoped startup Preset select, never as ordinary model choices,
and could not persist a preset as the global default. Selecting a preset
now behaves exactly like picking a model: Q10 models.list/current lists
every availability-filtered profile as a logical gajae-code/<profile> row,
and model.set on that namespace activates the profile live AND persists it
to global modelProfile.default (mirror of `gjc --mpreset <name> --default`),
with the same credential preflight, role clearing, flush, and rollback as
the CLI path.

The facade lives only in the Q10 projection and a shared resolver: no fake
Model registry entries, no TUI / --list-models / /v1-models / coordinator /
Q29 surface changes. Logical current derives exclusively from the in-session
active-profile marker (never the persisted default), cleared only on
successful concrete user/startup-override materialization (including the
scoped, unscoped, and role cycle paths and the interactive setModel
bindings); config.patch and activation serialize through the shared session
admission boundary, with a structurally-compared shadow that invalidates
profile-owned keys after activation while preserving unrelated patches;
availability uses the Q27 authenticated-provider derivation with
pattern-aware, managed-fallback-eligible default resolution, and degrades
fail-closed on registry, join, or per-profile resolution errors. gajae-code
is a reserved namespace with deterministic collision handling.

Planned via ralplan (run 20260806-201255; Architect CLEAR pass 3, Critic
OKAY pass 3) and executed under ultragoal with boundary-cohort gate and
terminal critic OKAY. Verified: 247/247 affected tests, package typecheck,
ACP core-v1 conformance 21/21, the four repo gates, and a live real-process
ACP smoke (paseo-shaped) proving selection, global persistence, fresh-launch
reapply, prompt execution on the profile default model, and non-leakage
into --list-models.

Lore-id: preset-to-model-sdk-facade
Directive: keep gajae-code reserved; document any future namespace change
Tested: Q10 synthetic projection, marker lifecycle, concurrency races,
  config.patch shadow freshness, registry-error fail-closed, ACP corpus,
  live ACP smoke
Not-tested: manual paseo picker render on a real device

* fix(sdk): keep session-only profile selection out of the successor session

Review of Yeachan-Heo#3988 found the advertised session-scoped guarantee did not hold
when no durable modelRoles.default exists -- the default state for the
ACP-only clients this feature targets. resolveConfiguredDefaultModel()
resolves with currentModel=undefined, so it returns undefined and the
restore guard in #initializeNewSessionState was a no-op, leaving /new to
record the profile's model as the successor's. Two sibling gates read the
merged settings layer where they authorize durable global writes.

Lore-id: 3f9c21ab
Constraint: the pre-profile model must be snapshotted by the caller -- activation replaces the runtime model before noteProfileInstalledOverrides runs
Constraint: first activation wins so chained session-only profiles restore the original selection, not the previous profile's
Rejected: read this.model inside noteProfileInstalledOverrides | setModelTemporary already overwrote it, so it captures the profile's own model
Rejected: change resolveConfiguredDefaultModel to accept a current model | the TUI resume flow depends on its current semantics
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: /new after a session-only profile with no durable default restores the pre-profile model and writes no modelRoles
Not-tested: rollback promotion under a concurrent project-config write

---------

Co-authored-by: probe <re2rar@gmail.com>
…se (Yeachan-Heo#4009)

`sticky-viewport-showcase.test.ts` was the slowest file in the repo at
~192s, 16% of the whole 759-file suite (total ~1228s) and the reason the
slowest CI shard ran 5.5x longer than the fastest.

Its `capture()` helper spawned a `bun` subprocess that re-rendered all 20
showcase frames, measured at ~9.4s (~2.5s interpreter boot + 20 x ~343ms
render). 17 cases called it, so ~187s of the ~192s was spent re-deriving
byte-identical evidence that each case then corrupted in its own way.

Capture once and hand out `fs.cp` copies. Determinism is not assumed: the
"reproducible within and across hosts" case captures three times through
`captureWithEnv` and asserts byte equality, so drift fails there rather
than silently sharing stale evidence. The single case that sets
`GJC_STICKY_VIEWPORT_ORACLE_COMMIT` before capturing calls the uncached
path, since a cached bundle would predate its mutation.

Lore-id: 3b7d0a94
Constraint: each case must keep a private, mutable bundle -- copies, not a shared dir
Constraint: captures observing mutated env/worktree state must stay uncached
Rejected: caching inside captureWithEnv | its whole point is per-env capture
Rejected: reusing one dir and restoring between cases | restore is exactly what the corruption cases defeat
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: 22 pass / 3388 assertions unchanged; sharing the dir without copying fails 9 cases; no temp-dir leaks; shard-8 369s -> 167s
Not-tested: CI-runner wall-clock (runner I/O differs from local)
…chan-Heo#4004)

* fix(sdk): key intent tracing on sub-sessions, not on having a UI

`_i` is a model-facing reasoning aid whose value becomes
`tool_execution_start.intent` for every consumer, but it was gated on
`hasUI`. Every headless top-level surface (ACP, print mode, SDK embedders)
therefore ran a different turn than the TUI on identical settings: the `_i`
guidance line was dropped from the system prompt and the field was stripped
from all tool schemas, so the model never stated a call's purpose and ACP
`tool_call.title` fell back to `tool: <path>` forever.

The gate came from Yeachan-Heo#1723 finding #8, whose stated goal was sparing
*sub-agents* the per-call token cost. `hasUI` was a poor proxy for that:
`createAgentSession` already computes `isCanonicalSubSession`, so the
omission now keys on it directly and surface shape no longer decides
prompt or tool-schema content.

Lore-id: 7c41e8b2
Constraint: sub-agents must keep the token saving -- key on isCanonicalSubSession
Constraint: tools.intentTracing / PI_INTENT_TRACING stay authoritative
Rejected: keep hasUI and special-case ACP | print mode and SDK embedders stay broken
Rejected: accept a boolean second arg for compatibility | existing hasUI callers would silently invert
Confidence: high
Scope-risk: medium
Reversibility: easy
Directive: do not reintroduce surface-shape gating for model-facing prompt content
Tested: prompt text and read-tool schema parity across TUI/ACP top-level sessions; sub-session omission
Not-tested: live provider turn quality difference

* test(hooks): use explicit $ultragoal token in delegate-flow tests

Yeachan-Heo#3992 restricted keyword autoroute to `$`-prefixed explicit tokens and
dropped bare skill names as triggers, but two `mcp-delegate-host-context`
cases still submitted `"ultragoal continue this objective"` and asserted
that it activates the workflow. Those cases have failed on `dev` ever
since; Yeachan-Heo#3992 verified the skill-state, definitions and prompt-template
suites and this file was not among them.

The tests are about delegate-flow host-context persistence, not about
routing, so they now use the canonical explicit token instead of
asserting the removed bare-name behavior.

Lore-id: 9f2ad5c1
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: mcp-delegate-host-context, gjc-skill-state-hooks, default-gjc-definitions, system-prompt-templates
…t ssh) (Yeachan-Heo#4008)

* feat(coding-agent): add SSH clipboard transport (--clipboard-transport ssh)

Adds an explicit, fail-closed SSH clipboard transport for WSL/remote/headless
sessions where the default OSC52+native path does not reach the actual
clipboard endpoint (e.g. WSL -> Mac over Tailscale).

- `clipboard.transport` (auto|native|osc52|ssh, default auto) and
  `clipboard.sshHost` settings, plus matching `--clipboard-transport` /
  `--clipboard-ssh-host` CLI flags. Precedence: CLI > config > auto. auto/
  native/osc52 are unchanged from current behavior.
- ssh mode: every copy/paste runs `ssh -o BatchMode=yes -o ConnectTimeout=3
  -- <host> pbcopy/pbpaste` via argv spawn (never a shell string). A 5s hard
  timeout covers the whole operation (connect, stdin write/end, stdout/
  stderr drain, exit) with guaranteed process/timer cleanup. Inbound bytes
  are decoded as strict UTF-8 (TextDecoder fatal:true) and the 1 MiB payload
  cap is enforced while streaming, before the stream is fully buffered, so
  an oversized or invalid response is rejected without unbounded memory
  growth. Outbound text rejects NUL bytes and unpaired UTF-16 surrogates
  before spawning. Explicit ssh failures (nonzero exit, timeout, invalid
  host, oversize/invalid payload) surface a sanitized, non-payload-bearing
  error and never silently fall back to native/OSC52.
- New `app.clipboard.pasteText` command-palette action (no default key, so
  it never collides with the existing image-paste binding) reads the
  configured ssh clipboard via pbpaste and inserts it at the cursor.
- 8 pre-existing fire-and-forget `copyToClipboard(...)` callsites (session
  dump, todo copy, debug log/SSE copy, composer copy-line/copy-prompt) now
  properly await/handle the promise so a copy failure cannot race ahead of
  its own success message.
- Regenerated `schemas/config.schema.json` for the two new settings.
- Docs: new docs/clipboard-transport.md, docs/keybindings.md entry,
  CHANGELOG entry under [Unreleased].

Tests: 298 passing (30 new focused: CLI flag parsing/precedence/injection
guards, SSH argv shape, UTF-8 fatal-decode rejection, 1 MiB streaming-cap
enforcement at and over the boundary, whole-lifecycle 5s timeout including
stdin-hang coverage, outbound NUL/surrogate rejection, exit-code/no-fallback
proof; 268 regression across command-palette, keybindings-audit, debug
viewers, and input-controller suites), tsc --noEmit clean, biome clean,
generate-json-schemas --check clean.

Focused tests:
  bun test packages/coding-agent/test/utils/clipboard-transport.test.ts
  bun test packages/coding-agent/test/cli/args-clipboard.test.ts
  bun test packages/coding-agent/test/utils/clipboard.test.ts
  bun run check
  bun run check:schemas

* fix(cli): register --clipboard-transport/--clipboard-ssh-host in RootHelpCommand

The Stage 3 PR registered these flags in commands/launch.ts's static flags
(the Command class oclif uses for actual parsing), but cli.ts also carries a
separate RootHelpCommand mirror used only by the fast --help/shell-completion
rendering path (per test/cli-help-load-order.test.ts's documented
single-source-of-truth contract). Found via the Stage 4 real-Mac-canary
manual verification pass: `gjc --help` omitted both flags even though
`--clipboard-transport ssh --clipboard-ssh-host mac` parsed and ran
correctly, and the generated shell-completion spec (test/completion-cli.test.ts)
was missing them too since it builds from RootHelpCommand.

Both flags now appear in --help output and the fig completion spec.
test/cli-help-load-order.test.ts and test/completion-cli.test.ts (18 tests)
still pass.

---------

Co-authored-by: GJC Ultragoal (P3-b productionization) <gjc-ultragoal@local>
…eo#4012)

The SDK host drops a session whose client has not ponged within
HEARTBEAT_TTL_MS (20s), but the ACP client inherited the transport's
one-shot reconnect defaults -- 3 attempts at a 25ms base backoff, a
total budget of 175ms. Any event-loop stall long enough for the host to
reap the session exceeded the client's entire retry window by two orders
of magnitude, so it surfaced as a terminal -32603 "ACP session transport
was lost: SDK WebSocket reconnect attempts exhausted". Under machine load
this killed eight long-running agent sessions inside a six-second window
while every one of their processes stayed alive.

The ACP adapter and the broker connection now share an explicit
ACP_SESSION_RECONNECT budget derived from HEARTBEAT_TTL_MS instead of a
magic number, and SdkClient gained a per-attempt backoff cap so a long
budget still probes often rather than sleeping through the outage.

Lore-id: 7c1f4a92
Constraint: client reconnect budget must outlive HEARTBEAT_TTL_MS -- a shorter budget makes every host-reaped stall unrecoverable
Constraint: no single backoff may swallow the TTL -- recovery must stay prompt once the host answers
Rejected: raise the transport defaults in bridge-client | correct for one-shot request clients, and every non-ACP caller already passes reconnectAttempts: 0 explicitly
Rejected: retry forever | a genuinely dead host must still fail in bounded time so ACP re-attach can run
Confidence: high
Scope-risk: narrow
Reversibility: easy
Directive: do not drop the reconnect options at the ACP call sites -- the adapter tests assert they reach the SdkClient
Tested: backoff clamping driven to exhaustion against a dead endpoint on a fake clock; budget passed through both the AcpSdkAdapter constructor and static connect paths; total budget asserted greater than HEARTBEAT_TTL_MS from the exported constants
Not-tested: recovery against a real host restart on a new ephemeral port
…an-Heo#4016)

Dev CI run 31205224428 went red on three shards; all three failures are
deterministic regressions introduced by the 4-commit batch 6ee4502..14ae92a,
not flakes (verified against base in a worktree; the fourth shard-2 failure is
a rare, non-reproducible flake left unmodified).

- /copy command (shard-8, Yeachan-Heo#4008): CommandController.#doCopy became promise-based
  (copyToClipboard(content).then(...)) but the tests assert showStatus
  synchronously, so the microtask-scheduled callback had not fired yet.
  Make the two tests async and flush with await Promise.resolve() x2.
- ACP reconnect exhaustion (shard-2, Yeachan-Heo#4012): the adapter now uses a ~40s
  reconnect budget (23 attempts) to outlive the host heartbeat TTL, so the
  exhaustion test can never finish in bun's 5s default. Inject a bounded
  client to keep the adapter's typed-rejection propagation fast; the full
  budget is covered under a fake clock by acp-session-reconnect.test.ts.
- StablePrefix fingerprint mismatch (shard-5, Yeachan-Heo#4004): intent tracing flipped
  from hasUI-gated to !subSession-gated, turning it on for harness sessions.
  importSnapshot re-normalized the cloned tool JSON, which drops function
  `intent` policies, flipping those tools from omit to optional `_i`
  injection and diverging the recomputed fingerprint. Verify the fingerprint
  against the stored, already-normalized tools instead; remove the
  non-idempotent normalizeImportedTools helper. The forked-child test also
  gets an explicit 30s bound (it hovers at bun's 5s default on slow runners;
  same flake reproduces at untouched base).

Lore-id: dev-14ae92-ci-repair
Tested: copy-command 4/4; sdk-acp-provider-reconnect 2/2 (170ms); replay 24/24;
append-only-context 66/66; append-only-mode 8/8; heap-eviction-retainers 6/6;
pagination 24/24 (10x rerun clean); agent + coding-agent tsc clean; biome clean
Not-tested: full Dev CI shard run (evidence: artifacts/dev-14ae92-ci-repair-receipt.json)
Confidence: high
Scope-risk: narrow
Reversibility: revertible

Co-authored-by: gaebal-gajae <gaebal-gajae@users.noreply.github.com>
…on-cli regression suite (Yeachan-Heo#4022)

Follow-up to Yeachan-Heo#4008 (merged 39b1d05). Architect review of the post-canary
RootHelpCommand delta (b119984) flagged that no test pinned the exact gap
that fix commit resolved: completion-cli.test.ts's real-surface assertion
list didn't include the two clipboard flags, so a future accidental removal
from RootHelpCommand would silently regress --help/shell-completion coverage
without any test catching it (cli-help-load-order.test.ts covers help-text
*sourcing*, not the flag table itself).

Verified this guard actually catches the regression it names: reverted
cli.ts to the pre-fix (b119984~1) state locally, confirmed the new
assertion fails exactly as expected (clipboard-transport suggestions:
undefined), then restored the fix and confirmed green. tsc --noEmit and
biome both clean.

Co-authored-by: GJC Ultragoal (P3-b productionization) <gjc-ultragoal@local>
…xecute (Yeachan-Heo#4017)

Both Command classes and their handlers existed, but neither entry was in
the `commands` registry in src/cli.ts. isSubcommand() therefore returned
false, so every documented verb (`gjc auth-broker serve|token|login|logout|
import|migrate|status`, `gjc auth-gateway serve|token|status|check`) was
rewritten to `launch` and billed a chat turn instead of executing the
credential action (issue Yeachan-Heo#3975). Adding the two lazy entries makes routing
and root help resolve them; the regression tests pin routeRootArgv so the
verbs can never reach the launch/chat fallback again.

Reachability-only fix: no auth semantics, credential handling, or secret
emission changed.

Lore-id: 3975-auth-cli
Constraint: registration must stay lazy (dynamic import) like every other entry
Constraint: no auth behavior changes beyond command reachability
Rejected: eager imports in cli.ts | defeats lazy startup, diverges from registry pattern
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Directive: keep auth-broker/auth-gateway out of the launch fallback path
Tested: 44/44 focused CLI routing tests; package biome + tsc clean; `gjc auth-broker --help` / `gjc auth-gateway --help` dispatch to command help, exit 0
Not-tested: live broker/gateway serve against a real credential store
Supersedes: n/a

Co-authored-by: gaebal-gajae <gaebal-gajae@users.noreply.github.com>
Codex can reject a named tool choice through either the initial HTTP response or a statusless SSE error even when the final request contained that tool. Keep the fallback Codex-specific, make the downgraded request sticky across every same-turn provider retry, and track its one-shot budget independently.

Lore-id: 3669-sse-tool-choice
Constraint: forced tool name must exist in final serialized tools
Constraint: retry only before output and outside managed fallback
Constraint: downgraded body must survive every later SSE reopen
Rejected: shared statusless classifier | broadens behavior across providers
Confidence: high
Scope-risk: narrow
Reversibility: easy
Tested: 50 focused tests, 287 assertions; packages/ai check; diff check
(cherry picked from commit bb9bce6)

Co-authored-by: Gajae Code Contributor <contributor@users.noreply.github.com>
Co-authored-by: probe <re2rar@gmail.com>
The immutable Anthropic provider source identity changed after the latest transport fixes, leaving the deterministic cache-placement artifact stale and Dev CI red.

Lore-id: 8c4e1b72

Tested: bun test packages/ai/test/anthropic-cache-eval.integration.test.ts
The catalog generator emitted valid but unformatted deterministic JSON, so the release check failed immediately after regeneration. Preserve the generated layout explicitly and remove a stale test constant that left the release gate warning.

Lore-id: 5d762a91

Tested: bun run ci:check:full

Tested: bun test packages/coding-agent/test/tools/tool-catalog.test.ts packages/coding-agent/test/smithery-env-trust.test.ts
…4028)

The committed cache-eval artifact still referenced the provider state before the JetBrains Junie provider change. Regenerating its immutable source identity restores the deterministic assertion without changing cache behavior.

Lore-id: 3350-devci-31241403107\nConstraint: Yeachan-Heo#4026 Codex path remains untouched\nConstraint: do not modify RevisionStore without lifecycle evidence\nRejected: retrying the SDK test | hides a runner-level EBADF\nConfidence: high\nScope-risk: narrow\nReversibility: artifact-only\nTested: anthropic-cache-eval integration test; sdk-query-pagination rerun-each 25 and 100

Co-authored-by: gaebal-gajae <gaebal-gajae@users.noreply.github.com>
Immediate shutdown under heavily contended CI could race a spill revision before terminal cleanup observed it. Reserve the write synchronously and give the filesystem-backed owner-intent matrix a contention-safe test budget.

Lore-id: 72ad39c4

Tested: bun test packages/coding-agent/test/sdk-query-pagination.test.ts packages/coding-agent/test/session-state-sidecar.test.ts

Scope-risk: narrow

Reversibility: easy
Directly await the revision write so a CI-only rejection retains its original error and stack instead of being collapsed into an opaque promise matcher failure.

Lore-id: 127fb9de

Tested: bun test packages/coding-agent/test/sdk-query-pagination.test.ts --test-name-pattern settles
Dev CI is green at 08ef163 and the release, CLI, SDK, generated-surface, smoke, and ci:check:full gates have passed.

# Conflicts:
#	.github/workflows/ci.yml
#	artifacts/issue-3670-anthropic-cache-eval.json
#	package.json
#	packages/ai/CHANGELOG.md
#	packages/ai/src/types.ts
#	packages/coding-agent/CHANGELOG.md
#	packages/coding-agent/scripts/generate-tool-catalog.ts
#	packages/coding-agent/src/modes/acp/acp-agent.ts
#	packages/coding-agent/src/sdk/acp/adapter.ts
#	packages/coding-agent/src/sdk/bus/index.ts
#	packages/coding-agent/src/sdk/host/session-runtime.ts
#	packages/coding-agent/src/sdk/models.ts
#	packages/coding-agent/src/sdk/session.ts
#	packages/coding-agent/src/session/agent-session.ts
#	packages/coding-agent/src/session/internal/managed-session-storage.ts
#	packages/coding-agent/src/tools/ask-contract.ts
#	packages/coding-agent/src/tools/descriptor-validation.ts
#	packages/coding-agent/src/tools/descriptors.test.ts
#	packages/coding-agent/src/tools/todo-write.ts
#	packages/coding-agent/src/tools/tool-catalog.generated.ts
#	packages/coding-agent/src/utils/clipboard.ts
#	packages/coding-agent/test/agent-session-openai-responses-replay.test.ts
#	packages/coding-agent/test/mcp-delegate-host-context.test.ts
#	packages/coding-agent/test/sdk-chat-daemon-worker.test.ts
#	packages/coding-agent/test/session/managed-lock-lease.windows.test.ts
#	scripts/release-policy.test.ts
Keep package versions, dated changelogs, generated plugin metadata, native sentinel, and release evidence aligned after the verified stable publication.
@twoimo

twoimo commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

Closing this fork-base duplicate; the replacement review is opened against upstream dev at Yeachan-Heo#4042.

@twoimo twoimo closed this Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.