Skip to content

feat(daemon): add the allocator-held device claim kind - #2263

Merged
thymikee merged 7 commits into
adr0021/u1-owner-kindfrom
adr0021/u2-allocator-claim
Sep 3, 2026
Merged

feat(daemon): add the allocator-held device claim kind#2263
thymikee merged 7 commits into
adr0021/u1-owner-kindfrom
adr0021/u2-allocator-claim

Conversation

@thymikee

@thymikee thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

ADR 0021 allocator-held execution claims, stacked on #2258's managed-local runtime foundation. A managed-pool device is held for the identity's pool lifetime rather than for one daemon process or session.

  • The claim store gains a separate schema-v3 AllocatorHeldDeviceClaim; the existing schema-v2 process claim is unchanged. The allocator record has an installation/allocator/incarnation principal and cannot carry process, session, workspace, or abandonment fields.
  • Record decoding and atomic publication are centralized in device-claim-record.ts and device-claim-store.ts. Both claim kinds reuse the existing host-kit publishFileSync and per-device process lock; there is no second persistence mechanism.
  • Managed admission is covered only when the exact owner, installation, and identity incarnation match the canonical managed fence. Missing, foreign, or stale-incarnation claims fail closed.
  • Ordinary admission performs the smallest read-only allocator classification for every device-touching policy. It deliberately avoids process-liveness observation on the hot binding path.
  • Malformed allocator-looking v3 records classify as allocator-inconsistent and fail closed instead of being treated as an absent claim. The ordinary observe regression was planted red by reverting that classification gate.
  • Existing process-owned clearing paths cannot clear allocator claims by type. The only clearing API requires the allocator's exact removal proof; status renders the allocator principal and device release --stale refuses without changing the file.

Nothing writes allocator claims in production yet. The acquire/release functions are the focused store interface for the later activation/removal units; they do not add lifecycle policy, command flags, or a second allocator abstraction.

Scope: 24 files, +1,695/-343 against #2258. Most growth is the split record/store implementation and one-to-one claim-family tests. The npm unpacked increase is 4.0 kB; startup moved by +0.1 ms for --version and +2.2 ms for --help, within the seven-run measurement noise. The design removes the old decoder/type cycle and reuses the shared atomic publisher instead of adding local file-publication code.

Validation

Exact head: db60db6761d4936f0f7f0a226fc6d18776501643.

  • pnpm check:affected --run: green at the feature/docs series across format, lint, typecheck, layering, fallow, build, and related tests; the final malformed-record fix is covered by exact-head GitHub CI.
  • GitHub: every required lane is green, including iOS, Android, macOS, Linux, Coverage, Integration, Repo Guards, CodeQL, Agent Guidance, and Size. The iOS workflow passed simulator E2E, gesture replay, and host-focus preservation; its physical-device step was the expected skip.
  • Layering: 198 checks, with no cycle or ownership-baseline relaxation. Eager closure: 407/407.
  • Planted-red coverage includes allocator/process record separation, full-principal matching, exact-incarnation admission, ordinary-owner refusal, malformed-v3 fail-closed behavior, stale-release refusal, inventory/status projection, shared atomic-publish ownership, and the total admission table.
  • No device evidence is claimed: no production caller creates an allocator claim or managed binding in this unit.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Size Report

Metric Base Current Diff
JS raw 2.59 MB 2.60 MB +4.0 kB
JS gzip 873.3 kB 874.3 kB +1.0 kB
npm bundled raw 2.59 MB 2.60 MB +4.0 kB
npm bundled gzip 873.3 kB 874.3 kB +1.0 kB
npm tarball 1.03 MB 1.03 MB +999 B
npm unpacked 3.48 MB 3.48 MB +4.0 kB
npm clean-installed 3.48 MB 3.48 MB +4.0 kB

npm unpacked components

Component Base Current Diff
JS / dist source 2.75 MB 2.76 MB +4.0 kB
Apple runner source/project 544.4 kB 544.4 kB 0 B
Apple snapshot presentation source 33.8 kB 33.8 kB 0 B
macOS helper source 54.8 kB 54.8 kB 0 B
Android helper artifacts 43.3 kB 43.3 kB 0 B
Other package files 46.4 kB 46.4 kB 0 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 31.8 ms 31.9 ms +0.1 ms
CLI --help 80.2 ms 82.4 ms +2.2 ms

Top changed chunks:

