Conversation
There was a problem hiding this comment.
All reported issues were addressed across 8 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
5659207 to
5929741
Compare
There was a problem hiding this comment.
All reported issues were addressed across 10 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
5929741 to
b90645b
Compare
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
4dfbf7f to
e2af7f6
Compare
|
CI is green at Two earlier runs on this same tree went red on two different tests (an e2e notice wait, then an async migration capture). Three identical trees, three different outcomes — filed with the evidence as #220. Neither failing test touches this change, and the amends between the three commits are empty diffs. Ready for review behind #219. |
|
Addressed the review findings and rebased onto the updated #219.
core 192/0 · opencode 1875/0 · pi 114/0 · typecheck, format, biome clean. |
602fca7 to
42c8e74
Compare
|
Bisecting on CI — temporarily dropped the Four login/custody tests fail identically on two pushes of a byte-identical tree, while passing locally (1875/0, including the exact root Restoring the commit after this run regardless of the result. Not ready to merge until this is explained. |
|
Restored the That test replaced the core module process-wide ( This is documented in-tree at Flagging a limit honestly: the leak does not reproduce on my machine even with the polluting file ordered first in one process, so the fix is structurally sound rather than locally demonstrated. CI is the discriminator — it failed identically twice before, so a green run here is the real signal. Worth noting the class: this is invisible to single-file runs, to |
There was a problem hiding this comment.
All reported issues were addressed across 1 file (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
6382971 to
59c1988
Compare
|
Squashed to one commit on top of #219 ( CI on this tip is red on exactly one test — I dug into the remaining one and updated #220: it is not a timeout to raise. At a 120s wait the Not fixing it here — it is unrelated to the credential-id change and the fix belongs in that delivery path. Flagging rather than re-running CI until it happens to go green. |
|
Added provider scoping for manifest credential ids, squashed into the single commit ( Dropping the label derivation left the id unscoped — only a non-empty-string check remained — so an id belonging to another provider parsed inside our own block. The manifest is a co-tenant file, so that matters: a sibling plugin writes its own block beside ours. The rule scopes on the provider segment, not on a kind prefix. My first attempt required So: the provider must be the id's second colon-separated segment, the kind is deliberately unconstrained, and the label remains unconsulted. Verified against the shared contract's conformance rows on the real parser: Not reachable in production before this: the handle is the real capability, each block is written only by its own tenant, the file is 0600, and the runtime fence already refuses on mismatch against what the vault returns. It was a check that used to exist and no longer did. Also added a comment where the test fixtures mint ids, recording that main's real vault id is the unlabelled core 199/0 · opencode 1875/0 · pi 114/0 · typecheck, format, biome clean. |
59c1988 to
bd0e262
Compare
…l clock The lock TTL serves two opposed roles: it is both the contender's give-up deadline and the holder's staleness threshold. Raising it cannot make these tests deterministic: a starved holder can still become evictable, while the longer contender wait can overrun Bun's 5000ms watchdog. Use injected clocks and explicit barriers instead. A synthetic fresh owner lets the lock_busy test advance from fresh-owner inspection to deadline exhaustion without elapsed time. Startup migration tests suppress only the test-observed 100ms warmup escape, and concurrent migration waits on entered/release/rename barriers rather than sleeps. Production behavior is untouched. Under 16 CPU hogs, the unmodified tests were 0/10 and included semantic failures such as 'Expected promise that rejects / Received promise that resolved'. After the change, no lock assertion failed; remaining red runs were exclusively Bun watchdog kills followed by temp-directory cleanup cascades. Green runs clustered below 100ms (the concurrent case occasionally took longer when descheduled), while watchdog failures began at 5.4s. The direct mkdir/write/read lock_busy test was once reported at 7588ms under two-core oversubscription, proving that extreme-load gate measured scheduler starvation rather than lock semantics.
bd0e262 to
a309e50
Compare
|
Stack, for merge order. Four PRs came out of chasing three separate CI failures to root cause; each fix is in its own PR rather than folded into the custody change. Suggested order: #219 → #228 → #229 → #218. Each rebases cleanly on the previous. This PR: the feature PR. Now stacked on #219 -> #228 so the lock flake no longer blocks it. Its own contribution is the single top commit The three CI failures were unrelated to each other: a Pi suite reading the operator's live config (#219), manifest-lock tests racing a wall-clock TTL (#228), and a genuine product bug where the model-restored notice never reaches a session with no TUI attached (#229). |
…y turn The switch notice was still inside promptAsync when the final cache warm queued the restoration notice. OpenCode published that ignored notice as a user message and a busy status, revoking the idle-delivery lease; the queued restoration then had no later idle event to release it. Track plugin-generated notice IDs separately from genuine user messages, then re-enter the existing bounded status probe after a successful noReply insertion only when no genuine user message arrived. The live status check and final lease check still gate insertion, so an active prompt cannot adopt the ignored message and duplicate a billed provider turn.
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Confidence score: 5/5
- The rewritten concurrent-migration test in
packages/opencode/src/tests/index.test.tsno longer has fail-fast timeout races, so a stalled connector or missing migration event could leave the test hanging instead of reporting a clear failure—restore the timeout guards around the awaited promises.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/tests/index.test.ts">
<violation number="1" location="packages/opencode/src/tests/index.test.ts:3068">
P3: The rewritten concurrent-migration test dropped the fail-fast timeout races that the previous revision had around `entered.promise` + `release.resolve()` and `secondManifestRename.promise`. If the connector never issues the second credential.get, or the second manifest rename never happens, the test now hangs silently until the suite timeout instead of failing fast with a descriptive error; the previous code raced against `Bun.sleep(1_000)` with specific messages. Restore the bounded races to keep regression diagnosis fast.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| pluginBPromise, | ||
| ]) | ||
| }) | ||
| await secondManifestRename.promise |
There was a problem hiding this comment.
P3: The rewritten concurrent-migration test dropped the fail-fast timeout races that the previous revision had around entered.promise + release.resolve() and secondManifestRename.promise. If the connector never issues the second credential.get, or the second manifest rename never happens, the test now hangs silently until the suite timeout instead of failing fast with a descriptive error; the previous code raced against Bun.sleep(1_000) with specific messages. Restore the bounded races to keep regression diagnosis fast.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/tests/index.test.ts, line 3068:
<comment>The rewritten concurrent-migration test dropped the fail-fast timeout races that the previous revision had around `entered.promise` + `release.resolve()` and `secondManifestRename.promise`. If the connector never issues the second credential.get, or the second manifest rename never happens, the test now hangs silently until the suite timeout instead of failing fast with a descriptive error; the previous code raced against `Bun.sleep(1_000)` with specific messages. Restore the bounded races to keep regression diagnosis fast.</comment>
<file context>
@@ -3032,21 +3036,36 @@ describe('fallback Claustrum credential resolution', () => {
+ pluginBPromise,
+ ])
+ })
+ await secondManifestRename.promise
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as {
providers: Array<{
</file context>
A derived identifier encodes a convention as a constraint. The custody handle manifest derived the expected vault credential id from the account label (`oauth:anthropic:<label>`) and rejected any entry that disagreed, so the parse-time comparison could only catch two fields disagreeing that the same writer wrote together — a corruption check, not an authorization check. It blocked a live flip. Main's vault credential id is the unlabelled provider-default `oauth:anthropic`: writing the true id gave `corrupt-binding` at parse, writing the derived id gave `credential_identity_mismatch` at runtime because the vault returns the real one. Any credential whose id does not match its label hits this. Carry the identifier verbatim through every layer that touches it — parse, resolve, write, remove, and the local-exit binding — and verify it against the vault at use time. Without the write and remove halves the plugin could not create main's entry, and could never clear it, so a local re-login would silently leave the account bound. The derivation survives only where no id exists to read: legacy handle-file migration, whose comment now states the checkable reason it is safe rather than implying a permanent property. Dropping the derivation left the id unscoped, so an id belonging to another provider parsed inside our own block. The manifest is a co-tenant file — a sibling plugin writes its own block beside ours — so the parser now requires the provider to be the id's SECOND colon-separated segment. The kind prefix (segment 1) is deliberately unconstrained: `oauth`, `chatgpt`, `antigravity` and `apikey` are all in use today and the set is open, so constraining it would reject real credentials. The label (segment 3+) remains unconsulted — provider scoping and label derivation are different properties, and only the second was ever wrong. Label becomes the sole lookup key, so duplicate labels within a provider block are now rejected — every entry sharing one is marked corrupt rather than silently picking a winner. The runtime identity fence skipped when the vault omitted a credential id, which would have left no identity check at all once the parse-time derivation was gone. It now refuses. The current daemon always supplies the id on a successful get, so the branch is unreachable today. The preflight passed `refresh_floor + 30min` (~4.5h) to `cache.get`. In Claustrum that is a staleness floor: a get below it forces an upstream refresh, so a readiness check rotated the credential it was about to use. Because the vault refreshes on a cadence close to the token lifetime, main sat below that floor for ~56% of every cycle. Replaced with a five-minute serving margin; the vault owns rotation. The credential-id lookup on the custody re-entry path was uncaught, so an unreadable or partially-written account file rejected the auth callback and discarded a just-exchanged credential — the failure lands exactly when someone is re-logging in to recover. It now falls through to the missing-entry branch the surrounding ternary already handles. The sibling lookup in `acknowledgeMainLocalLogin` had the same shape. The `TAKEOVER_INCOMPLETE_MAIN_REAL` guidance named `ck auth migrate-plugin`, which exists on no deployed Claustrum binary.
a309e50 to
c12bc60
Compare
|
Re-stacked into a single linear chain so every PR can go green independently: Merge in that order. The earlier shape had #228/#229/#218 as siblings on #219, which left #218 red on the e2e notice bug that #229 fixes — a failure that was never its own. Local gates on the chain tip: core 199/0 · opencode 1875/0 · pi 114/0 · e2e 36/0 · typecheck clean. |
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Confidence score: 4/5
packages/opencode/src/index.tsretains revision state for every session in a long-lived process, allowing unbounded memory growth over time; apply the existing 128-session eviction policy todesktopNoticeUserRevisions.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/index.ts">
<violation number="1" location="packages/opencode/src/index.ts:5320">
P2: In a long-lived OpenCode process, every retained session adds revision state indefinitely because `desktopNoticeUserRevisions` has no eviction policy. Bound this map with the same 128-session policy used for desktop-notice state, or remove revisions when no notice is pending.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| typeof info.id === 'string' && | ||
| desktopNoticeMessageIds.get(sessionId)?.has(info.id) | ||
| if (!isDesktopNotice) { | ||
| desktopNoticeUserRevisions.set( |
There was a problem hiding this comment.
P2: In a long-lived OpenCode process, every retained session adds revision state indefinitely because desktopNoticeUserRevisions has no eviction policy. Bound this map with the same 128-session policy used for desktop-notice state, or remove revisions when no notice is pending.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/index.ts, line 5320:
<comment>In a long-lived OpenCode process, every retained session adds revision state indefinitely because `desktopNoticeUserRevisions` has no eviction policy. Bound this map with the same 128-session policy used for desktop-notice state, or remove revisions when no notice is pending.</comment>
<file context>
@@ -5251,21 +5311,32 @@ const anthropicAuthPlugin = async (
+ typeof info.id === 'string' &&
+ desktopNoticeMessageIds.get(sessionId)?.has(info.id)
+ if (!isDesktopNotice) {
+ desktopNoticeUserRevisions.set(
+ sessionId,
+ (desktopNoticeUserRevisions.get(sessionId) ?? 0) + 1,
</file context>
# Conflicts: # packages/core/src/claustrum.ts # packages/core/src/tests/claustrum.test.ts
|
The custody credential-ID patch is integrated on |
Found by attempting the custody flip on a live deployment. The preflight refused, correctly, and the refusal was unfixable from config — the main account's vault credential id cannot be expressed in the handle manifest at all.
The blocker
The manifest binds an account label to a Claustrum credential id. The parser derived the expected id from the label (
oauth:anthropic:<label>) and rejected any entry that disagreed. Main's credential lives on the unlabelled provider-default lane,oauth:anthropic, so both encodings refuse:credential_idoauth:anthropic(the real one)corrupt-bindingat parseoauth:anthropic:main(the derived one)credential_identity_mismatchat runtime — the vault returns the real idVerified against the parser before writing anything, and again after the fix:
Not specific to main.
antigravity:googleis unlabelled the same way; anything whose id does not happen to match its label hits this.Why carry the id rather than special-case main
A derived identifier encodes a convention as a constraint. Conventions change; parser constraints do not.
The parse-time equality check compared the manifest against itself — two fields the same writer wrote in the same breath. It can catch corruption, not a wrong id. The check that means something is the runtime fence in
custody-mode.ts:That one has vault ground truth on one side. It is unchanged here. Dropping the derivation loses no failure path: a typo'd id gets
not_foundfrom the vault (uniform mask, fails closed), a mismatched id gets the fence.Special-casing
mainwould leave the derivation in place and add an exception to it — working today, broken again at the next unlabelled credential.Changes
readCustodyHandlescarriescredential_idverbatim; all other validation (handle shape, label validity, superseded parsing, corrupt-label path) is untouched.resolveCustodyHandlematches on label and returns the entry's own id.corruptLabels. This replaces what the derivation was incidentally doing.refresh_floor + 30min(~4.5h) tocache.get. In Claustrum that value is a staleness floor — a get below it forces an upstream refresh, so a readiness check silently rotated the credential it was about to use. Because the vault refreshes on a cadence close to the token lifetime, a credential sits below a 4.5h floor for most of its cycle: main is below it for ~4.5h of every ~8h. Replaced with a 5-minute serving margin (CUSTODY_PREFLIGHT_MIN_TTL_MS). The vault owns rotation; the preflight only needs the credential to serve the takeover.TAKEOVER_INCOMPLETE_MAIN_REALpointed atck auth migrate-plugin, which exists on no deployed Claustrum binary — it is parked on an unmerged branch. Reworded to the supported path (ck auth mint-handle; this plugin writes the manifest entry).Verification
Each behaviour is pinned by a mutation of implemented logic, not a missing symbol.
returns the entry's own credential id verbatim(Expected "resolved", received "unresolved") and the duplicate-label test (Set {} vs Set { "alice" }).preflight hands a small serving margin to cache.get even when expiry is far below the refresh floor— the credential expires in 1h, comfortably above the 5-minute margin and far below the old floor. The test asserts the argument handed tocache.get, not just the outcome, so it cannot pass for the wrong reason.ck auth mint-handleassertion.Not included
Whether main should eventually be re-homed onto a 3-segment id in the vault. That is a real option with a migration — Insula's handle binding, the sealer, both recorders, the latch-watch and the rotation probe all key on the literal
oauth:anthropic— and it should not happen as a side effect of unblocking a flip.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Custody manifest bindings now carry the vault's
credential_idverbatim instead of deriving it from account labels, so provider-default credentials such asoauth:anthropiccan support takeover while the runtime vault identity check remains authoritative. Preflight now uses a five-minute serving margin instead ofrefresh_floor + 30minand refuses when the vault omits an ID; the Fable restoration notice is now delivered after the final recovery turn instead of being revoked when OpenCode republishes it as a user message.Bug Fixes
TAKEOVER_INCOMPLETE_MAIN_REALnow directs users tock auth mint-handle.Test Isolation
packages/pitests use per-test temporary OpenCode and Pi paths; manifest-lock contention tests use injected clocks and explicit barriers instead of wall-clock sleeps.Written for commit c12bc60. Summary will update on new commits.