Skip to content

fix(custody): make the withheld fallback refresh observable - #240

Closed
iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:fix/custody-gate-observability
Closed

iceteaSA wants to merge 2 commits into
cortexkit:mainfrom
iceteaSA:fix/custody-gate-observability

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Closes #239.

The problem

packages/opencode/src/index.ts decides at plugin construction whether to start the fallback-account background refresh. When it withholds, nothing observable says so — the process is indistinguishable from a healthy one.

Measured on main before this change:

'not-started' occurrences in index.ts .............. 1   (the assignment itself)
consumers of fallbackRefreshReady .................. 1   (__fallbackRefreshReady, a test export)
logger calls inside the gate block ................. 0
logger.{warn,info,debug}('claustrum', …) elsewhere . 16  ← the convention exists, this site opted out

The withheld state reached neither the log file nor sidebar state. Its only consumer was a test seam.

Why it matters

A boot-time flag gating a resource fails loud — the acquire blocks or errors. A boot-time flag gating a capability fails silent: nothing errors, a manager simply never exists, and everything downstream is merely quiet.

There is a live precedent for this exact gate. An earlier revision computed boot evidence as "every bound fallback has a usable resident credential right now", so a single cold account at boot returned provisional FAIL_CLOSED and suppressed startBackgroundRefresh() across all accounts — re-introducing a fleet-wide idle-expiry exposure. The logic was corrected to a structural-only gate; the silence was not.

The change

  • One logger.warn('claustrum', 'fallback refresh withheld at construction', …) at the decision, carrying the three dimensions that produced it (custodyMode, provisional, fallbacks) — so a reader can tell why it withheld, not just that it did. Follows the existing convention used at 16 other sites in the file.
  • A process-wide fallbackRefreshStructuralDark flag on SidebarState, using the house ...(cond && { field }) additive idiom.
  • Two tests pinning both signals.

The gate's boolean is unchanged. This attaches a signal to an existing decision; it does not re-decide it.

Verification

Three mutations, all re-run independently of the implementer's report, each restored to a clean tree before the next:

mutation result
delete the warn call withholding the fallback refresh logs the three dimensions that produced it — Received: undefined
remove the sidebar field the sidebar carries the structural-dark flag — Expected: true, Received: undefined
keep the fields, falsify their values ('local', false, 'X') same log test — - Expected - 3 / + Received + 3

The third was not in the brief. It distinguishes a test that asserts a line fired from one that asserts the line carries the right dimensions — without it, the log could regress to useless content while staying green. It reddens, so the test checks values rather than presence.

Gates on the restored tree: core 199/0 · opencode 1896/0 · typecheck clean · aft_inspect 0/0.

Note on the test harness

The sidebar test triggers its write via claude-account add-apikey rather than the loader's own sidebar write: in the dark state the loader refuses with RESUME_TAKEOVER before reaching that write. Harness choice only — no production write path was altered.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Closes #239. Makes the withheld fallback refresh observable so a process that silently stopped refreshing no longer looks identical to a healthy one. The gate's boolean is unchanged; only its visibility changes.

  • Logs a claustrum warning at the gate carrying custodyMode, provisional, and fallbacks, so the why is visible, not just the that.
  • Adds a process-wide fallbackRefreshStructuralDark field to sidebar state, published at construction and never cleared — it records the boot decision, not current darkness. The loader's own sidebar write is unreachable while structurally dark, so this boot publish exposes a warm-vault dark boot without waiting for a command.
  • Non-dark boots emit no warning and omit the field.

Tests cover dark and non-dark boots, include a non-dark control proving the signal is absent when the gate does not withhold, and poll for the fire-and-forget construction-time publish.

Written for commit 1bdb29d. Summary will update on new commits.

Review in cubic

RetriggerConfidence Score: 5/5

The PR appears mergeable, although the sidebar diagnostic can still become stale when multiple plugin instances share the same state file.

Findings

  1. P2 Shared signal can become stale ▶
Summary

