Skip to content

fix(custody): carry the vault credential id in manifest bindings - #218

Closed
iceteaSA wants to merge 3 commits into
cortexkit:mainfrom
iceteaSA:fix/custody-binding-credential-id
Closed

iceteaSA wants to merge 3 commits into
cortexkit:mainfrom
iceteaSA:fix/custody-binding-credential-id

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Stacked on #219 — that PR fixes a pre-existing test-isolation defect that makes CI red on main itself, so this one cannot go green without it. Review/merge #219 first; the diff here is the last commit only (b90645b).

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_id outcome
oauth:anthropic (the real one) corrupt-binding at parse
oauth:anthropic:main (the derived one) credential_identity_mismatch at runtime — the vault returns the real id

Verified against the parser before writing anything, and again after the fix:

corrupt: []
  main     -> {"status":"resolved","credentialId":"oauth:anthropic"}
  work-alt -> {"status":"resolved","credentialId":"oauth:anthropic:work-alt"}

Not specific to main. antigravity:google is 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:

credential.credentialId !== binding.credentialId -> refuse

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_found from the vault (uniform mask, fails closed), a mismatched id gets the fence.

Special-casing main would leave the derivation in place and add an exception to it — working today, broken again at the next unlabelled credential.

Changes

  • readCustodyHandles carries credential_id verbatim; all other validation (handle shape, label validity, superseded parsing, corrupt-label path) is untouched.
  • resolveCustodyHandle matches on label and returns the entry's own id.
  • Duplicate-label guard, new. The label is now the sole lookup key, so two entries sharing one had to stop silently resolving to whichever came first — every entry with a duplicated label goes to corruptLabels. This replaces what the derivation was incidentally doing.
  • Preflight freshness gate. It passed refresh_floor + 30min (~4.5h) to cache.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.
  • Refusal text for TAKEOVER_INCOMPLETE_MAIN_REAL pointed at ck 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.

  • Restoring the derivation reddens both new manifest tests: returns the entry's own credential id verbatim (Expected "resolved", received "unresolved") and the duplicate-label test (Set {} vs Set { "alice" }).
  • Restoring the 4.5h floor reddens 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 to cache.get, not just the outcome, so it cannot pass for the wrong reason.
  • Restoring the old guidance reddens the ck auth mint-handle assertion.
  • core 188/188 · opencode 1872/1872 · pi 114/114 · typecheck, format, biome clean.

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.


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

Custody manifest bindings now carry the vault's credential_id verbatim instead of deriving it from account labels, so provider-default credentials such as oauth:anthropic can support takeover while the runtime vault identity check remains authoritative. Preflight now uses a five-minute serving margin instead of refresh_floor + 30min and 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

  • The parser scopes credential IDs to their provider, rejects duplicate labels as corrupt, and returns IDs verbatim.
  • Legacy handle paths derive IDs only when no usable manifest binding exists.
  • Local-exit and OAuth callback flows use resolved IDs, and corrupt account stores no longer discard exchanged credentials.
  • TAKEOVER_INCOMPLETE_MAIN_REAL now directs users to ck auth mint-handle.

Test Isolation

  • packages/pi tests use per-test temporary OpenCode and Pi paths; manifest-lock contention tests use injected clocks and explicit barriers instead of wall-clock sleeps.
  • Regression coverage includes provider scoping, duplicate labels, identity fencing, preflight freshness, restoration-notice delivery, end-to-end bindings, and corrupt-store callbacks.

Written for commit c12bc60. Summary will update on new commits.

Review in cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/core/src/claustrum.ts Outdated
@iceteaSA
iceteaSA force-pushed the fix/custody-binding-credential-id branch from 5659207 to 5929741 Compare September 16, 2026 11:20

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 10 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/index.ts Outdated
Comment thread packages/opencode/src/tests/custody-mode.test.ts
@iceteaSA
iceteaSA force-pushed the fix/custody-binding-credential-id branch from 5929741 to b90645b Compare September 16, 2026 11:42

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread packages/pi/src/tests/setup.ts Outdated
@iceteaSA

Copy link
Copy Markdown
Contributor Author

CI is green at e2af7f6.

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.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

Addressed the review findings and rebased onto the updated #219.

  • loadAccounts on the custody re-entry path was uncaught: a corrupt or partially-written account file would reject the auth callback and discard a just-exchanged credential — the failure lands exactly when someone is re-logging in to recover. Now falls through to the missing-entry branch the ternary already handles. The sibling site in acknowledgeMainLocalLogin had the same shape and got the same fix; the remaining loadAccounts calls in that file are already inside try/catch.
  • Red proof is a new test that corrupts the store between authorize() and callback() and asserts the exchange still returns its credentials — it fails with the raw account store ... is corrupt or unreadable rejection when the catch is removed.
  • The P3 on the binding-construction comment was already addressed in b90645b.

core 192/0 · opencode 1875/0 · pi 114/0 · typecheck, format, biome clean.

@iceteaSA
iceteaSA force-pushed the fix/custody-binding-credential-id branch 3 times, most recently from 602fca7 to 42c8e74 Compare September 16, 2026 13:46
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Bisecting on CI — temporarily dropped the .catch commit from this branch.

Four login/custody tests fail identically on two pushes of a byte-identical tree, while passing locally (1875/0, including the exact root bun run test CI runs, under an empty HOME, and on 2 pinned cores). Deterministic on both sides in opposite directions, so it is environmental rather than a race — and CI is the only place it reproduces, so the bisect has to happen here.

