@polderlabs/bizar-omp 0.7.9 → 0.7.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +35 -4
  2. package/dist/cli/agents.d.ts.map +1 -1
  3. package/dist/cli/agents.js +46 -23
  4. package/dist/cli/agents.js.map +1 -1
  5. package/dist/cli/doctor.d.ts +4 -4
  6. package/dist/cli/install.d.ts +126 -9
  7. package/dist/cli/install.d.ts.map +1 -1
  8. package/dist/cli/install.js +19 -4
  9. package/dist/cli/install.js.map +1 -1
  10. package/dist/cli/mobile-claim.d.ts +143 -0
  11. package/dist/cli/mobile-claim.d.ts.map +1 -0
  12. package/dist/cli/mobile-claim.js +399 -0
  13. package/dist/cli/mobile-claim.js.map +1 -0
  14. package/dist/cli/omb-commands.d.ts +1 -1
  15. package/dist/cli/omb-commands.d.ts.map +1 -1
  16. package/dist/cli/omb-commands.js +22 -1
  17. package/dist/cli/omb-commands.js.map +1 -1
  18. package/dist/cli/omb-mobile.d.ts +47 -0
  19. package/dist/cli/omb-mobile.d.ts.map +1 -0
  20. package/dist/cli/omb-mobile.js +307 -0
  21. package/dist/cli/omb-mobile.js.map +1 -0
  22. package/dist/cli/omb-sessions.d.ts +64 -0
  23. package/dist/cli/omb-sessions.d.ts.map +1 -0
  24. package/dist/cli/omb-sessions.js +231 -0
  25. package/dist/cli/omb-sessions.js.map +1 -0
  26. package/dist/cli/omb.js +20 -15
  27. package/dist/cli/omb.js.map +1 -1
  28. package/dist/cli/omp-mobile-dispatch.d.ts +10 -0
  29. package/dist/cli/omp-mobile-dispatch.d.ts.map +1 -0
  30. package/dist/cli/omp-mobile-dispatch.js +148 -0
  31. package/dist/cli/omp-mobile-dispatch.js.map +1 -0
  32. package/dist/omp/compatibility-receipt.d.ts +69 -0
  33. package/dist/omp/compatibility-receipt.d.ts.map +1 -0
  34. package/dist/omp/compatibility-receipt.js +60 -0
  35. package/dist/omp/compatibility-receipt.js.map +1 -0
  36. package/dist/omp/compatibility.d.ts +5 -5
  37. package/dist/omp/compatibility.d.ts.map +1 -1
  38. package/dist/omp/compatibility.js +2 -2
  39. package/dist/omp/compatibility.js.map +1 -1
  40. package/dist/omp/daemon-service.d.ts +4 -0
  41. package/dist/omp/daemon-service.d.ts.map +1 -1
  42. package/dist/omp/daemon-service.js +418 -8
  43. package/dist/omp/daemon-service.js.map +1 -1
  44. package/dist/omp/omb-sessions.d.ts +10 -0
  45. package/dist/omp/omb-sessions.d.ts.map +1 -1
  46. package/dist/omp/omb-sessions.js +8 -6
  47. package/dist/omp/omb-sessions.js.map +1 -1
  48. package/dist/omp/tmux.d.ts +6 -0
  49. package/dist/omp/tmux.d.ts.map +1 -1
  50. package/dist/omp/tmux.js +11 -0
  51. package/dist/omp/tmux.js.map +1 -1
  52. package/dist/scripts/emit-compatibility-receipt.d.ts +2 -0
  53. package/dist/scripts/emit-compatibility-receipt.d.ts.map +1 -0
  54. package/dist/scripts/emit-compatibility-receipt.js +33 -0
  55. package/dist/scripts/emit-compatibility-receipt.js.map +1 -0
  56. package/docs/07-bizarharness-integration.md +44 -0
  57. package/docs/compatibility/baseline.json +9 -8
  58. package/docs/compatibility/supported-surfaces.json +2 -1
  59. package/docs/releases/0.7.10.md +27 -0
  60. package/docs/releases/support-matrix.md +8 -1
  61. package/package.json +6 -4
  62. package/skills/omp-native-development/SKILL.md +1 -1
  63. package/skills/omp-native-development/references/accuracy-and-versioning.md +1 -1
  64. package/skills/omp-native-development/references/source-manifest.json +3 -3