The PR makes a boot-time decision to withhold fallback-account background refresh observable.

  • Logs the custody mode, provisional state, and fallback classification when refresh is withheld.
  • Adds and normalizes an optional structural-dark field in sidebar state.
  • Publishes the dark boot decision immediately and adds dark/non-dark coverage.
  • The previous review’s unresolved concern remains: independent plugin instances sharing a sidebar file can overwrite each other’s process-wide diagnostic state.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Plugin construction] --> B{Fallback refresh structurally dark?}
    B -->|No| C[Start background refresh]
    B -->|Yes| D[Emit diagnostic warning]
    D --> E[Publish structural-dark sidebar field]
    E --> F[Shared sidebar state]
Loading

Reviews (2) · Last reviewed commit: "fix(custody): close observability gaps i..."

@iceteaSA

Copy link
Copy Markdown
Contributor Author

Pushed 2225d81 — closes both SHOULDs from review. Independent review verdict was APPROVE (0 must / 2 should / 2 nit); these are the two SHOULDs.

SHOULD 1 — the tests only proved absence-detection

The original three mutations all deleted something, so they demonstrated the tests notice a missing signal and said nothing about a false one. With the gate forced unconditional at both sites:

index.ts  if (fallbackRefreshStructuralDark)      → if (true)
index.ts  ...(fallbackRefreshStructuralDark && …) → ...(true && …)

fallback-refresh-observability.test.ts   2 pass / 0 fail

A regression firing the warn in local mode, or making the sidebar always claim dark, was invisible to the whole 1896-test suite.

Added a non-dark boot emits no withheld warn and omits the sidebar flag — a claustrum boot with a tombstoned fallback (dimension T), asserting the warn record is absent and the sidebar key is omitted rather than false, since ...(cond && {…}) drops the key entirely.

Mutation polarity is the point: for a positive assertion you delete to redden; for an absence assertion you must add the forbidden behavior. Reverting the warn cannot redden a test that requires no warn.

SHOULD 2 — the boot case had no sidebar write

Review traced that the loader's own sidebar write is unreachable while structurally dark: reconcileCustodyStartup throws for every dark combination (TAKEOVER_INCOMPLETE_MAIN_REAL / RESUME_TAKEOVER / TAKEOVER_INCOMPLETE_SLOT_ABSENT) and the loader returns first. Warm-vault probe: no sidebar file. Cold-vault probe: refreshVaultBackedOAuthAccounts republishes and the flag appears.

So an operator who booted dark and ran no command saw nothing in the sidebar. Fixed with a 3-line publish at the end of the boot sequence via the existing refreshSidebarQuota helper — the same pattern as the cold-vault republish. The refusal path is untouched; the diff adds no return, no reconcile change.

Verification

Three mutations re-run independently of the implementer's report, tree restored to clean between each:

mutation result
unconditional gate, both sites a non-dark boot emits no withheld warn and omits the sidebar flag — Expected: false, Received: true
remove the boot write the boot decision reaches the sidebar without a command — Expected: true, Received: undefined
sidebar spread only unconditional, warn left gated same negative test reddens — Expected: false, Received: true

The third was not requested. It isolates the two halves of the negative test: without it, a single warn assertion could have been carrying the whole thing while the sidebar-absence assertion rode along untested. Each half fails on its own.

Gates on the restored tree: core 199/0 · opencode 1898/0 (1896 + 2) · typecheck clean · aft_inspect 0/0.

Nits

fallbackRefreshStructuralDark is a construction-time decision that is never cleared, so after recovery it still reads true — a lifetime comment now says so at the projection site, keeping the wire name aligned with the source variable rather than introducing a second term. The TUI not rendering the flag was left alone as out of scope for this PR.

@iceteaSA
iceteaSA force-pushed the fix/custody-gate-observability branch from 2225d81 to 47221cb Compare September 22, 2026 18:59
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Rebased onto main at v1.23.0. No conflicts; the patch applies unchanged.

