fix(telegram): harden durable topic authority on current dev - #31
Closed
twoimo wants to merge 1219 commits into
Closed
fix(telegram): harden durable topic authority on current dev#31twoimo wants to merge 1219 commits into
twoimo wants to merge 1219 commits into
Conversation
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(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>
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.
Owner
Author
|
Closing this fork-base duplicate; the replacement review is opened against upstream dev at Yeachan-Heo#4042. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replacement for the closed Telegram topic-authority review, rebased onto current
upstream/dev(3bddcc579).Verification
git diff --check: passedThe prior PR Yeachan-Heo#3700 is closed; this branch is published separately for fresh maintainer review. No merge is requested by this session.