The bound-thread webhook persona path swallowed every pre-dispatch
failure (deleted webhook, revoked token, rate limit) and silently fell
back to the plain bot send. The fallback is intended — persona delivery
is best-effort — but the failure was invisible, so a broken webhook
binding degraded every reply in the thread with no operator signal.
Warn with the error before falling back. Post-dispatch failures still
rethrow (webhookSelected) to avoid duplicate sends.
The palette's session search swallowed every gateway error and rendered
the plain "No results" empty state — a failed search (e.g. sessions.list
rejecting with a store-needs-doctor migration error) was
indistinguishable from a successful search with zero matches. Silent
failure on a default path is the worst bug class in this repo.
Track a sessionSearchFailed flag (set only for the current request id
while the palette is open, cleared on every new query/clear), and render
"Chat search failed — check the gateway logs and retry" in the empty
state instead. Navigation commands stay usable throughout.
* feat(protocol): add named session icon glyphs
* feat(ui): add session glyph and custom emoji picker
* docs: describe named and custom session icons
* fix(ui): give the custom emoji input an accessible name
ClawSweeper P2: the input had no label relationship; assistive tech announced an unnamed edit control.
* fix(protocol): guard the v-flag icon regex for browser module loads
ClawSweeper P1: the picker's static import evaluates this module in the browser; pre-Unicode-Sets engines threw at module scope and took down the session menu. Lazy capability-guarded construction; such engines fall back to the grapheme heuristic as client pre-validation while the Gateway keeps exact RGI validation.
Both SQLite lifecycle producers have returned maintenanceReport: null and
unreferencedArtifacts: null since the legacy JSON store collapsed
(c440ae3e86), leaving cleanup-service carrying a 60-line dead ternary
branch, dead applied-count computations, and dead fallback reads. The
evict-budget cleanup action was equally dead: budgetEvictedKeys has been
an always-empty set since the SQLite flip (0a8e3604ba), so the action
could never be rendered.
Delete both fields from SessionEntryLifecycleMutationResult, collapse the
apply-summary to the single live branch, and drop the evict-budget action
and its always-empty plumbing through cleanup previews and the CLI
renderer. Disk-budget eviction reporting lives in the diskBudget summary
field, which remains.
Fork pull requests were pinned to GitHub-hosted runners while maintainer
PRs rode Blacksmith. That split arrived with ci.yml's first commit
(f4f990a) and was never a reasoned posture -- no rationale in docs,
commit bodies, or the ci-limits skill -- and this week's Blacksmith
tranches widened the gap: maintainer walls are ~3:36 while the last fork
PR I measured (#124633) took 13m0s. We already paid for it once in
#118530, which raised the cross-repo artifact budget to 35 minutes
because contributor PR #117992 timed out twice on hosted capacity.
Runner choice now follows author_association: OWNER, MEMBER, COLLABORATOR,
and CONTRIBUTOR get Blacksmith; FIRST_TIME_CONTRIBUTOR, FIRST_TIMER, NONE,
and MANNEQUIN stay on hosted runners, which are free for public repos, so
an unreviewed author cannot spend Blacksmith capacity. Earning the fast
path requires a landed commit, which requires a maintainer merge.
Note for anyone tempted to trim that list: maintainers report CONTRIBUTOR
here, not OWNER/MEMBER, because org membership is concealed -- steipete's
74 sampled PRs are all CONTRIBUTOR. Dropping CONTRIBUTOR would move
maintainer PRs to hosted.
Scope is deliberately runner-only: 27 runs-on clauses. The 34
dependency-cache/use-actions-cache conditions and 6 job ifs stay
fork-gated, because cache poisoning is a different risk from runner
choice -- a fork run still never writes an archive a trusted run restores.
Verified by evaluating all 25 configurable runs-on expressions: maintainer
PR 12/25 Blacksmith, returning-contributor fork 12/25 (identical), unknown
author 0/25, FIRST_TIME_CONTRIBUTOR 0/25, push to main 12/25 unchanged.
Guard tests gained trusted/untrusted fork cases; 119 pass.
The ACPX lazy runtime proxy optional-chained every forwarded hook and
fabricated success when the resolved runtime lacked one: doctor() returned
{ ok: true }, getStatus() returned {}, and setMode/setConfigOption/
prepareFreshSession silently no-opped. Combined with the session-reset
service calling prepareFreshSession through two raw optional chains, an
unregistered or incomplete backend made reset preparation a silent no-op:
the reset appeared to succeed while the backend kept resuming the old
conversation.
Contract decision: the SDK-level AcpRuntime type keeps optional hooks
(third-party backends legitimately omit capabilities, and core consumers
gate on presence), but every runtime the ACPX extension resolves
implements the full surface. A new CompleteAcpRuntime type in the
extension makes those hooks required for proxy-resolved runtimes, so an
absent hook is a compile error instead of a fabricated runtime success.
The now-dead legacy runTurn->startTurn adapter (~190 LOC) is deleted with
its tests; the proxy forwards startTurn directly.
The session-reset-service call sites consolidate onto the control-plane
owner helper tryPrepareFreshManagerRuntimeSession, which now records a
visible outcome for every skipped path (backend not registered, hook not
supported) instead of returning silently; that also covers the same
silent skip in manager.close-session.
Regression tests fail pre-fix: proxy hooks reject on contract-violating
runtimes instead of fabricating success, and skipped fresh-session
preparation is recorded.
The worker placement session evidence resolver swallowed every pipeline
failure into a bare catch that returned "unknown" for all placements.
Fail-open is correct for retirement safety, but the silent catch hid a
broken evidence pipeline (bad config, store corruption) behind indefinite
placement retention with no operator-visible signal. Record the fact at
the boundary that owns it: warn with the error before returning unknown.
Also collapse the byte-identical publish branches in
acquirePreparedModelRuntimeLeaseFromOwners: staleDynamicOwner and
missing-owner both published the exact same snapshot call; one branch
now owns publication with the invariant comment preserved.
When the local pairing fallback could not find the gateway's requestId, the
error always blamed a profile/state-dir mismatch and prescribed
--token/--password — even on gateways with no shared auth, and even though a
populated local pending list proves the CLI and gateway share one store and
the held id was merely superseded by a re-minted request. The supersession
case now names the current pending requestId with the exact approve command;
the mismatch hypothesis and shared-auth flags remain only for an empty local
pending list, phrased as possibilities. The fail-closed replacement-validation
paths keep the mismatch wording so they never point at an incompatible pending
request as a recovery step.
* fix(ui): surface the blocking reason when Enter cannot start a new session
On the New Session page, pressing Enter while any submit gate was active
silently did nothing: handleComposerKeydown returned when canSubmit was
false, and submitDisabledReason() only knew 3 of the ~14 gates canSubmit()
checked, so during transient states (async worktree/model preference
restore, WS reconnect, roster hydration) neither the tooltip nor the page
explained why nothing happened. Clicking Start seconds later worked.
Fix at the owner: replace the divergent canSubmit()/submitDisabledReason()
pair with one ordered gate table (submit-gates.ts). Every block is a typed
{gate, reason} entry; only the closed silent list (busy button, empty
draft) may omit a reason, enforced at the type level so a new gate cannot
silently eat Enter again. canSubmit, the Start tooltip, and the terminal
split-button all derive from the same walk.
Enter during a reasoned gate is now recorded as a blocked submit attempt:
the flow captures the gate, the composer renders the reason as a role=status
notice, and the notice retires itself as soon as that gate lifts (no queue,
no timers). Gates that already render a page callout (outcome-unknown,
invalid worktree name) are excluded to avoid duplicate text.
Regression tests: a gate-table completeness sweep asserting every blocking
scenario yields canSubmit=false plus a visible reason (fails if a gate is
added without one), Enter-while-restoring surfaces then clears the notice,
and composer-level coverage that reasoned gates consume Enter while silent
gates keep it native.
* test(ui): scope outcome-unknown e2e assertion to the page callout
The consolidated gate table now also surfaces the outcome-unknown reason
in the Start tooltip, so the bare getByText match resolved to 3 elements
under Playwright strict mode. Scope the wait to the role=alert callout.
Deliver same-target channel replies in inbound order while keeping steer admission concurrent. Record accepted steer/followup ownership so intentional deferred turns do not look silently empty.
Fixes#124618
* refactor(compaction): delete dead generic-fallback variant and prod-dead wrappers
The CompactionSummaryResult union's generic-fallback variant lost its
only producer in b942db4d56 (summarization failures now throw
CompactionError), leaving summarizeInStages returning kind:'summary'
unconditionally, a dead consumer branch in the safeguard claiming a
degradation path that cannot execute, and a test asserting the
impossible shape. Collapse the return type to string and delete the
branch + obsolete test.
Also delete three prod-dead helpers that survived only through the
safeguard's testing export (invisible to the dead-export lint):
isOversizedForSummary (planning uses its own inline threshold since the
oversized-plan refactor), capCompactionSummaryPreservingSuffix, and
formatPreservedTurnsSection (thin wrappers over budgetCompactionSummary
/ buildPreservedTurnsSection with no prod callers). Tests point at the
surviving primitives; the details-exclusion test retargets the real
owner (estimateMessagesTokens sanitization).
* test(compaction): update staged-summary assertion to string return
resolveExistingAgentSessionStoreTargetsReadOnlyResult added the
configured per-agent template target unconditionally and early-returned
on the first unavailable candidate. A configured store path that has not
been created yet (fresh config, store-migration window) therefore
reported database-missing for the whole agent even when the agent's
real sessions live in a readable discovered store.
Worker placement session evidence maps database-missing to 'absent',
and reconcile destroys environments for absent sessions — so live
placements referencing sessions in the discovered store were retired
and force-destroyed on restart (partial visibility proving absence,
which the evidence doctrine forbids).
Skip missing candidates and return database-missing only when no
candidate store exists; broken-but-present stores (schema/table/read
failures) still fail the whole agent so degraded state keeps mapping to
'unknown', never 'absent'.
* refactor(ai): give transport streams an honest writer type
* test(ai): use canonical transport stream fixtures
* fix(ai): preserve partial-less stream deltas
* fix(cli): announce when nodes list degrades to paired-only data
tryReadNodeList swallowed every enrichment failure, so the unfiltered nodes list silently rendered a table without connected/commands state. The fallback is now announced on stderr, keeping --json stdout parseable.
* fix(gateway): silently widen local pairing scopes as documented
`autoApproveLocal` has documented "silently approves pairing, role upgrades,
and scope upgrades from trusted local connections" since the loopback
auto-approval landed, but a later hardening pass forced every scope-upgrade
pairing request non-silent. That block protected nothing locally — silent
initial pairing grants a fresh identity arbitrary requested scopes, so any
local process could mint a new keypair instead of upgrading — while it
stranded every row-authorized client (CLI, native apps, node hosts) on a
manual approval no local surface could perform.
Scope upgrades now ride the same silent-local rule as initial pairing, with
one new restriction that encodes the real boundary: the connect must itself
prove local-grade credentials (auth mode none, or the shared token/password).
Identity-proxy connects (tailscale, trusted-proxy) and bearer device tokens
never did, so their pairing rows remain a durable scope cap, and
`autoApproveLocal: false` still forces manual approval for everything.
The silent self-grant also approves the union of requested plus already-held
scopes: approval merges the existing row back in, so a client requesting only
its missing scope no longer fails the caller-authority check.
The decision surface shrinks with the behavior change: the scope-upgrade veto
contradicted shouldAllowSilentLocalPairing's answer, the CLI-container
locality was a duplicate of the shared-secret-loopback predicate, and three
classifiers re-derived the same shared-secret auth check.
Live-verified on an isolated auth-none loopback gateway: a CLI identity
paired at operator.pairing silently widens to operator.read on the next wider
command, with the scope-upgrade security audit line still emitted.
* test(gateway): rewrite veto-era pairing locks for silent local widening
Five control-ui pairing suite cases and the silent-scope-upgrade poc locked
the removed non-silent veto. The suite cases now assert the new invariant
(local shared-auth upgrades widen silently, malformed and legacy-shaped rows
are repaired by the fresh approval, node-then-operator grants complete
without a stranded prompt), and the poc case now exercises the surviving
manual-approval gate by disabling autoApproveLocal after its watcher
connects, keeping the pairing-request broadcast and remediation-hint
assertions alive on a real remaining path. The voice-node bootstrap failure
was leakage from the aborted sibling tests, and passes again once they
complete their flows.
* fix(agents): allow required-preflight native Codex compaction
Required reply-preflight compaction on a Codex app-server-backed session
returns the intentional `ok: true, compacted: false` "codex app-server owns
automatic compaction" no-op because the preflight caller never passes
`allowNonManualNativeRequest`. The reply/preflight path then misclassifies
that successful skip as a failure and throws, dropping the user's turn with
"Context is too large and auto-compaction could not recover this turn." The
equivalent CLI path was fixed by #88207; this is the second, unpatched caller.
Route required-preflight through the existing private
`compactAfterContextEngine` harness capability (which already passes
`allowNonManualNativeRequest: true`) by adding a typed
`nativeCompactionRequest: "required_preflight" | "after_context_engine"`
origin on `maybeCompactAgentHarnessSession` and the Codex compact bridge.
The non-manual skip guard is bypassed for preflight, so Codex actually
compacts the thread.
A binding change between the initial read and the native request is a
stale-binding race, not a benign skip. For `required_preflight` (and the
non-manual CLI path) it now surfaces as the canonical recoverable
`stale_thread_binding` failure so the queued harness falls back to the
context engine instead of treating an uncompacted `ok: true` result as a
completed turn. A genuine post-context-engine request may still skip,
because the context engine has already compacted. Required-preflight is also
the one scoped exception to the model-locked terminal rule: missing or stale
Codex thread bindings recover via the shared context-engine fallback while
the persisted harness lock stays intact; other locked failures remain
terminal.
Rebased onto main after #120740 restructured the guarded native compaction
block; the recoverable-binding semantics are reintroduced on the new
structure and scoped by `nativeCompactionRequest` so #120740's
post-context-engine skip behavior is preserved.
Closes#119971.
* test(evidence): commit inspectable required-preflight live proof scripts for #119971
Adds the two live codex app-server proof scripts (binding-race +
locked-preflight) so the redacted terminal traces in the PR body are
inspectable on the exact head. Both drive the real codex binary and real
maybeCompactCodexAppServerSession with nativeCompactionRequest:
"required_preflight"; neither runs in CI (no codex binary).
* fix(agents): scope locked-preflight compaction fallback to Codex
Restrict the required-preflight model-lock exception to the Codex harness
so missing/stale thread bindings in other locked native harnesses (e.g.
Copilot) stay terminal instead of escaping the persisted model-lock
boundary via context-engine fallback. Add a model-locked Copilot
required-preflight regression covering both missing and stale thread
bindings.
* fix(codex): require native preflight compaction
* chore(plugin-sdk): account for native compaction exports
* test(codex): use complete cron authority fixtures
* chore(lint): shrink compaction assertion baseline
* fix(lint): honor root boundary timeout
* fix(lint): extend package boundary timeout
* fix(plugins): verify native compaction owner
---------
Co-authored-by: fuller-stack-dev <263060202+fuller-stack-dev@users.noreply.github.com>
Make validated CLI cache writes idempotent and reuse the startup-scoped node worker launch across route retries, keeping the signed menu-bar app near-zero CPU while idle.\n\nCloses #124592
Removing build-artifacts' unread job outputs left the step still writing
four `*-result` step outputs and carrying an `id` that nothing
references. The wave already reports failures through ::error
annotations and its exit code, so these were pure leftovers.
Re-simulated the step body with stubbed pnpm/node: normal run exits 0, a
failing verifier still exits 1.
Two pieces of dead workflow surface, found auditing where CI time goes:
- ci-timings-summary was hard-disabled (`if: ${{ false && ... }}`) with a
TODO to re-enable or delete it after the next timing-optimization
review. That review happened; the local `pnpm ci:timings` helper is what
we actually use, and docs already pointed there. The job carried a
25-entry needs list that had to be kept in sync to stay lintable.
- build-artifacts exported four `*-result` outputs that no job or workflow
reads.
Removing the job lets the gate guard assert the stronger invariant it
wanted all along: ci-gate needs *every* job in the file, so a new lane
cannot slip in ungated (28 jobs, 27 gated, zero exceptions).
No runtime behavior changes: the job could never run and the outputs had
no consumers. Also audited every `pnpm <script>` and `node scripts/...`
reference in ci.yml for rot -- all resolve.
* fix(ui): surface hidden-pane steer terminal failures globally
Three terminal branches in steer-lifecycle.ts (transport null result,
failed queue-row restore, failed queue-row removal) still gated their
error on itemStillVisible, so a steer that failed after the operator
navigated away parked the error on the queue row with no visible
outcome — the exact invariant #124473 introduced
surfaceChatDeliveryFailure() to protect.
Route all three through the canonical helper and delete the divergent
visibility-only branches. Regression test fails pre-fix
(stash-verified): steer transport failure with the pane hidden now
surfaces the session-named global toast.
* fix(logging): demote per-turn gateway log noise to debug
Live campaign evidence showed three lines dominating operator logs at
info level with no per-turn diagnostic value:
- 'tool policy removed N tool(s)': the policy pipeline runs on every
turn, so this repeated 42x in one session. Demote to debug and delete
the now-dead toolPolicyAuditLogLevel/auditLogLevel plumbing that only
existed to lower diagnostic probes to the level that is now the
default (net -13 production LOC).
- 'codex app-server one-shot cleanup checked shared client retirement':
routine per-attempt teardown detail; demote to debug.
- 'codex trajectory capture requires the SQLite host recorder': static
config condition warned per attempt; warn once per process.
Skipped: the [model-fetch] info carve-out in model-transport-debug.ts is
a named contract (docs/logging.md, #89648) — always-info by design.
* fix(codex): drop test-only trajectory warn-once reset export
Knip's production unused-export gate rejects
resetCodexTrajectoryRecorderWarningForTest — it was a test-only seam in
production code. Reset the process-wide warn-once flag via
vi.resetModules() + fresh dynamic import in the test instead.
* test(cron): wait for backoff re-arm instead of fixed sleep
The 0ms retry timer arms only after async watcher-state persistence, so
'await delay(5)' races it on loaded CI workers (flaked on
checks-node-compact-large-2: spawn called 1 time, expected 2). Replace
both fixed-sleep re-arm waits with vi.waitFor on the spawn count. The
remaining delay(5) guards a negative no-further-spawn assertion after
cancel, where a bounded sleep is the correct shape.
* fix(codex): scope trajectory recorder warn dedupe to session
ClawSweeper P2: the host recorder factory returns null for per-session
target-mapping conflicts, not only static config, so a process-wide
warn-once flag silenced a later distinct session's recorder loss. Warn
once per session (bounded set, cleared past 64 entries) so retries stay
quiet but each newly affected session records its loss. Regression
covers a later distinct session still warning.
The computer.act v1 wire contract is gone, but the naming that survived it
still described a version split instead of the real one: screen-coordinate
execution versus window/element-scoped execution. Both are live rungs of the
same ladder.
- Extract the screen-coordinate half of the 1334-line ComputerActionService
into ComputerScreenActionExecutor (dispatch, typing, scroll, coordinate
mapping, button-hold watchdog, raw CoreGraphics primitives). Moved code is
unchanged apart from threading the queue authority check as a parameter
instead of reaching back into the queue.
- ComputerActionService keeps its name and becomes the coordinator that owns
the execution queue, the permission probe, and the shared error vocabulary.
- Rename ComputerActionServiceV2 to ComputerWindowActionExecutor, isV2Request
to isWindowScopedRequest, isComputerActV2Only to isWindowScopedOnly, and
ComputerActionError.invalidV2Request to .invalidRequest. The emitted
COMPUTER_INVALID_REQUEST: prefix is unchanged.
- cua-computer: v2-actions.ts becomes window-actions.ts, handleV2Act becomes
handleWindowAct, and the stale v1Params local in handleDesktopAct becomes
desktopParams.
- Note at the computer.act idempotency key that its v1 prefix versions the key
composition, not the wire contract.
Behavior-neutral: no logic edits, no new branches, no changed error strings.