@@ -0,0 +1,143 @@
1
+ import { type Lease } from "../storage/lease.js";
2
+ import { type OmbSessionInfo } from "../omp/omb-sessions.js";
3
+ export interface MobileClaim {
4
+ readonly claimId: string;
5
+ readonly session: string;
6
+ /** The exact generation this claim is bound to. */
7
+ readonly generation: string;
8
+ readonly owner: string;
9
+ /**
10
+ * The holder's pid, normally the mobile host — never the one-shot CLI that
11
+ * created the claim. Absent means the claim expires only on its TTL; see
12
+ * `isClaimStale`.
13
+ */
14
+ readonly pid?: number;
15
+ readonly claimedAt: number;
16
+ }
17
+ export interface ClaimOutcome {
18
+ readonly claim: MobileClaim;
19
+ readonly sessionDir: string;
20
+ readonly nativeId?: string;
21
+ readonly nativeSessionName?: string;
22
+ readonly cwd: string;
23
+ readonly takeover: boolean;
24
+ }
25
+ export declare function mobileClaimsDir(env?: NodeJS.ProcessEnv): string;
26
+ /**
27
+ * The per-session acquisition lock.
28
+ *
29
+ * Named after the session rather than shared, so claims for different sessions
30
+ * proceed independently — the contention is per ownership, not global.
31
+ */
32
+ export declare function claimLockPath(session: string, env?: NodeJS.ProcessEnv): string;
33
+ /** Serializes claim acquisition/release with OMB rename, continue and delete. */
34
+ export declare function acquireMobileSessionLease(session: string, env?: NodeJS.ProcessEnv): Lease;
35
+ export declare function claimPath(claimId: string, env?: NodeJS.ProcessEnv): string;
36
+ export declare function writeClaimAtomic(path: string, claim: MobileClaim): void;
37
+ /**
38
+ * Why a claim record could not be read.
39
+ *
40
+ * A claim is ownership metadata, so "I could not read it" and "there is none"
41
+ * are very different facts and must not share a return value. The previous
42
+ * implementation returned `undefined` for both, so a torn or truncated record
43
+ * silently disappeared from the inventory and the next caller saw no owner and
44
+ * was admitted — the fail-open this enum now makes impossible to express.
45
+ */
46
+ export type ClaimReadFailure = "malformed" | "unreadable";
47
+ export interface ClaimReadResult {
48
+ readonly claim: MobileClaim | undefined;
49
+ readonly failure: ClaimReadFailure | undefined;
50
+ }
51
+ export declare function readClaimResult(claimId: string, env?: NodeJS.ProcessEnv): ClaimReadResult;
52
+ /** Convenience for callers that only care whether a usable claim exists. */
53
+ export declare function readClaim(claimId: string, env?: NodeJS.ProcessEnv): MobileClaim | undefined;
54
+ export interface ClaimInventory {
55
+ readonly claims: MobileClaim[];
56
+ /** Claim ids that exist but could not be read, with the reason. */
57
+ readonly unreadable: readonly {
58
+ readonly claimId: string;
59
+ readonly reason: ClaimReadFailure;
60
+ }[];
61
+ }
62
+ export declare function listClaimsDetailed(env?: NodeJS.ProcessEnv): ClaimInventory;
63
+ export declare function listClaims(env?: NodeJS.ProcessEnv): MobileClaim[];
64
+ /**
65
+ * A claim is stale only when its holder is provably gone, or it is simply old.
66
+ *
67
+ * `pid` is the *holder's* pid — normally the mobile host, supplied by the
68
+ * caller — and is deliberately not this process. `omb mobile sessions claim` is
69
+ * a one-shot command that exits immediately, so recording its own pid would
70
+ * make every claim look abandoned the moment it was created and break mutual
71
+ * exclusion entirely. A claim with no holder pid therefore expires only on its
72
+ * TTL, which is the safe direction: an abandoned claim is a nuisance, a claim
73
+ * that evaporates while its owner is still running is a second writer.
74
+ */
75
+ export declare function isClaimStale(claim: MobileClaim, now?: number): boolean;
76
+ export declare function releaseClaim(claimId: string, env?: NodeJS.ProcessEnv): void;
77
+ /**
78
+ * Claims one session for mobile ownership.
79
+ *
80
+ * The generation is re-resolved from a live scan rather than trusted from the
81
+ * caller, then re-checked a second time immediately before the claim is written.
82
+ * The gap between those two reads is the window a concurrent TUI action could
83
+ * exploit, and the second read is what closes it.
84
+ */
85
+ export declare function claimSessionForMobile(input: {
86
+ cwd: string;
87
+ name: string;
88
+ generation: string;
89
+ owner: string;
90
+ /** The mobile host's pid, when it knows one. Never this process. */
91
+ holderPid?: number;
92
+ env?: NodeJS.ProcessEnv;
93
+ now?: number;
94
+ }): Promise<ClaimOutcome>;
95
+ /**
96
+ * Releases a claim, but only while the session still answers to the generation
97
+ * that was claimed. A session that changed under the claim is reported rather
98
+ * than silently released, so the caller can decide.
99
+ */
100
+ export declare function releaseMobileClaim(input: {
101
+ claimId: string;
102
+ cwd: string;
103
+ env?: NodeJS.ProcessEnv;
104
+ }): Promise<{
105
+ released: true;
106
+ claim: MobileClaim;
107
+ generationHeld: boolean;
108
+ }>;
109
+ export declare function describeClaim(claim: MobileClaim, now?: number): Record<string, unknown>;
110
+ export type { OmbSessionInfo };
111
+ /** A detached tmux client does not prove that its native OMP writer stopped. */
112
+ export declare function assertMobileClaimCandidate(session: Pick<OmbSessionInfo, "state" | "attached" | "authorityKnown" | "processObserved" | "uncertainLiveness" | "nativeId" | "nativeSessionAvailable">): void;
113
+ /** Resolve remote adoption only when the exact name and generation are unique. */
114
+ export declare function findMobileClaimSession<T extends Pick<OmbSessionInfo, "name" | "generation">>(sessions: readonly T[], name: string, generation: string): T;
115
+ /**
116
+ * Centralized admission check for taking local ownership of a session.
117
+ *
118
+ * A claim records that a mobile host has been handed a session, but until this
119
+ * existed nothing on the local side consulted it: a local command could still
120
+ * attach to the same journal and become a second writer, which is the one thing
121
+ * the claim contract exists to prevent. It lives here rather than in a CLI
122
+ * command so every local ownership path inherits it.
123
+ *
124
+ * `generation` is optional and the difference matters:
125
+ *
126
+ * - given, only a claim for that exact generation blocks. This is what keeps
127
+ * session-name reuse from letting an old claim block a brand-new session.
128
+ * - omitted, *any* non-stale claim on the name blocks. Conservative on purpose:
129
+ * a caller that cannot resolve the current generation must not be allowed to
130
+ * start a second writer on a guess.
131
+ *
132
+ * Read-only operations never call this, so inventory and status keep working
133
+ * while a session is claimed.
134
+ */
135
+ export declare function admitLocalOwnership(session: string, env?: NodeJS.ProcessEnv, generation?: string, now?: number): {
136
+ admitted: true;
137
+ } | {
138
+ admitted: false;
139
+ claim: MobileClaim;
140
+ };
141
+ /** Operator-facing explanation for a refused local attach. */
142
+ export declare function describeOwnershipRefusal(claim: MobileClaim): string;
143
+ //# sourceMappingURL=mobile-claim.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-claim.d.ts","sourceRoot":"","sources":["../../src/cli/mobile-claim.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,399 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { ContractError } from "../core/errors.js";
6
+ import { acquireLease } from "../storage/lease.js";
7
+ import { assertOmbSessionGeneration, listOmbSessions, ombSessionDir, } from "../omp/omb-sessions.js";
8
+ /**
9
+ * Exact-generation mobile ownership claims.
10
+ *
11
+ * A claim is the record that a mobile host has been handed one session, so the
12
+ * TUI does not start a second writer on the same journal. Three rules make it
13
+ * safe, and all three are enforced here rather than documented and hoped for:
14
+ *
15
+ * - **The generation is compared, never the name.** Claiming `bizar-foo` when a
16
+ * different journal now answers to that name must fail. That is the whole
17
+ * point of `generation`, and a name match is not evidence of anything.
18
+ * - **A live local owner refuses the claim.** Handing over a session someone is
19
+ * attached to is the dual-writer race, so an attached session is not claimable
20
+ * without an explicit controlled transition.
21
+ * - **A crashed claim does not lock a session forever.** The holder's pid is
22
+ * recorded; if the process is provably gone the claim is stale and may be taken
23
+ * over. Unknown liveness is never treated as gone.
24
+ */
25
+ const CLAIM_TTL_MS = 24 * 60 * 60 * 1000;
26
+ export function mobileClaimsDir(env = process.env) {
27
+ const base = env.XDG_STATE_HOME || join(env.HOME || homedir(), ".local", "state");
28
+ return join(base, "bizar-omp", "mobile-claims");
29
+ }
30
+ /**
31
+ * The per-session acquisition lock.
32
+ *
33
+ * Named after the session rather than shared, so claims for different sessions
34
+ * proceed independently — the contention is per ownership, not global.
35
+ */
36
+ export function claimLockPath(session, env = process.env) {
37
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(session)) {
38
+ throw new ContractError("session name is not a valid identifier");
39
+ }
40
+ const directory = mobileClaimsDir(env);
41
+ // The lock lives inside the claims directory, which may not exist yet on a
42
+ // freshly installed host. Created here so every caller of this function gets a
43
+ // usable path — otherwise the very first claim on a new machine fails with
44
+ // ENOENT before it can even start.
45
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
46
+ return join(directory, `${session}.lock`);
47
+ }
48
+ /** Serializes claim acquisition/release with OMB rename, continue and delete. */
49
+ export function acquireMobileSessionLease(session, env = process.env) {
50
+ return acquireLease(claimLockPath(session, env), "another process is changing ownership of this session");
51
+ }
52
+ export function claimPath(claimId, env = process.env) {
53
+ // The id is generated here, but a caller could pass anything; keep the
54
+ // filename to a safe alphabet so a claim id can never escape the directory.
55
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(claimId)) {
56
+ throw new ContractError("claim id is not a valid identifier");
57
+ }
58
+ return join(mobileClaimsDir(env), `${claimId}.json`);
59
+ }
60
+ export function writeClaimAtomic(path, claim) {
61
+ // Derive the directory from the path itself. Reading it back out of
62
+ // `process.env` would create a directory that is not the one being written
63
+ // whenever the caller passed its own environment.
64
+ const directory = dirname(path);
65
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
66
+ const temporary = `${path}.${process.pid}.tmp`;
67
+ const fd = openSync(temporary, "wx", 0o600);
68
+ try {
69
+ writeFileSync(fd, `${JSON.stringify(claim, null, 2)}\n`, "utf8");
70
+ fsyncSync(fd);
71
+ }
72
+ finally {
73
+ closeSync(fd);
74
+ }
75
+ renameSync(temporary, path);
76
+ // The rename itself is not durable until the *directory* entry is synced.
77
+ // Without this, a claim the CLI has already reported as acquired can vanish
78
+ // on power loss while the session journal it protects survived — the exact
79
+ // state where a second writer would be admitted with nothing to detect it.
80
+ fsyncDirectory(directory);
81
+ }
82
+ /**
83
+ * Makes a rename durable where the platform supports it.
84
+ *
85
+ * Directory fsync is POSIX and is not available on every platform or
86
+ * filesystem. When the open genuinely cannot be performed, the claim is still
87
+ * written and this is a no-op — failing here would turn a durability
88
+ * limitation into an outage, and the file is still correct on the next boot
89
+ * on any filesystem that journals the rename itself.
90
+ */
91
+ function fsyncDirectory(directory) {
92
+ let fd;
93
+ try {
94
+ fd = openSync(directory, "r");
95
+ }
96
+ catch {
97
+ // Windows cannot open a directory as a file; EPERM/EISDIR/EACCES/EINVAL all
98
+ // mean "not supported here" rather than "the claim was not written".
99
+ return;
100
+ }
101
+ try {
102
+ fsyncSync(fd);
103
+ }
104
+ catch {
105
+ // Some filesystems reject fsync on a directory handle. The rename already
106
+ // happened; losing the extra barrier is not worth failing the claim over.
107
+ }
108
+ finally {
109
+ closeSync(fd);
110
+ }
111
+ }
112
+ export function readClaimResult(claimId, env = process.env) {
113
+ let raw;
114
+ try {
115
+ raw = readFileSync(claimPath(claimId, env), "utf8");
116
+ }
117
+ catch (error) {
118
+ // ENOENT is genuinely absent. Anything else — permissions, a directory, an
119
+ // I/O error — means the record exists and could not be read, which is not
120
+ // the same thing at all.
121
+ const code = error.code;
122
+ return { claim: undefined, failure: code === "ENOENT" ? undefined : "unreadable" };
123
+ }
124
+ try {
125
+ const parsed = JSON.parse(raw);
126
+ if (typeof parsed !== "object" || parsed === null)
127
+ return { claim: undefined, failure: "malformed" };
128
+ const record = parsed;
129
+ if (typeof record.claimId !== "string" ||
130
+ typeof record.session !== "string" ||
131
+ typeof record.generation !== "string" ||
132
+ typeof record.owner !== "string" ||
133
+ typeof record.claimedAt !== "number" ||
134
+ // `pid` is optional: a claim created by a one-shot CLI has no holder pid.
135
+ (record.pid !== undefined && typeof record.pid !== "number")) {
136
+ return { claim: undefined, failure: "malformed" };
137
+ }
138
+ return {
139
+ failure: undefined,
140
+ claim: {
141
+ claimId: record.claimId,
142
+ session: record.session,
143
+ generation: record.generation,
144
+ owner: record.owner,
145
+ ...(typeof record.pid === "number" ? { pid: record.pid } : {}),
146
+ claimedAt: record.claimedAt,
147
+ },
148
+ };
149
+ }
150
+ catch {
151
+ return { claim: undefined, failure: "malformed" };
152
+ }
153
+ }
154
+ /** Convenience for callers that only care whether a usable claim exists. */
155
+ export function readClaim(claimId, env = process.env) {
156
+ return readClaimResult(claimId, env).claim;
157
+ }
158
+ export function listClaimsDetailed(env = process.env) {
159
+ let entries;
160
+ try {
161
+ entries = readdirSync(mobileClaimsDir(env));
162
+ }
163
+ catch {
164
+ return { claims: [], unreadable: [] };
165
+ }
166
+ const claims = [];
167
+ const unreadable = [];
168
+ for (const entry of entries) {
169
+ if (!entry.endsWith(".json"))
170
+ continue;
171
+ const claimId = entry.slice(0, -".json".length);
172
+ const result = readClaimResult(claimId, env);
173
+ if (result.claim !== undefined)
174
+ claims.push(result.claim);
175
+ else if (result.failure !== undefined)
176
+ unreadable.push({ claimId, reason: result.failure });
177
+ }
178
+ return { claims, unreadable };
179
+ }
180
+ export function listClaims(env = process.env) {
181
+ return listClaimsDetailed(env).claims;
182
+ }
183
+ /**
184
+ * A claim is stale only when its holder is provably gone, or it is simply old.
185
+ *
186
+ * `pid` is the *holder's* pid — normally the mobile host, supplied by the
187
+ * caller — and is deliberately not this process. `omb mobile sessions claim` is
188
+ * a one-shot command that exits immediately, so recording its own pid would
189
+ * make every claim look abandoned the moment it was created and break mutual
190
+ * exclusion entirely. A claim with no holder pid therefore expires only on its
191
+ * TTL, which is the safe direction: an abandoned claim is a nuisance, a claim
192
+ * that evaporates while its owner is still running is a second writer.
193
+ */
194
+ export function isClaimStale(claim, now = Date.now()) {
195
+ // Liveness is decided before age. The previous order checked the TTL first,
196
+ // so a mobile host that stayed up for more than 24 hours silently lost its
197
+ // ownership exclusion while it was demonstrably still running — and a second
198
+ // writer could then be admitted onto the same journal. Age is a fallback for
199
+ // claims with nobody to ask, not a way to override a live holder.
200
+ if (claim.pid === undefined) {
201
+ // No holder to probe: the TTL is the only evidence available, and
202
+ // expiring is the safe direction here. An abandoned claim is a nuisance;
203
+ // the reverse mistake is a second writer.
204
+ return now - claim.claimedAt > CLAIM_TTL_MS;
205
+ }
206
+ if (claim.pid === process.pid)
207
+ return false;
208
+ try {
209
+ // `kill 0` only performs the permission/existence check.
210
+ process.kill(claim.pid, 0);
211
+ // Positively alive, whatever the age.
212
+ return false;
213
+ }
214
+ catch (error) {
215
+ const code = error.code;
216
+ // EPERM means the process exists but belongs to another user: still alive.
217
+ // Anything other than a definite ESRCH is unknown liveness, and unknown
218
+ // is not dead — failing open here is exactly the bug this guards.
219
+ return code === "ESRCH";
220
+ }
221
+ }
222
+ export function releaseClaim(claimId, env = process.env) {
223
+ rmSync(claimPath(claimId, env), { force: true });
224
+ }
225
+ /**
226
+ * Claims one session for mobile ownership.
227
+ *
228
+ * The generation is re-resolved from a live scan rather than trusted from the
229
+ * caller, then re-checked a second time immediately before the claim is written.
230
+ * The gap between those two reads is the window a concurrent TUI action could
231
+ * exploit, and the second read is what closes it.
232
+ */
233
+ export async function claimSessionForMobile(input) {
234
+ const env = input.env ?? process.env;
235
+ const now = input.now ?? Date.now();
236
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(input.name)) {
237
+ throw new ContractError("session name is not a valid identifier");
238
+ }
239
+ const sessions = await listOmbSessions(input.cwd, env, { includeStopped: true, includeAllStopped: true, requireMultiplexer: true });
240
+ const session = findMobileClaimSession(sessions, input.name, input.generation);
241
+ if (!session.authorityKnown) {
242
+ throw new ContractError("session authority is unavailable");
243
+ }
244
+ if (session.generation !== input.generation) {
245
+ throw new ContractError("session changed externally");
246
+ }
247
+ assertMobileClaimCandidate(session);
248
+ // The existence check and the write that follows it are one serialized
249
+ // critical section. Previously they were separate steps, so two callers could
250
+ // both observe "no existing claim" and each write its own file for the same
251
+ // session and generation — both would report success, and the mutual
252
+ // exclusion the claim exists to provide was simply absent.
253
+ //
254
+ // The lease is Bizar's existing recoverable one, so a crashed holder does not
255
+ // wedge the session: recovery only rotates the lock when the recorded process
256
+ // is *provably* gone, and the receipt binds the pid to its process start time
257
+ // so a recycled pid cannot masquerade as the original owner.
258
+ const lease = acquireMobileSessionLease(input.name, env);
259
+ try {
260
+ // Re-evaluated inside the lock, not before it: this is the read that decides,
261
+ // and it has to be the read that happens after serialization.
262
+ const inventory = listClaimsDetailed(env);
263
+ // Fail closed. A record that exists but cannot be read is not evidence that
264
+ // nobody owns the session.
265
+ const blocking = inventory.unreadable.filter((entry) => entry.claimId.startsWith("corrupt-"));
266
+ if (blocking.length > 0) {
267
+ throw new ContractError("an existing mobile claim for this session could not be read; repair or quarantine it first", {
268
+ claimIds: blocking.map((entry) => entry.claimId),
269
+ reason: blocking[0]?.reason,
270
+ });
271
+ }
272
+ const existing = inventory.claims.find((claim) => claim.session === input.name);
273
+ if (existing !== undefined && !isClaimStale(existing, now)) {
274
+ throw new ContractError("session is already claimed for mobile ownership", {
275
+ claimId: existing.claimId,
276
+ owner: existing.owner,
277
+ });
278
+ }
279
+ // Re-resolve immediately before writing, still inside the lock, so a
280
+ // generation change during the wait cannot slip past.
281
+ const rechecked = await assertOmbSessionGeneration({ name: input.name, generation: input.generation }, input.cwd, env);
282
+ assertMobileClaimCandidate(rechecked);
283
+ const claim = {
284
+ claimId: randomUUID(),
285
+ session: input.name,
286
+ generation: input.generation,
287
+ owner: input.owner,
288
+ ...(input.holderPid === undefined ? {} : { pid: input.holderPid }),
289
+ claimedAt: now,
290
+ };
291
+ // A stale predecessor is released only once the successor is committed to
292
+ // disk, so a crash mid-takeover cannot leave the session unowned.
293
+ writeClaimAtomic(claimPath(claim.claimId, env), claim);
294
+ if (existing !== undefined)
295
+ releaseClaim(existing.claimId, env);
296
+ return {
297
+ claim,
298
+ sessionDir: ombSessionDir(rechecked.name, env),
299
+ cwd: rechecked.cwd,
300
+ takeover: existing !== undefined,
301
+ ...(rechecked.nativeId === undefined ? {} : { nativeId: rechecked.nativeId }),
302
+ ...(rechecked.nativeName === undefined ? {} : { nativeSessionName: rechecked.nativeName }),
303
+ };
304
+ }
305
+ finally {
306
+ lease.release();
307
+ }
308
+ }
309
+ /**
310
+ * Releases a claim, but only while the session still answers to the generation
311
+ * that was claimed. A session that changed under the claim is reported rather
312
+ * than silently released, so the caller can decide.
313
+ */
314
+ export async function releaseMobileClaim(input) {
315
+ const env = input.env ?? process.env;
316
+ const claim = readClaim(input.claimId, env);
317
+ if (claim === undefined) {
318
+ throw new ContractError("no such claim");
319
+ }
320
+ let generationHeld = true;
321
+ try {
322
+ await assertOmbSessionGeneration({ name: claim.session, generation: claim.generation }, input.cwd, env);
323
+ }
324
+ catch {
325
+ // The session moved on. The claim is still released, but the caller is told
326
+ // it was no longer holding the generation it thought it had.
327
+ generationHeld = false;
328
+ }
329
+ releaseClaim(input.claimId, env);
330
+ return { released: true, claim, generationHeld };
331
+ }
332
+ export function describeClaim(claim, now = Date.now()) {
333
+ return {
334
+ claimId: claim.claimId,
335
+ session: claim.session,
336
+ generation: claim.generation,
337
+ owner: claim.owner,
338
+ claimedAt: claim.claimedAt,
339
+ stale: isClaimStale(claim, now),
340
+ };
341
+ }
342
+ /** A detached tmux client does not prove that its native OMP writer stopped. */
343
+ export function assertMobileClaimCandidate(session) {
344
+ if (!session.authorityKnown)
345
+ throw new ContractError("session authority is unavailable");
346
+ if (session.attached)
347
+ throw new ContractError("session has a live local owner; release it locally first");
348
+ if (session.state !== "stopped" || session.processObserved || session.uncertainLiveness) {
349
+ throw new ContractError("session must be stopped and its native OMP process confirmed absent before mobile takeover");
350
+ }
351
+ if (!session.nativeSessionAvailable || typeof session.nativeId !== "string" || session.nativeId.length === 0) {
352
+ throw new ContractError("session has no readable native OMP identity to resume");
353
+ }
354
+ }
355
+ /** Resolve remote adoption only when the exact name and generation are unique. */
356
+ export function findMobileClaimSession(sessions, name, generation) {
357
+ const matches = sessions.filter(session => session.name === name && session.generation === generation);
358
+ if (matches.length !== 1)
359
+ throw new ContractError(matches.length === 0 ? "no such Bizar session generation" : "session identity is ambiguous");
360
+ return matches[0];
361
+ }
362
+ /**
363
+ * Centralized admission check for taking local ownership of a session.
364
+ *
365
+ * A claim records that a mobile host has been handed a session, but until this
366
+ * existed nothing on the local side consulted it: a local command could still
367
+ * attach to the same journal and become a second writer, which is the one thing
368
+ * the claim contract exists to prevent. It lives here rather than in a CLI
369
+ * command so every local ownership path inherits it.
370
+ *
371
+ * `generation` is optional and the difference matters:
372
+ *
373
+ * - given, only a claim for that exact generation blocks. This is what keeps
374
+ * session-name reuse from letting an old claim block a brand-new session.
375
+ * - omitted, *any* non-stale claim on the name blocks. Conservative on purpose:
376
+ * a caller that cannot resolve the current generation must not be allowed to
377
+ * start a second writer on a guess.
378
+ *
379
+ * Read-only operations never call this, so inventory and status keep working
380
+ * while a session is claimed.
381
+ */
382
+ export function admitLocalOwnership(session, env = process.env, generation, now = Date.now()) {
383
+ for (const claim of listClaimsDetailed(env).claims) {
384
+ if (claim.session !== session)
385
+ continue;
386
+ if (generation !== undefined && claim.generation !== generation)
387
+ continue;
388
+ if (isClaimStale(claim, now))
389
+ continue;
390
+ return { admitted: false, claim };
391
+ }
392
+ return { admitted: true };
393
+ }
394
+ /** Operator-facing explanation for a refused local attach. */
395
+ export function describeOwnershipRefusal(claim) {
396
+ return (`session ${claim.session} is claimed for mobile ownership by ${claim.owner}` +
397
+ ` (claim ${claim.claimId}). Release it on the phone, or run: omb mobile sessions release ${claim.claimId}`);
398
+ }
399
+ //# sourceMappingURL=mobile-claim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mobile-claim.js","sourceRoot":"","sources":["../../src/cli/mobile-claim.ts"],"names":[],"mappings":"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"}
@@ -15,7 +15,7 @@ export interface OmbCommand {
15
15
  usage: string;
16
16
  run(args: string[], env: NodeJS.ProcessEnv): Promise<number>;
17
17
  }