Chunk Raw diff Gzip diff
dist/src/device-claim-rule.js +1.5 kB +375 B
dist/src/internal/daemon.js +114 B +39 B
dist/src/session2.js +36 B -2 B

Top changed packed files

Packed file Base Current Diff
dist/src/device-claim-rule.js 56.3 kB 57.8 kB +1.5 kB
dist/src/device-claim-inspection.js 4.2 kB 5.5 kB +1.3 kB
dist/src/device2.js 4.1 kB 4.7 kB +584 B
dist/src/perf-runtime-plan.js 11.9 kB 12.3 kB +436 B
dist/src/internal/daemon.js 107.7 kB 107.8 kB +114 B
dist/src/session2.js 218.0 kB 218.0 kB +36 B

@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

BLOCKED at 690041cc9d1f4a5aaedec8ac3040d9952777982b.

It is CONFLICTING/DIRTY against stacked base adr0021/u1-owner-kind (c48403b…).

Coverage is also an owner-action failure: new src/daemon/device-claim-store.ts publishes a same-directory file without the shared publishFileSync atomic-publish owner required by atomic-publish-ownership.test.ts. Rebase and route publication through the shared atomic publisher, then rerun exact-head gates.

Current completed checks are green; iOS is in progress. Size is +3.7 kB unpacked and needs growth justification after the functional gate is fixed. Do not label ready.

@thymikee
thymikee force-pushed the adr0021/u2-allocator-claim branch from 690041c to 0cbc751 Compare September 3, 2026 14:27
@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Rebased onto #2258's blocking fix and fixed at 0cbc751.

Rebase. device-claims.ts no longer writes its claim file directly — it delegates to writeDeviceClaim in the new device-claim-store.ts, so atomic-publish-ownership.test.ts's SIMPLE_PUBLISHERS source-grep for publishFileSync no longer matched anything in device-claims.ts and Coverage failed. Registered device-claim-store.ts (the single writer, per its own docstring) as the owner in that list instead, with device-claims.ts dropped from it. Planted-red verified: reverting the list entry reproduces the exact atomic-publish-ownership.test.ts:20 failure from the Coverage run.

Same-switch conflict from #2258's rebase. decideAllocatorHeldAdmission picked up this PR's covered/incarnation-stale arms on top of #2258's totalized {admitted}|{admitted:false,error} shape. Also caught and fixed: ADMITTED_BY_OUTCOME_STATUS in device-claim-conflict.test.ts — the exhaustiveness table decideAllocatorHeldAdmission is built to force a compile error against — still only listed the three original statuses and TS2739'd once rebased against the widened union. Extended it (covered: true, incarnation-stale: false) and the outcomes array alongside it; planted-red verified by reverting the two new entries and re-running tsc.

Also carried forward the earlier should-fix rename (DeviceClaimRecordStoredDeviceClaim, decodeDeviceClaimRecorddecodeStoredDeviceClaim) that was sitting uncommitted in the worktree.

pnpm check:affected --run green at 0cbc751: 534 files / 3908 tests, format/lint/typecheck/layering/fallow/agent-guidance all clean. CONTEXT.md at 11,808 bytes (12,000 cap).

@thymikee
thymikee force-pushed the adr0021/u2-allocator-claim branch from 0cbc751 to ff74cdd Compare September 3, 2026 15:41
@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

BLOCKED. readAllocatorHeldClaimFile returns null whenever a record does not decode to a valid allocator claim. An allocator-looking v3 record made inconsistent by corruption (for example a forbidden process-principal field) is therefore treated as absent by ordinary-owner admission, allowing observe and other device-touching policies to proceed. That violates ADR-0021’s fail-closed corrupted-state requirement. Refuse admission when the existing claim cannot be proven safe/non-allocator, and add a planted-red ordinary-owner observe regression with a malformed v3 allocator record. The branch also currently conflicts with its advanced base and must be rebased; re-check the >3 kB size result afterward.

@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Re-reviewed at ff74cddf1ad6adba1b5a2abd4bea1e413f34305a: still BLOCKED. This update resolves the stack conflict but is effectively a rebase and does not address the malformed allocator-record finding. readAllocatorHeldClaimFile still maps an inconsistent allocator-looking record to null, ordinary-owner admission still treats that as safe, and no malformed-v3 observe regression/planted-red evidence was added. Fail closed at the ordinary admission seam, add that regression, then complete exact-head CI (Repo Guards, Coverage, and Smoke lanes are currently running).