Re-checked that the PR is still valid rather than assuming it. v1.23.0 reworked index.ts heavily (+618/−81) including zero-bind scoped custody and resumable consumer enrollment, so the gate this PR observes could plausibly have been restructured out from under it. It was not — the block is byte-identical to the version this PR was written against:

const fallbackRefreshStructuralDark =
  getClaustrumMode(initialStorage) === 'claustrum' &&
  provisionalCustody.provisional === true &&
  (fallbackDimensions.fallbacks === 'M' ||
    fallbackDimensions.fallbacks === 'R')
const fallbackRefreshReady = fallbackRefreshStructuralDark
  ? Promise.resolve('not-started')
  : fallbackManager.startBackgroundRefresh()

startBackgroundRefresh() still has exactly one call site, so this remains the only place the capability is silently withheld, and the withheld state still reaches nothing but a test export on main.

Pre-existing failures on this branch are upstream's, not this PR's

The full opencode suite shows two failures on this branch. Both reproduce on clean upstream/main with none of this PR's changes present:

  • AnthropicAuthPlugin > vault reauth leaves the account absent and projects the reauth state — passes in isolation, fails in-suite (cross-test pollution)
  • fallback Claustrum credential resolution > starts a durable enrollment ceremony in Claustrum mode without enabling scoped serving — load-sensitive; failed on one clean-main full run and passed on the next

Counts vary run to run (2 fails, then 1) on both this branch and main, which matches the CI non-determinism in #220. This branch adds no failure of its own: the failing set is identical with and without it.

Typecheck passes. Core is 295/0 on the new baseline.

One correction to my own method

My first read of this was "upstream v1.23.0 ships a broken build" — root typecheck and packages/core build both failed. That was wrong, and entirely an artefact of my tree: v1.23.0 adds @cortexkit/claustrum-client@0.3.0 and my node_modules predated it, so Cannot find module cascaded into 68 failures. After bun install, clean main typechecks and builds green. A missing-dependency signature is not a code defect, and I nearly filed it as one.

Comment on lines +4066 to +4071
// Set once at construction and never cleared: this stays true after the
// process recovers, so it records that the boot gate withheld the refresh,
// not that the refresh is currently dark.
...(fallbackRefreshStructuralDark && {
fallbackRefreshStructuralDark: true,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Shared signal can become stale

Each plugin instance adds this field based only on its own construction-time decision, but instances can share a sidebar file and each write replaces the complete state. A non-dark instance can therefore remove a dark sibling's signal, while a pending write from a disposed dark instance can restore stale true. This makes the new process-wide diagnostic unreliable in multi-project processes. Preserve or aggregate the flag across writers, and test mixed dark and non-dark instances sharing one sidebar file.

Knowledge Base Used:

The construction-time gate that withholds the fallback-account background
refresh when vault residency is structurally unsafe emitted no signal, so a
process that had silently stopped refreshing looked identical from outside
to a healthy one.

Emit one claustrum warn at the decision carrying the three dimensions that
produced it (custody mode, provisional flag, fallbacks dimension), and add a
process-wide fallbackRefreshStructuralDark flag to sidebar state so the
condition is visible on the wire. The gate's boolean is unchanged.
Add a non-dark control test: an unconditional gate (if (true) / ...(true &&))
previously passed the whole suite, so nothing proved the signal is absent when
the gate does not withhold. The new test asserts no withheld warn and that the
sidebar key is omitted entirely.

Publish the boot decision to the sidebar at construction. The loader's own
sidebar write is unreachable while structurally dark (the custody reconcile
refuses first), so a warm-vault dark boot left the sidebar silent until a
command ran. A poll-based test covers the fire-and-forget boot publish.

Document at the projection site that the flag is a boot fact, never cleared.
@iceteaSA

Copy link
Copy Markdown
Contributor Author

No longer applies: on main, startBackgroundRefresh() now runs unconditionally at index.ts:1952, so the structural gate this PR made observable is gone. Closing, and closing #239 with it.

@iceteaSA iceteaSA closed this Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custody boot gate withholds fallback background refresh silently — no log, no sidebar signal

1 participant