18
- export declare const HELP_USAGE = "omb help [command]\n\n Lists every omb command, or prints one command's usage.\n\n --json print the command list as { commands: [{ name, names, summary, usage }] }\n\n Arguments omb does not recognise are passed to OMP, so \"omb --resume\",\n \"omb <prompt>\", and bare \"omb\" keep working exactly as before. \"omb help\",\n \"omb --help\" and \"omb -h\" print this table instead; use \"omp --help\" for\n OMP's own help.";
18
+ export declare const HELP_USAGE = "omb help [command]\n\n Lists every omb command, or prints one command's usage.\n\n --json print public commands and the internal dispatch capabilities\n\n Arguments omb does not recognise are passed to OMP, so \"omb --resume\",\n \"omb <prompt>\", and bare \"omb\" keep working exactly as before. \"omb help\",\n \"omb --help\" and \"omb -h\" print this table instead; use \"omp --help\" for\n OMP's own help.";
19
19
  /** Look up a command by its first token; undefined means "pass through to OMP". */
20
20
  export declare function findOmbCommand(token: string): OmbCommand | undefined;
21
21
  export declare function renderOmbUsage(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"omb-commands.d.ts","sourceRoot":"","sources":["../../src/cli/omb-commands.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9D;AAED,eAAO,MAAM,UAAU,mbASL,CAAC;AAEnB,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAEpE;AAED,wBAAgB,cAAc,IAAI,MAAM,CAavC;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAwBrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,UAAU,EAiDpC,CAAC"}
1
+ {"version":3,"file":"omb-commands.d.ts","sourceRoot":"","sources":["../../src/cli/omb-commands.ts"],"names":[],"mappings":"AASA;;;;;;;GAOG;AACH,MAAM,WAAW,UAAU;IACzB,6CAA6C;IAC7C,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9D;AAED,eAAO,MAAM,UAAU,saASL,CAAC;AAEnB,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAEpE;AAED,wBAAgB,cAAc,IAAI,MAAM,CAavC;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAyBrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,UAAU,EAmEpC,CAAC"}
@@ -4,11 +4,13 @@ import { DASHBOARD_USAGE, runDashboardCommand } from "./omb-dashboard.js";
4
4
  import { RESTART_USAGE, STOP_USAGE, runRestartCommand, runStopCommand } from "./omb-runtime.js";
5
5
  import { TRAY_USAGE, runTrayCommand } from "./omb-tray.js";
6
6
  import { UPDATE_USAGE, runUpdateCommand } from "./omb-update.js";
7
+ import { SESSIONS_USAGE, runSessionsCommand } from "./omb-sessions.js";
8
+ import { MOBILE_USAGE, PAIR_USAGE, runMobileCommand, runPairCommand } from "./omb-mobile.js";
7
9
  export const HELP_USAGE = `omb help [command]
8
10
 
9
11
  Lists every omb command, or prints one command's usage.
10
12
 
11
- --json print the command list as { commands: [{ name, names, summary, usage }] }
13
+ --json print public commands and the internal dispatch capabilities
12
14
 
13
15
  Arguments omb does not recognise are passed to OMP, so "omb --resume",
14
16
  "omb <prompt>", and bare "omb" keep working exactly as before. "omb help",
@@ -41,6 +43,7 @@ export function runHelpCommand(args) {
41
43
  summary: command.summary,
42
44
  usage: command.usage,
43
45
  })),
46
+ internalCommands: ["--internal-mobile"],
44
47
  }, null, 2)}\n`);