Restoring the commit after this run regardless of the result. Not ready to merge until this is explained.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

Restored the .catch commit — it was never the cause. The bisect convicted the commit; the culprit was the test that shipped in it.

That test replaced the core module process-wide (mock.module('@cortexkit/anthropic-auth-core', …)) and relied on mock.restore() in afterAll to undo it. It does not: the registry replacement is permanent for the run, so every file executing afterwards imported the stub. The tell is in the CI diff — a re-login assertion in index.test.ts received access: "exchanged-access", a string that exists only in that test's fixture. The stub answered another suite's OAuth exchange.

This is documented in-tree at accounts.test.ts:6396 ("Bun mock.restore() does not undo mock.module"), where the workaround is a manual re-mock with the real module. The new test now stubs TOKEN_URL through globalThis.fetch instead — the pattern its neighbours already use — so there is no module registry replacement to leak.

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 -t filtered runs, and to a local full-suite pass. Only cross-file ordering in one process exposes it.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread packages/opencode/src/tests/oauth-callback-corrupt-store.test.ts
@iceteaSA
iceteaSA force-pushed the fix/custody-binding-credential-id branch 2 times, most recently from 6382971 to 59c1988 Compare September 16, 2026 14:26
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Squashed to one commit on top of #219 (59c1988); tree verified byte-identical at each step.

CI on this tip is red on exactly one test — bridges back to a stale Opus cache after more than 20 Fable blocks, the pre-existing e2e failure from #220. The four custody/login tests that were failing are gone, which confirms the mock.module diagnosis.

I dug into the remaining one and updated #220: it is not a timeout to raise. At a 120s wait the Returning to Fable 5 notice still never arrives, so the test is observing a real absence. CI pins opencode-ai@1.18.18 while this host runs 1.18.29, and the notice is absent here 4/4 and absent on CI in 3 of 4 runs. That points at the no-TUI desktop-notice delivery path rather than at this branch.

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.

@iceteaSA

Copy link
Copy Markdown
Contributor Author

Added provider scoping for manifest credential ids, squashed into the single commit (bd0e262).

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 oauth:<provider>, which was wrong: the vault holds four kind prefixes today — oauth, chatgpt, antigravity, apikey — and three entries in the manifest on my own machine are apikey:* with no oauth: at all. Hardcoding oauth: would have rejected real credentials, including the sibling plugin's own (chatgpt:openai). That was the same defect this commit removes at the label layer, rebuilt one layer over; the vault owner caught it by listing the ids rather than reasoning about their shape.

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:

anthropic  oauth:anthropic:something-else   RESOLVE   (guards against rebuilding label derivation)
openai     chatgpt:openai                   RESOLVE   (guards against hardcoding a kind)
google     antigravity:google               RESOLVE   (a live credential)
anthropic  chatgpt:openai                   REJECT    (the real cross-tenant smuggle)
anthropic  oauth:anthropic                  RESOLVE   (main)
anthropic  apikey:anthropic-lookalike:x     REJECT

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 oauth:anthropic — the fixtures use labelled ids, which are valid but not main's shape, and fixtures teach shape to whoever reads them next.

core 199/0 · opencode 1875/0 · pi 114/0 · typecheck, format, biome clean.

@iceteaSA
iceteaSA force-pushed the fix/custody-binding-credential-id branch from 59c1988 to bd0e262 Compare September 16, 2026 18:35
…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.
@iceteaSA

Copy link
Copy Markdown
Contributor Author

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.

#219  fix(pi): isolate the test suite from the host config   <- base
 ├── #228  test(custody): lock tests independent of wall clock
 ├── #229  fix(recovery): deliver the restoration notice
 └── #218  fix(custody): carry the vault credential id        <- stacked on #228

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 a309e50.

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.ts no 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.
@iceteaSA
iceteaSA force-pushed the fix/custody-binding-credential-id branch from a309e50 to c12bc60 Compare September 16, 2026 20:09
@iceteaSA

Copy link
Copy Markdown
Contributor Author

Re-stacked into a single linear chain so every PR can go green independently:

#219  fix(pi): isolate the test suite from the host config
  └── #228  test(custody): lock tests independent of wall clock
        └── #229  fix(recovery): deliver the restoration notice
              └── #218  fix(custody): carry the vault credential id

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Confidence score: 4/5

  • packages/opencode/src/index.ts retains revision state for every session in a long-lived process, allowing unbounded memory growth over time; apply the existing 128-session eviction policy to desktopNoticeUserRevisions.
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(

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: 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>

ualtinok added a commit that referenced this pull request Sep 16, 2026
# Conflicts:
#	packages/core/src/claustrum.ts
#	packages/core/src/tests/claustrum.test.ts
@ualtinok

Copy link
Copy Markdown
Contributor

The custody credential-ID patch is integrated on main with contributor ancestry preserved (cf4814cf). The latest force-pushed head carries the same patch but is re-stacked over #229, so GitHub could not mark it merged automatically. I also added a provider-scope guard for manifest writes in f612af98. Core (199), OpenCode (1,876), and the full 36-test custody/E2E suite pass under Bun 1.3.14.

@ualtinok ualtinok closed this Sep 16, 2026
@iceteaSA
iceteaSA deleted the fix/custody-binding-credential-id branch September 16, 2026 22:25
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.

2 participants