ADR 0021 foundations, unit 2. A device an allocator-managed pool owns is held for that identity's
whole pool lifetime, not for a session, so the claim store gains a second record kind whose
principal is an installation instead of a process. Nothing writes one in production yet.

- `AllocatorHeldDeviceClaim` is a SEPARATE record at `schemaVersion: 3` with `kind: 'allocator'`:
  `stateDir` + `allocator.instanceId` + `allocator.identityIncarnationId`, and no
  ownerPid/ownerStartTime/ownerToken/session/workspace/abandonedAtMs — a record carrying any of
  them does not decode. `DeviceClaim` and its v2 files are untouched, so an older daemon never
  meets a changed process-owned record and only ever sees a v3 file for a managed identity, which
  it reads as an unreadable claim record and fails closed on.
- The managed owner is DERIVED from the recorded allocator instance (`allocatorHeldClaimOwner`),
  never stored, so an owner that disagrees with the claim's principal cannot exist.
- `InspectedDeviceClaim` becomes a union: the allocator member carries `allocatorClaim` and
  `claim: undefined`. Every clearing surface — ownership match, abandon, stale release, the
  startup sweep, acquire-path reconciliation, session close, lease expiry, the shutdown ledger,
  and `processOwnsActiveDeviceClaim` — reaches for `claim`, so none of them can be written against
  an allocator-held claim, and `DeviceClaimReconciler` stays typed to the process-owned record.
- New classification `allocator-held`: not stale, not owner-releasable. `device status` shows it
  in the normal view with its allocator, incarnation and installation; `device release --stale`
  refuses it with `allocator-held-owner`.
- The verifier gains `covered` and `incarnation-stale`. A session or transient command whose fence
  names the incarnation the claim holds executes under it, acquiring and clearing nothing; a fence
  for a re-provisioned identity is refused, because an incarnation is stable for that identity's
  pool lifetime.
- The ORDINARY arm of the admission gate now performs a read-only inspection under every policy
  but `none` and refuses `DEVICE_IN_USE` / `DEVICE_CLAIM_ALLOCATOR_HELD`: `apps` and `app-state`
  boot a device through `ensureReady` exactly as a mutation would. It still acquires nothing, so
  the policy/claim-file table is unchanged. That reason is deliberately outside
  `DeviceClaimConflictReason`, so replay never retries it as infrastructure. The inspection reads
  the record and stops (`readAllocatorHeldClaimFile`): the owner-liveness probe `inspectDeviceClaimFile`
  runs costs a host process observation per binding, and this kind has no owner process to observe.
- `acquireAllocatorHeldDeviceClaim` reattaches only on the full principal and never reconciles or
  supersedes an ordinary claim (a conflicting ordinary claim prevents publication, ADR 0021 §4);
  `releaseAllocatorHeldClaim` takes removal proof and is the only clearing path. Both are
  by-design production-unused in this unit, with a comment naming the unit that will call them.
- The record/decoder and the lock/write leaves move out of device-claims.ts into
  device-claim-record.ts and device-claim-store.ts, which dissolves the
  device-claims <-> device-claim-inspection type cycle and lets the allocator module share the
  writer without device-claims.ts ever importing it.

`devices` still reports no `claimedBy` for an allocator-held claim: the public field is
`{ session, workspace }` and this kind has neither.
Wording-only tightening of 41 existing definitions. No term is added, removed or redefined, and the
five entries unit 1 added and the three unit 2 touched are left alone. The glossary ends at 11,589
of the 12,000-byte guidance budget, which leaves room for the sibling unit's vocabulary to land
without a second trim.
…ublish owner

device-claims.ts no longer writes its claim file directly: it delegates every write to
writeDeviceClaim in the new device-claim-store.ts, the single writer shared by the process-owned
and allocator-held claim kinds. atomic-publish-ownership.test.ts's SIMPLE_PUBLISHERS list still
named device-claims.ts, so its source-grep for `publishFileSync` no longer matched anything and
the ownership gate failed in CI. Register device-claim-store.ts as the owner instead.