45
48
  return 0;
46
49
  }
@@ -64,6 +67,18 @@ export function runHelpCommand(args) {
64
67
  * the hub needs the cwd its caller was started in, so both are forwarded here.
65
68
  */
66
69
  export const OMB_COMMANDS = [
70
+ {
71
+ names: ["pair"],
72
+ summary: "Pair the Android app with this host",
73
+ usage: PAIR_USAGE,
74
+ run: (args, env) => runPairCommand(args, env),
75
+ },
76
+ {
77
+ names: ["mobile"],
78
+ summary: "Manage the integrated mobile host and app workspace",
79
+ usage: MOBILE_USAGE,
80
+ run: (args, env) => runMobileCommand(args, env),
81
+ },
67
82
  {
68
83
  names: ["agents", "hub", "a"],
69
84
  summary: "Session hub: attach, start, rename, and stop sessions",
@@ -76,6 +91,12 @@ export const OMB_COMMANDS = [
76
91
  usage: DASHBOARD_USAGE,
77
92
  run: (args, env) => runDashboardCommand(args, env),
78
93
  },
94
+ {
95
+ names: ["sessions", "s"],
96
+ summary: "List Bizar-owned sessions, with machine-readable output",
97
+ usage: SESSIONS_USAGE,
98
+ run: (args, env) => runSessionsCommand(process.cwd(), args, env),
99
+ },
79
100
  {
80
101
  names: ["daemon"],
81
102
  summary: "Detached loopback service behind the dashboard",