Also rename DeviceClaimRecord -> StoredDeviceClaim (and decodeDeviceClaimRecord ->
decodeStoredDeviceClaim): the shutdown ledger's own DeviceClaimRecord in daemon-shutdown-report.ts
is a different type (one row of what teardown released), and the shared name invited confusing
that unrelated type for this module's claim-file record.
The rebase onto adr0021/u1-owner-kind's decideAllocatorHeldAdmission fix carried forward
ADMITTED_BY_OUTCOME_STATUS and its outcomes array from before this branch added the covered and
incarnation-stale statuses, so the table no longer covered the whole AllocatorHeldClaimAdmission
union and TS2739 caught it. Add both: covered admits, incarnation-stale does not.
readAllocatorHeldClaimFile gated on entry.allocatorClaim, which is only set once a record
decodes all the way through. A v3-schema record corrupted into also carrying a process
principal field fails decodeAllocatorHeldClaim's carriesProcessPrincipal check and
decodeStoredDeviceClaim returns null -- exactly like a record that never existed. Ordinary
admission's inspectAllocatorHeldDeviceClaim then read that as "no allocator claim" and let
observe and every other non-transient-exclusive policy proceed, against a device an
allocator may actually hold.

A record that declares schemaVersion 3 but fails to decode is not provably a non-allocator
record, so it cannot be treated as absent. Add looksLikeAllocatorHeldClaim (device-claim-
record.ts) to distinguish "declares the allocator schema, doesn't decode" from every other
kind of corruption, give it its own classification (allocator-inconsistent), and have
readAllocatorHeldClaimFile return the entry -- not null -- for it. Ordinary admission then
refuses through the existing deviceClaimConflictError path (DEVICE_IN_USE /
DEVICE_CLAIM_OWNER_UNCERTAIN), same as any other claim it cannot verify.

deviceClaimRequiresStaleInspection, deviceClaimOwnerCannotRelease and conflictReason are
exhaustive switches over DeviceClaimClassification, so the new member forced a decision at
every site rather than one that could be missed: not stale (nothing dead to surface), not
owner-releasable (no process proof exists to make), and DEVICE_CLAIM_OWNER_UNCERTAIN like
the other undecodable-record classifications, not a permanent-condition reason.

Planted-red verified: reverting readAllocatorHeldClaimFile's gate to entry?.allocatorClaim
alone makes the new regression fail with error.code 'UNKNOWN' -- admission resolves with no
error at all, exactly the silent pass-through this fixes.
The switch exceeded fallow's health gate at 10 cyclomatic / 31.6 CRAP once the
allocator-inconsistent case joined it. A Record<DeviceClaimClassification, string> reads as
one branch to the complexity walker instead of one per case, while TypeScript still refuses
to compile a missing key -- the same exhaustiveness guarantee a switch gave, at a fraction
of the counted complexity.
Main gained bytes elsewhere since this stack's own docs-condense commit landed, pushing
CONTEXT.md to 12,028. Tighten four of this stack's own entries (managed local owner, request
generation, identity incarnation, managed device allocator port) rather than touch anyone
else's; 11,947 bytes leaves headroom against the next PR that lands first.
@thymikee
thymikee force-pushed the adr0021/u2-allocator-claim branch from ff74cdd to db60db6 Compare September 3, 2026 16:48
@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

The malformed allocator-record code blocker is fixed at db60db6761d4936f0f7f0a226fc6d18776501643: inconsistent v3 allocator records now reach fail-closed ordinary admission, and the real observe regression has valid planted-red behavior. Overall still BLOCKED by #2258’s scope/stack mismatch, stale PR-body file/validation accounting, and pending Repo Guards, Coverage, Android/iOS/macOS Smoke. Update the body/evidence after the base is resolved and wait for exact-head CI.

@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

The remaining review blockers are resolved at exact head db60db6761d4936f0f7f0a226fc6d18776501643.

The body now matches the actual 24-file/+1,695/-343 diff and the combined #2258 base. It records the malformed allocator-looking v3 fail-closed behavior and planted-red observe regression, shared publishFileSync ownership, the read-only hot-path classifier, the +4.0 kB size evidence, and the absence of production/device reachability.

Code review is clean. Every completed exact-head lane is green; iOS Smoke is the only remaining in-progress gate, so readiness waits on that result.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Sep 3, 2026
@thymikee

thymikee commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

READY at exact head db60db6761d4936f0f7f0a226fc6d18776501643.

The final iOS Smoke lane passed, including simulator E2E, gesture replay, and host-focus preservation; the physical-device step was the expected workflow skip. Every exact-head required check is now green, the stack base is current, the PR contract matches the 24-file diff, and the malformed-v3 fail-closed review finding remains pinned by its planted-red regression.

@thymikee
thymikee merged commit 309a536 into main Sep 3, 2026
19 checks passed
@thymikee
thymikee deleted the adr0021/u2-allocator-claim branch September 3, 2026 17:41
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-09-03 17:42 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant