@tonbo/yield 0.3.9 → 0.3.11

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.
package/dist/bin/yield.js CHANGED
@@ -1253,6 +1253,12 @@ async function assertPrivateRegularFile(filename) {
1253
1253
  var exec = promisify(execFile);
1254
1254
  var DEFAULT_LOGIN_HINT = credentialsContract.reader.login_hint;
1255
1255
  var LOOPBACK_HOST = "127.0.0.1";
1256
+ var BrowserUnavailableError = class extends Error {
1257
+ };
1258
+ function prefersDeviceLogin(env = process.env) {
1259
+ const ssh = Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.SSH_TTY);
1260
+ return ssh && !env.DISPLAY && !env.WAYLAND_DISPLAY;
1261
+ }
1256
1262
  function newLoginSecret() {
1257
1263
  return randomBytes(32).toString("base64url");
1258
1264
  }
@@ -1268,7 +1274,8 @@ var AuthClient = class {
1268
1274
  hostname;
1269
1275
  loginHint;
1270
1276
  entryLine;
1271
- constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT, entryLine = "cli") {
1277
+ env;
1278
+ constructor(credentials, fetcher, loginOrigin2 = "https://tonbo.dev", browserOpener = openBrowser, hostname = loginHostname(), loginHint = DEFAULT_LOGIN_HINT, entryLine = "cli", env = process.env) {
1272
1279
  this.credentials = credentials;
1273
1280
  this.fetcher = fetcher;
1274
1281
  this.loginOrigin = loginOrigin2;
@@ -1276,6 +1283,7 @@ var AuthClient = class {
1276
1283
  this.hostname = hostname;
1277
1284
  this.loginHint = loginHint;
1278
1285
  this.entryLine = entryLine;
1286
+ this.env = env;
1279
1287
  }
1280
1288
  /** The personal key requests are made with: TONBO_API_KEY, or the saved
1281
1289
  * login while it has not expired. */
@@ -1295,12 +1303,16 @@ var AuthClient = class {
1295
1303
  * a free port, then the one-time code is redeemed, with this device's login
1296
1304
  * secret, for the account's login key, which replaces this device's earlier
1297
1305
  * login key there ("CLI on <hostname>" in a personal account, `Yield CLI ·
1298
- * <8 hex>` in any other). The key joins the file's `accounts`; it becomes
1299
- * the top level too unless `current` is false (a login made only to publish
1300
- * into an organization). Answers that account's login.
1306
+ * <8 hex>` in any other). With `device`, in an SSH session without a
1307
+ * display, or when no browser can be opened, the same PKCE challenge goes
1308
+ * to a device-code login instead: the person enters a short code at
1309
+ * /cli/device in any browser and the CLI polls for the key. The key joins
1310
+ * the file's `accounts`; it becomes the top level too unless `current` is
1311
+ * false (a login made only to publish into an organization). Answers that
1312
+ * account's login.
1301
1313
  */
1302
1314
  async login(progress2 = () => {
1303
- }, account, { current: current2 = true } = {}) {
1315
+ }, account, { current: current2 = true, device } = {}) {
1304
1316
  const existing = await this.credentials.load().catch((error) => {
1305
1317
  if (error instanceof UnusableLoginError)
1306
1318
  return null;
@@ -1309,6 +1321,30 @@ var AuthClient = class {
1309
1321
  const loginSecret = existing?.login_secret ?? newLoginSecret();
1310
1322
  const verifier = randomBytes(48).toString("base64url");
1311
1323
  const challenge = createHash("sha256").update(verifier).digest("base64url");
1324
+ const redeemed = device ?? prefersDeviceLogin(this.env) ? await this.deviceLogin(challenge, verifier, loginSecret, account, progress2) : await this.browserLogin(challenge, verifier, loginSecret, account, progress2).catch((error) => {
1325
+ if (!(error instanceof BrowserUnavailableError))
1326
+ throw error;
1327
+ progress2({ status: "failed", step: "browser", reason: error.message });
1328
+ return this.deviceLogin(challenge, verifier, loginSecret, account, progress2);
1329
+ });
1330
+ const login = {
1331
+ account_id: redeemed.account_id,
1332
+ key_id: redeemed.api_key.id,
1333
+ key: redeemed.key,
1334
+ expires_at: redeemed.api_key.expires_at,
1335
+ kind: redeemed.account_kind ?? null,
1336
+ name: redeemed.account_name ?? null
1337
+ };
1338
+ progress2({ status: "started", step: "credential-store" });
1339
+ const latest = await this.credentials.load().catch(() => existing);
1340
+ await this.credentials.save(withAccountLogin(latest, login, {
1341
+ current: current2,
1342
+ loginSecret: latest?.login_secret ?? loginSecret
1343
+ }));
1344
+ progress2({ status: "completed", step: "credential-store" });
1345
+ return login;
1346
+ }
1347
+ async browserLogin(challenge, verifier, loginSecret, account, progress2) {
1312
1348
  const state = randomBytes(24).toString("base64url");
1313
1349
  const code = await this.listenForCode(state, (redirectUri) => {
1314
1350
  const authorization = new URL("/cli/login", this.loginOrigin);
@@ -1331,34 +1367,85 @@ var AuthClient = class {
1331
1367
  body: JSON.stringify({ code, code_verifier: verifier, login_secret: loginSecret })
1332
1368
  });
1333
1369
  progress2({ status: "completed", step: "key-redemption" });
1334
- const login = {
1335
- account_id: redeemed.account_id,
1336
- key_id: redeemed.api_key.id,
1337
- key: redeemed.key,
1338
- expires_at: redeemed.api_key.expires_at,
1339
- kind: redeemed.account_kind ?? null,
1340
- name: redeemed.account_name ?? null
1341
- };
1342
- progress2({ status: "started", step: "credential-store" });
1343
- const latest = await this.credentials.load().catch(() => existing);
1344
- await this.credentials.save(withAccountLogin(latest, login, {
1345
- current: current2,
1346
- loginSecret: latest?.login_secret ?? loginSecret
1347
- }));
1348
- progress2({ status: "completed", step: "credential-store" });
1349
- return login;
1370
+ return redeemed;
1371
+ }
1372
+ /**
1373
+ * The device-code login (RFC 8628's shape): the PKCE challenge and the
1374
+ * hostname buy a device code and a user code; the person approves the user
1375
+ * code at /cli/device, seeing the hostname; the CLI polls with the device
1376
+ * code, its verifier and the login secret, all in POST bodies, and the
1377
+ * approved poll answers the key once. A denied or expired code ends it.
1378
+ */
1379
+ async deviceLogin(challenge, verifier, loginSecret, account, progress2) {
1380
+ progress2({ status: "started", step: "device-code" });
1381
+ const started = await requestJson(this.fetcher, new URL("/api/cli/login/device", this.loginOrigin).toString(), {
1382
+ method: "POST",
1383
+ headers: { "content-type": "application/json" },
1384
+ body: JSON.stringify({
1385
+ code_challenge: challenge,
1386
+ code_challenge_method: "S256",
1387
+ hostname: this.hostname
1388
+ })
1389
+ });
1390
+ progress2({ status: "completed", step: "device-code" });
1391
+ const page = new URL("/cli/device", this.loginOrigin);
1392
+ const complete = new URL(page);
1393
+ complete.search = new URLSearchParams({
1394
+ code: started.user_code,
1395
+ from: this.entryLine,
1396
+ ...account ? { account } : {}
1397
+ }).toString();
1398
+ progress2({
1399
+ status: "device-code",
1400
+ step: "device-authorization",
1401
+ userCode: started.user_code,
1402
+ url: page.toString(),
1403
+ completeUrl: complete.toString(),
1404
+ expiresIn: started.expires_in
1405
+ });
1406
+ progress2({ status: "started", step: "device-authorization" });
1407
+ const expired = new Error(`The login code expired. Run \`${this.loginHint}\` again.`);
1408
+ const deadline = Date.now() + started.expires_in * 1e3;
1409
+ let interval = started.interval;
1410
+ for (; ; ) {
1411
+ await new Promise((resolve) => setTimeout(resolve, interval * 1e3));
1412
+ try {
1413
+ const redeemed = await requestJson(this.fetcher, new URL("/api/cli/login/device/token", this.loginOrigin).toString(), {
1414
+ method: "POST",
1415
+ headers: { "content-type": "application/json" },
1416
+ body: JSON.stringify({
1417
+ device_code: started.device_code,
1418
+ code_verifier: verifier,
1419
+ login_secret: loginSecret
1420
+ })
1421
+ });
1422
+ progress2({ status: "completed", step: "device-authorization" });
1423
+ return redeemed;
1424
+ } catch (error) {
1425
+ const code = error instanceof HttpError ? error.code : null;
1426
+ if (code === "slow_down")
1427
+ interval += 5;
1428
+ else if (code === "access_denied")
1429
+ throw new Error("The login was not approved (access_denied).");
1430
+ else if (code === "expired_token")
1431
+ throw expired;
1432
+ else if (code !== "authorization_pending" && !transient(error))
1433
+ throw error;
1434
+ if (Date.now() >= deadline)
1435
+ throw expired;
1436
+ }
1437
+ }
1350
1438
  }
1351
1439
  listenForCode(expectedState, authorizationUrl, progress2) {
1352
1440
  return new Promise((resolve, reject) => {
1353
1441
  let settled = false;
1354
1442
  const sockets = /* @__PURE__ */ new Set();
1355
1443
  let authorizationStarted = false;
1356
- const startAuthorization = (browserOpened = true) => {
1444
+ const startAuthorization = () => {
1357
1445
  if (authorizationStarted)
1358
1446
  return;
1359
1447
  authorizationStarted = true;
1360
- if (browserOpened)
1361
- progress2({ status: "completed", step: "browser" });
1448
+ progress2({ status: "completed", step: "browser" });
1362
1449
  progress2({ status: "started", step: "browser-authorization" });
1363
1450
  };
1364
1451
  const finish = (result, completedResponseSocket) => {
@@ -1428,18 +1515,17 @@ var AuthClient = class {
1428
1515
  void Promise.resolve().then(() => this.browserOpener(url)).then(() => startAuthorization(), (error) => {
1429
1516
  if (settled || authorizationStarted)
1430
1517
  return;
1431
- progress2({
1432
- status: "failed",
1433
- step: "browser",
1434
- reason: error instanceof Error ? error.message : String(error),
1435
- callbackPort: port
1518
+ finish({
1519
+ error: new BrowserUnavailableError(error instanceof Error ? error.message : String(error))
1436
1520
  });
1437
- startAuthorization(false);
1438
1521
  });
1439
1522
  });
1440
1523
  });
1441
1524
  }
1442
1525
  };
1526
+ function transient(error) {
1527
+ return error instanceof HttpError ? error.status === 429 || error.status >= 500 : error instanceof TypeError;
1528
+ }
1443
1529
  function respond(response, outcome, finished) {
1444
1530
  const page = callbackResponse(outcome);
1445
1531
  if (finished)
@@ -1918,7 +2004,7 @@ var USD = new Intl.NumberFormat("en-US", {
1918
2004
  // ../../packages/tonbo-cli-core/dist/src/login.js
1919
2005
  async function loginCommand(deps, options = {}) {
1920
2006
  try {
1921
- const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current });
2007
+ const login = await deps.auth.login((event) => reportLoginProgress(deps.progress, event), options.account, { current: options.current, device: options.device });
1922
2008
  deps.output(`Logged in to account ${login.name ?? login.account_id}.${login.expires_at ? ` The login expires ${login.expires_at}.` : ""}`);
1923
2009
  return login;
1924
2010
  } catch (error) {
@@ -1947,6 +2033,14 @@ var LOGIN_PROGRESS_MESSAGES = {
1947
2033
  started: "Receiving the CLI key",
1948
2034
  completed: "CLI key received"
1949
2035
  },
2036
+ "device-code": {
2037
+ started: "Requesting a login code",
2038
+ completed: "Login code received"
2039
+ },
2040
+ "device-authorization": {
2041
+ started: "Waiting for approval in a browser",
2042
+ completed: "Login approved"
2043
+ },
1950
2044
  "credential-store": {
1951
2045
  started: "Saving login",
1952
2046
  completed: "Login saved"
@@ -1958,9 +2052,13 @@ function reportLoginProgress(progress2, event) {
1958
2052
  return;
1959
2053
  }
1960
2054
  if (event.status === "failed") {
1961
- const port = event.callbackPort;
1962
- progress2.fail(`Could not open a browser: ${event.reason.replace(/\.$/, "")}.`);
1963
- progress2.note(`Open the URL above in a browser that can reach 127.0.0.1:${port} on this machine. From another computer, forward that port first: \`ssh -L ${port}:127.0.0.1:${port} <this host>\` or your VM's port forwarding both work.`);
2055
+ progress2.fail(`Could not open a browser: ${event.reason.replace(/\.$/, "")}. Approve with a code instead.`);
2056
+ return;
2057
+ }
2058
+ if (event.status === "device-code") {
2059
+ const minutes = Math.max(1, Math.round(event.expiresIn / 60));
2060
+ progress2.note(`To approve this login, open ${event.url} in any browser and enter the code ${event.userCode}. It expires in ${minutes} minutes.`);
2061
+ progress2.note(`Or open ${event.completeUrl}`);
1964
2062
  return;
1965
2063
  }
1966
2064
  const messages = LOGIN_PROGRESS_MESSAGES[event.step];
@@ -3397,8 +3495,8 @@ var recallContract = {
3397
3495
  "GET /files?repo=<name>&path=<path>&limit=<n>": "the turns that changed the file, newest first: {repo, path, change, direct, session, turn, src, device, sub, commit, base, at_ms, cwd, git_branch, stream_offset_from, stream_offset_to}; a row of another author's turn answers no src, device, cwd, git_branch or stream offsets (team_metadata)",
3398
3496
  "GET /sessions/<session id>/files?limit=<n>": "the files the session changed, newest first: {repo, path, change, direct, turn, commit, at_ms}",
3399
3497
  "GET /sessions?repo=<name>&limit=<n>": "sessions seen in the repository, most recent activity first: {session, src, device, sub, cwd, git_branch, started_ms, last_turn, last_ms}; a row of another author's session answers no src, device, cwd or git_branch (team_metadata)",
3400
- "GET /sessions?scope=team&limit=<n>": "the team catalog: the catalog's rows for every author in the workspace's account, each with its author (the stamped sub), under the catalog's credential rule; in an organization's workspace that is every member's team sessions (ADR-0017 D-1), in a personal one the person's own; the caller's own rows are the catalog's, and another author's row answers team_metadata's fields only: {session, author, last_ms, state, state_ms, paths_written, turns}",
3401
- "GET /sessions?limit=<n>": "the catalog (docs/design/yield-session-state.md): the sessions whose records the caller appended (their stamped author is the token's sub), most recent activity first, for a person's own key only (a token bound by git_workspace with no agent_id; an Agent's key and a token minted before ADR-0016 PR 3 answer 403) holding git.repos.read on the whole workspace (git:<ws_hex>:*) and denied it on none of it: {session, src, device, device_handle, cwd, git_root, git_branch, started_ms, last_turn, last_ms, state, state_ms, paths_written, paths_inferred}; a row of the organization-published form answers device, cwd and git_root null and names its device_handle, null otherwise; state is the session's latest session_state in stream order, null when none was reported, and paths_written is at most caps.catalog_paths paths, the turn in progress's first, then those of its newest turns; paths_inferred is the same of capture-v1 inbox.paths_inferred, kept apart and never exact"
3498
+ "GET /sessions?scope=team&limit=<n>": "the team catalog: the catalog's rows for every author in the workspace's account, each with its author (the stamped sub), under the catalog's credential rule; in an organization's workspace that is every member's team sessions (ADR-0017 D-1), in a personal one the person's own; the caller's own rows are the catalog's, and another author's row answers team_metadata's fields only: {session, author, project_id, last_ms, state, state_ms, paths_written, turns}",
3499
+ "GET /sessions?limit=<n>": "the catalog (docs/design/yield-session-state.md): the sessions whose records the caller appended (their stamped author is the token's sub), most recent activity first, for a person's own key only (a token bound by git_workspace with no agent_id; an Agent's key and a token minted before ADR-0016 PR 3 answer 403) holding git.repos.read on the whole workspace (git:<ws_hex>:*) and denied it on none of it: {session, src, device, device_handle, project_id, cwd, git_root, git_branch, started_ms, last_turn, last_ms, state, state_ms, paths_written, paths_inferred, turns}; a row of the organization-published form answers device, cwd and git_root null and names its device_handle, null otherwise; state is the session's latest session_state in stream order, null when none was reported, and paths_written is at most caps.catalog_paths paths, the turn in progress's first, then those of its newest turns; paths_inferred is the same of capture-v1 inbox.paths_inferred, kept apart and never exact; project_id is the Project the session's latest record named (capture-v1 inbox.project), null when none did, which the person's sessions of one Project on their other devices share; turns is team_metadata's turns, so the person's daemons compare their own sessions across devices (conflict alerts)"
3402
3500
  },
3403
3501
  "organization": "rows indexed from an organization's proc.inbox, whose records take the published form (contracts/sessions/capture-v1.json inbox.organization_published), answer cwd null, name the device only by its device_handle and never by a device id, and name paths relative to the bound folder; their provenance (session, stream_offset_from, stream_offset_to) points into the author's own session.<session> in the author's personal store (capture-v1 team_session_stream), or for a session captured before ADR-0017's metadata-only amendment into session.t.<session> in the organization's store, and only the author's rows carry it",
3404
3502
  "provenance": "every row the caller may read raw names its session, turn, commit and time; a reader reads the turn's records from session.<session> between stream_offset_from and stream_offset_to with its own Streams token. A team session's author reads them from their own personal store, and from the records they wrote to session.t.<session> before the metadata-only amendment; a teammate never reads them (team_metadata.raw)",
@@ -6558,105 +6656,609 @@ var LineFilter = class {
6558
6656
 
6559
6657
  // src/conflicts.ts
6560
6658
  import { execFile as execFile5 } from "node:child_process";
6561
- import { createHash as createHash7 } from "node:crypto";
6659
+ import { createHash as createHash8 } from "node:crypto";
6562
6660
  import path16 from "node:path";
6563
6661
  import { promisify as promisify5 } from "node:util";
6564
6662
 
6565
- // src/inbox.ts
6566
- import { execFile as execFile4 } from "node:child_process";
6567
- import { randomUUID as randomUUID7 } from "node:crypto";
6568
- import { appendFile, mkdir as mkdir5, open as open8, readFile as readFile9, rename as rename7, rm as rm7 } from "node:fs/promises";
6569
- import path15 from "node:path";
6570
- import { promisify as promisify4 } from "node:util";
6571
- import { setTimeout as delay4 } from "node:timers/promises";
6572
-
6573
- // src/handoffs.ts
6574
- async function listHandoffs(request) {
6575
- const data = await request("listHandoffs", {});
6576
- if (!Array.isArray(data)) throw new Error("The handoff list answered an invalid body.");
6577
- return data;
6578
- }
6579
- async function readHandoffEvents(request, after, wait) {
6580
- const answer = await request("listHandoffEvents", {
6581
- query: { after: String(after), wait: String(wait) }
6582
- });
6583
- if (!Array.isArray(answer?.events) || !Number.isSafeInteger(answer.next_after))
6584
- throw new Error("The handoff events answered an invalid body.");
6585
- return answer;
6586
- }
6587
- async function cancelHandoff(request, link3) {
6588
- const id = handoffId(link3);
6589
- const answer = await request(
6590
- "cancelPersonalHandoff",
6591
- { params: { id } }
6592
- );
6593
- if (answer.id !== id || answer.state !== "cancelled")
6594
- throw new Error("The handoff answered an invalid cancellation.");
6595
- return answer;
6596
- }
6597
- function handoffListLine(item, me, name) {
6598
- const who = (sub) => sub === me ? "you" : name(item.account_id, sub);
6599
- const parts = [`by ${who(item.author_sub)}`];
6600
- if (item.target_kind === "person" && item.target_sub) parts.push(`for ${who(item.target_sub)}`);
6601
- if (item.target_kind === "open") parts.push("for any member");
6602
- if (item.target_kind === "agent") parts.push(`for ${item.target_harness ?? "another harness"}`);
6603
- if (item.target_kind === "machine")
6604
- parts.push(`dispatched to your device on ${item.target_harness ?? "a harness"}`);
6605
- if (item.claimed_by_sub) parts.push(`taken by ${who(item.claimed_by_sub)}`);
6606
- return `${item.state.padEnd(10)} ${item.link} ${item.task} (${parts.join(", ")})`;
6607
- }
6608
- var VERBS2 = {
6609
- created: "created",
6610
- claimed: "took",
6611
- running: "started",
6612
- done: "finished",
6613
- need_human: "needs a human on",
6614
- failed: "failed",
6615
- returned: "returned",
6616
- cancelled: "cancelled"
6617
- };
6618
- function handoffEventLine(event, me, name) {
6619
- const actor = !event.actor_sub ? "someone" : event.actor_sub === me ? "you" : name(event.account_id, event.actor_sub);
6620
- return `#${event.seq} ${event.at} ${actor} ${VERBS2[event.kind] ?? event.kind} ${event.link} ${event.task}`;
6621
- }
6663
+ // src/device-conflicts.ts
6664
+ import { createHash as createHash7 } from "node:crypto";
6622
6665
 
6623
- // src/inbox.ts
6624
- var InboxStore = class {
6625
- constructor(directory2 = stateDirectory()) {
6626
- this.directory = directory2;
6627
- }
6628
- file(name) {
6629
- return path15.join(this.directory, name);
6630
- }
6631
- /** Appends messages to the spool (the daemon). */
6632
- async append(messages) {
6633
- if (!messages.length) return;
6634
- await mkdir5(this.directory, { recursive: true, mode: 448 });
6635
- await appendFile(
6636
- this.file("inbox.jsonl"),
6637
- messages.map((message3) => `${JSON.stringify(message3)}
6638
- `).join(""),
6639
- { mode: 384 }
6640
- );
6641
- }
6642
- /** Every message, oldest first, each once; a torn last line is skipped. */
6643
- async messages() {
6644
- const text3 = await readFile9(this.file("inbox.jsonl"), "utf8").catch(() => "");
6645
- const seen = /* @__PURE__ */ new Map();
6646
- for (const line of text3.split("\n")) {
6647
- if (!line.trim()) continue;
6648
- try {
6649
- const message3 = JSON.parse(line);
6650
- if (typeof message3?.id === "string" && !seen.has(message3.id)) seen.set(message3.id, message3);
6651
- } catch {
6666
+ // src/generated/machine.ts
6667
+ var machineRegisterDefs = {
6668
+ "byoc_arch": {
6669
+ "description": "A `byoc` device's architecture; grows only by a contract change. The regional machine_byoc_arches() equals it.",
6670
+ "enum": [
6671
+ "arm64",
6672
+ "amd64"
6673
+ ]
6674
+ },
6675
+ "byoc_client": {
6676
+ "description": "The client that registered a `byoc` device; grows only by a contract change. The regional machine_byoc_clients() equals it.",
6677
+ "enum": [
6678
+ "yield"
6679
+ ]
6680
+ },
6681
+ "byoc_client_version": {
6682
+ "description": "The client's version, a semantic version of at most 64 octets.",
6683
+ "type": "string",
6684
+ "maxLength": 64,
6685
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
6686
+ },
6687
+ "byoc_harness": {
6688
+ "description": "An agent harness `yield` captures on a `byoc` device: a key of contracts/sessions/capture-v1.json `sources`, its one authority, which automation/tests/test_machines_machine_v2_contract.py holds this enum equal to. The regional machine_byoc_harnesses() equals it.",
6689
+ "enum": [
6690
+ "claude-code",
6691
+ "codex"
6692
+ ]
6693
+ },
6694
+ "byoc_harnesses": {
6695
+ "description": "The harnesses installed on the device that its client captures, without duplicates.",
6696
+ "type": "array",
6697
+ "uniqueItems": true,
6698
+ "items": {
6699
+ "$ref": "#/$defs/byoc_harness"
6700
+ }
6701
+ },
6702
+ "byoc_hostname": {
6703
+ "description": "The device's hostname as the client reports it, at most 253 octets (the database counts octets). Personal data, answered to the device's owner only (machine-v2.json `byoc.visibility`).",
6704
+ "type": "string",
6705
+ "minLength": 1,
6706
+ "maxLength": 253
6707
+ },
6708
+ "byoc_os": {
6709
+ "description": "A `byoc` device's operating system family; grows only by a contract change. The regional machine_byoc_oses() equals it.",
6710
+ "enum": [
6711
+ "darwin",
6712
+ "linux"
6713
+ ]
6714
+ },
6715
+ "name": {
6716
+ "description": "A Machine name: the Agent name grammar (contracts/agents/agent-name-v1.json `$defs.name`, the same pattern and lengths, which the contract test holds equal) without its reserved-name list, which a Machine label cannot collide with (machine-v2.json `names`); 1-40 characters, no `--` and no `.`. Unique within its account across both kinds (ADR-0015 Q5); `<name>-<account>` must also pass the label rule of machine-v2.json `hostnames` for a managed Machine. `self` is reserved, so `/v1/machines/self` never names a Machine; global machine_name_valid follows this definition verbatim.",
6717
+ "type": "string",
6718
+ "minLength": 1,
6719
+ "maxLength": 40,
6720
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
6721
+ "not": {
6722
+ "const": "self"
6723
+ }
6724
+ },
6725
+ "register": {
6726
+ "description": "The body of `PUT /v1/machines/self` (machine-v2.json `actions` `register`, ADR-0015 `Registration`), signed by the CLI login key of the computer it registers. It has no `kind`: the server derives `byoc`, and a `kind` is refused as an unknown field. `name` is read only by the registration that creates the device; a device that exists keeps its name.",
6727
+ "type": "object",
6728
+ "additionalProperties": false,
6729
+ "required": [
6730
+ "device_id",
6731
+ "hostname",
6732
+ "os",
6733
+ "arch",
6734
+ "client",
6735
+ "client_version",
6736
+ "harnesses"
6737
+ ],
6738
+ "properties": {
6739
+ "device_id": {
6740
+ "$ref": "#/$defs/uuid"
6741
+ },
6742
+ "hostname": {
6743
+ "$ref": "#/$defs/byoc_hostname"
6744
+ },
6745
+ "os": {
6746
+ "$ref": "#/$defs/byoc_os"
6747
+ },
6748
+ "arch": {
6749
+ "$ref": "#/$defs/byoc_arch"
6750
+ },
6751
+ "client": {
6752
+ "$ref": "#/$defs/byoc_client"
6753
+ },
6754
+ "client_version": {
6755
+ "$ref": "#/$defs/byoc_client_version"
6756
+ },
6757
+ "harnesses": {
6758
+ "$ref": "#/$defs/byoc_harnesses"
6759
+ },
6760
+ "name": {
6761
+ "$ref": "#/$defs/name"
6652
6762
  }
6653
6763
  }
6654
- return [...seen.values()];
6655
- }
6656
- async cursor() {
6657
- return readJson(this.file("inbox-cursor.json"));
6764
+ },
6765
+ "uuid": {
6766
+ "description": "A lowercase canonical UUID.",
6767
+ "type": "string",
6768
+ "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
6658
6769
  }
6659
- async saveCursor(cursor) {
6770
+ };
6771
+ var byocPresence = {
6772
+ "heartbeat": {
6773
+ "interval_seconds": 300,
6774
+ "jitter_seconds": 30
6775
+ },
6776
+ "online_window_seconds": 900
6777
+ };
6778
+ var byocRetryable = [
6779
+ "device_registration_conflict",
6780
+ "device_report_throttled"
6781
+ ];
6782
+
6783
+ // src/devices.ts
6784
+ async function listDevices(list2) {
6785
+ const devices = [];
6786
+ const unavailable = /* @__PURE__ */ new Set();
6787
+ let cursor;
6788
+ for (; ; ) {
6789
+ const page = await list2(cursor ? { cursor } : {});
6790
+ devices.push(
6791
+ ...page.data.filter((machine) => machine.kind === "byoc")
6792
+ );
6793
+ for (const region of page.unavailable_regions ?? []) unavailable.add(region);
6794
+ if (!page.next_cursor || page.next_cursor === cursor) break;
6795
+ cursor = page.next_cursor;
6796
+ }
6797
+ return { devices, unavailableRegions: [...unavailable] };
6798
+ }
6799
+ var DISPATCHABLE = /* @__PURE__ */ new Set(["claude-code", "codex"]);
6800
+ function isOnline(lastSeen, now) {
6801
+ const seen = Date.parse(lastSeen);
6802
+ return Number.isFinite(seen) && now - seen <= byocPresence.online_window_seconds * 1e3;
6803
+ }
6804
+ function deviceRows(devices, device, now) {
6805
+ return devices.map((machine) => ({
6806
+ dispatch_target: machine.byoc.device_id !== device && machine.byoc.key_state === "active" && isOnline(machine.byoc.last_seen, now) && machine.byoc.harnesses.some((harness) => DISPATCHABLE.has(harness)),
6807
+ name: machine.name,
6808
+ os: machine.byoc.os,
6809
+ arch: machine.byoc.arch,
6810
+ harnesses: machine.byoc.harnesses,
6811
+ client_version: machine.byoc.client_version,
6812
+ last_seen: machine.byoc.last_seen,
6813
+ online: isOnline(machine.byoc.last_seen, now),
6814
+ key_state: machine.byoc.key_state,
6815
+ this_device: machine.byoc.device_id === device,
6816
+ device_id: machine.byoc.device_id ?? null,
6817
+ hostname: machine.byoc.hostname ?? null
6818
+ })).sort((a, b) => Number(b.this_device) - Number(a.this_device) || a.name.localeCompare(b.name));
6819
+ }
6820
+
6821
+ // src/recall.ts
6822
+ var str = (value) => typeof value === "string" ? value : null;
6823
+ var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
6824
+ var flag = (value) => value === true || value === 1;
6825
+ function rowsOf(body) {
6826
+ const record3 = body && typeof body === "object" ? body : null;
6827
+ const rows = Array.isArray(body) ? body : record3?.data ?? record3?.rows ?? record3?.files;
6828
+ if (!Array.isArray(rows)) throw new Error("The recall answer holds no rows.");
6829
+ return rows.filter((row) => !!row && typeof row === "object");
6830
+ }
6831
+ function parseFileTurns(body) {
6832
+ return rowsOf(body).flatMap((row) => {
6833
+ const session = str(row.session);
6834
+ const turn = num(row.turn);
6835
+ const commit = str(row.commit);
6836
+ const at = num(row.at_ms);
6837
+ if (!session || turn === null || !commit || at === null) return [];
6838
+ return [
6839
+ {
6840
+ repo: str(row.repo) ?? "",
6841
+ path: str(row.path) ?? "",
6842
+ change: str(row.change) ?? "?",
6843
+ direct: flag(row.direct),
6844
+ session,
6845
+ turn,
6846
+ src: str(row.src),
6847
+ device: str(row.device),
6848
+ sub: str(row.sub),
6849
+ commit,
6850
+ base: str(row.base),
6851
+ at_ms: at,
6852
+ cwd: str(row.cwd),
6853
+ git_branch: str(row.git_branch),
6854
+ stream_offset_from: str(row.stream_offset_from),
6855
+ stream_offset_to: str(row.stream_offset_to)
6856
+ }
6857
+ ];
6858
+ });
6859
+ }
6860
+ function parseSessionFiles(body) {
6861
+ return rowsOf(body).flatMap((row) => {
6862
+ const turn = num(row.turn);
6863
+ const path34 = str(row.path);
6864
+ const at = num(row.at_ms);
6865
+ if (turn === null || !path34 || at === null) return [];
6866
+ return [
6867
+ {
6868
+ repo: str(row.repo) ?? "",
6869
+ path: path34,
6870
+ change: str(row.change) ?? "?",
6871
+ direct: flag(row.direct),
6872
+ turn,
6873
+ commit: str(row.commit) ?? "",
6874
+ at_ms: at
6875
+ }
6876
+ ];
6877
+ });
6878
+ }
6879
+ function parseRepoSessions(body) {
6880
+ return rowsOf(body).flatMap((row) => {
6881
+ const session = str(row.session);
6882
+ if (!session) return [];
6883
+ return [
6884
+ {
6885
+ session,
6886
+ src: str(row.src),
6887
+ device: str(row.device),
6888
+ sub: str(row.sub),
6889
+ cwd: str(row.cwd),
6890
+ git_branch: str(row.git_branch),
6891
+ started_ms: num(row.started_ms),
6892
+ last_turn: num(row.last_turn),
6893
+ last_ms: num(row.last_ms)
6894
+ }
6895
+ ];
6896
+ });
6897
+ }
6898
+ function parseCatalog(body) {
6899
+ return rowsOf(body).flatMap((row) => {
6900
+ const session = str(row.session);
6901
+ if (!session) return [];
6902
+ const strings = (value) => (Array.isArray(value) ? value : []).filter(
6903
+ (path34) => typeof path34 === "string"
6904
+ );
6905
+ return [
6906
+ {
6907
+ session,
6908
+ src: str(row.src),
6909
+ device: str(row.device),
6910
+ cwd: str(row.cwd),
6911
+ git_root: str(row.git_root),
6912
+ git_branch: str(row.git_branch),
6913
+ started_ms: num(row.started_ms),
6914
+ last_turn: num(row.last_turn),
6915
+ last_ms: num(row.last_ms),
6916
+ state: str(row.state),
6917
+ state_ms: num(row.state_ms),
6918
+ paths_written: strings(row.paths_written),
6919
+ paths_inferred: strings(row.paths_inferred),
6920
+ device_handle: str(row.device_handle),
6921
+ author: str(row.author),
6922
+ project_id: str(row.project_id),
6923
+ turns: (Array.isArray(row.turns) ? row.turns : []).flatMap((value) => {
6924
+ const entry = value && typeof value === "object" ? value : {};
6925
+ const turn = num(entry.turn);
6926
+ const at = num(entry.at_ms);
6927
+ return turn === null || at === null ? [] : [{ turn, at_ms: at, paths_written: strings(entry.paths_written) }];
6928
+ })
6929
+ }
6930
+ ];
6931
+ });
6932
+ }
6933
+ function indexUnavailableReason(status, body) {
6934
+ const record3 = body && typeof body === "object" ? body : {};
6935
+ if (status !== 503 || record3.code !== "index_unavailable") return null;
6936
+ const index = record3.index && typeof record3.index === "object" ? record3.index : {};
6937
+ const phase = str(index.phase);
6938
+ const kind = str(index.kind) ?? (num(index.kind) === null ? null : String(index.kind));
6939
+ const since = str(index.since);
6940
+ const time2 = since && Number.isFinite(Date.parse(since)) ? since.slice(11, 16) : null;
6941
+ const where = [phase && kind ? `${phase}: ${kind}` : phase, time2 && `since ${time2} UTC`].filter(Boolean).join(" ");
6942
+ return `the index is not open yet${where ? ` (${where})` : ""}`;
6943
+ }
6944
+ function recallLimit(limit = RECALL_DEFAULT_LIMIT) {
6945
+ return Math.max(1, Math.min(RECALL_MAX_LIMIT, Math.floor(limit)));
6946
+ }
6947
+ var RecallClient = class {
6948
+ constructor(options) {
6949
+ this.options = options;
6950
+ }
6951
+ async files(repo, path34, limit) {
6952
+ return parseFileTurns(
6953
+ await this.get("/files", { repo, path: path34, limit: String(recallLimit(limit)) })
6954
+ );
6955
+ }
6956
+ async sessionFiles(session, limit) {
6957
+ return parseSessionFiles(
6958
+ await this.get(`/sessions/${encodeURIComponent(session)}/files`, {
6959
+ limit: String(recallLimit(limit))
6960
+ })
6961
+ );
6962
+ }
6963
+ async sessions(repo, limit) {
6964
+ return parseRepoSessions(
6965
+ await this.get("/sessions", { repo, limit: String(recallLimit(limit)) })
6966
+ );
6967
+ }
6968
+ /** The catalog: the caller's sessions of the index, most recent activity
6969
+ * first; `team`, every author's (recall-v1 `GET /sessions?scope=team`). */
6970
+ async catalog(limit, options = {}) {
6971
+ return parseCatalog(
6972
+ await this.get("/sessions", {
6973
+ ...options.team ? { scope: "team" } : {},
6974
+ limit: String(recallLimit(limit))
6975
+ })
6976
+ );
6977
+ }
6978
+ async get(path34, query) {
6979
+ const { fetch: fetcher, tokens: tokens2, key, origin, workspace } = this.options;
6980
+ const url = `${recallBase(origin, workspace)}${path34}?${new URLSearchParams(query)}`;
6981
+ const response = await tokens2.send(
6982
+ key,
6983
+ (token) => fetcher(url, {
6984
+ redirect: "manual",
6985
+ signal: AbortSignal.timeout(3e4),
6986
+ headers: { authorization: `Bearer ${token}`, accept: "application/json" }
6987
+ })
6988
+ );
6989
+ const body = await response.json().catch(() => null);
6990
+ if (response.ok) return body;
6991
+ const record3 = body && typeof body === "object" ? body : {};
6992
+ const detail = indexUnavailableReason(response.status, body) ?? str(record3.detail) ?? str(record3.title) ?? str(record3.error);
6993
+ throw new HttpError(
6994
+ response.status === 404 ? "The recall API answered 404: it is not available for this workspace yet." : `The recall API answered ${response.status}${detail ? `: ${detail}` : "."}`,
6995
+ response.status,
6996
+ body,
6997
+ response.headers.get("x-request-id")
6998
+ );
6999
+ }
7000
+ };
7001
+
7002
+ // src/team-conflicts.ts
7003
+ import { createHash as createHash6 } from "node:crypto";
7004
+ var TEAM_POLL_MS = 6e4;
7005
+ var NAMES_KEPT_MS = 10 * 6e4;
7006
+ function liveRow(row, now) {
7007
+ const [shown] = psRows([row], { device: "", now, all: true });
7008
+ return ["running", "waiting", "idle"].includes(shown?.shown_state ?? "");
7009
+ }
7010
+ function teamConflicts(mine, catalog, options) {
7011
+ const found = [];
7012
+ for (const row of catalog.rows) {
7013
+ if (!row.author || row.author === catalog.me || !liveRow(row, options.now)) continue;
7014
+ const theirs = /* @__PURE__ */ new Set();
7015
+ for (const turn of row.turns)
7016
+ if (options.now - turn.at_ms < CONFLICT_WINDOW_MS) {
7017
+ for (const file of turn.paths_written) if (!ignoredPath(file)) theirs.add(file);
7018
+ }
7019
+ if (!theirs.size) continue;
7020
+ for (const own of mine) {
7021
+ const pair = `t ${own.session} ${row.session}`;
7022
+ if (options.alerted(pair)) continue;
7023
+ const files = Object.entries(own.paths).filter(([file, ms]) => options.now - ms < CONFLICT_WINDOW_MS && theirs.has(file)).map(([file]) => file).sort();
7024
+ if (files.length)
7025
+ found.push({
7026
+ pair,
7027
+ session: own.session,
7028
+ teammate: row.session,
7029
+ author: row.author,
7030
+ files
7031
+ });
7032
+ }
7033
+ }
7034
+ return found;
7035
+ }
7036
+ function teamConflictMessage(conflict, name, now) {
7037
+ const shown = conflict.files.slice(0, 3).join(", ") + (conflict.files.length > 3 ? `, +${conflict.files.length - 3}` : "");
7038
+ const who = name ?? `teammate ${conflict.author.slice(0, 8)}`;
7039
+ return {
7040
+ id: `c${createHash6("sha256").update(conflict.pair).digest("hex").slice(0, 10)}`,
7041
+ kind: "conflict",
7042
+ at: new Date(now).toISOString(),
7043
+ recipient: { kind: "session", id: conflict.session },
7044
+ reason: "a teammate's live session wrote the same file",
7045
+ source: { kind: "local", detail: conflict.teammate },
7046
+ content: `${who}'s session ${conflict.teammate.slice(0, 8)} in this team Project also wrote ${shown} in the last 2 h`,
7047
+ notify: true
7048
+ };
7049
+ }
7050
+ function teamCatalogReader(options) {
7051
+ const people = /* @__PURE__ */ new Map();
7052
+ return async (accountId) => {
7053
+ const file = await options.credentials.load();
7054
+ const login = file ? accountLogins(file).find(
7055
+ (entry) => entry.account_id === accountId && entry.kind !== "user" && !isExpiredLogin(entry)
7056
+ ) : void 0;
7057
+ if (!login) throw new Error(`no current login of organization ${accountId}`);
7058
+ let known = people.get(accountId);
7059
+ if (!known || known.key !== login.key || Date.now() - known.at > NAMES_KEPT_MS) {
7060
+ const [account, members, store] = await Promise.all([
7061
+ options.api.getAccount(login.key),
7062
+ options.api.page(
7063
+ "listMembers",
7064
+ login.key,
7065
+ { limit: 100 }
7066
+ ),
7067
+ getSessionsStore(options.fetch, options.apiOrigin, login.key, { team: true })
7068
+ ]);
7069
+ known = {
7070
+ at: Date.now(),
7071
+ key: login.key,
7072
+ me: account.member.subject,
7073
+ workspace: workspaceHex(store.workspace_id),
7074
+ names: new Map(
7075
+ members.data.map((member2) => [
7076
+ member2.subject,
7077
+ member2.display_name ?? member2.name ?? member2.subject
7078
+ ])
7079
+ )
7080
+ };
7081
+ people.set(accountId, known);
7082
+ }
7083
+ const recall = new RecallClient({
7084
+ fetch: options.fetch,
7085
+ tokens: options.gitTokens,
7086
+ key: login.key,
7087
+ origin: gitOrigin(options.apiOrigin),
7088
+ workspace: known.workspace
7089
+ });
7090
+ return { me: known.me, names: known.names, rows: await recall.catalog(100, { team: true }) };
7091
+ };
7092
+ }
7093
+
7094
+ // src/device-conflicts.ts
7095
+ function deviceConflicts(mine, rows, options) {
7096
+ const found = [];
7097
+ for (const row of rows) {
7098
+ if (!row.device || row.device === options.device || !row.project_id || !row.git_root) continue;
7099
+ if (!liveRow(row, options.now)) continue;
7100
+ const theirs = /* @__PURE__ */ new Set();
7101
+ for (const turn of row.turns)
7102
+ if (options.now - turn.at_ms < CONFLICT_WINDOW_MS) {
7103
+ for (const file of turn.paths_written) if (!ignoredPath(file)) theirs.add(file);
7104
+ }
7105
+ if (!theirs.size) continue;
7106
+ for (const own of mine) {
7107
+ if (own.project !== row.project_id) continue;
7108
+ const pair = `d ${own.session} ${row.session}`;
7109
+ if (options.alerted(pair)) continue;
7110
+ const files = Object.entries(own.paths).filter(([file, ms]) => options.now - ms < CONFLICT_WINDOW_MS && theirs.has(file)).map(([file]) => file).sort();
7111
+ if (files.length) found.push({ pair, session: own.session, other: row, files });
7112
+ }
7113
+ }
7114
+ return found;
7115
+ }
7116
+ function deviceConflictMessage(conflict, names, now, home) {
7117
+ const shown = conflict.files.slice(0, 3).join(", ") + (conflict.files.length > 3 ? `, +${conflict.files.length - 3}` : "");
7118
+ const { other } = conflict;
7119
+ const device = other.device;
7120
+ const on = names.get(device) ?? `device ${device.slice(0, 8)}`;
7121
+ const where = other.git_root ? ` in ${tilde(other.git_root, home)}` : "";
7122
+ return {
7123
+ id: `c${createHash7("sha256").update(conflict.pair).digest("hex").slice(0, 10)}`,
7124
+ kind: "conflict",
7125
+ at: new Date(now).toISOString(),
7126
+ recipient: { kind: "session", id: conflict.session },
7127
+ reason: "your session on another device wrote the same file",
7128
+ source: { kind: "local", detail: other.session },
7129
+ content: `your ${other.src ?? "agent"} session ${other.session.slice(0, 8)} on ${on}${where} also wrote ${shown} in the last 2 h`,
7130
+ notify: true
7131
+ };
7132
+ }
7133
+ var KEPT_MS = 10 * 6e4;
7134
+ function ownCatalogReader(options) {
7135
+ const now = options.now ?? (() => Date.now());
7136
+ let known = null;
7137
+ return async () => {
7138
+ const key = await options.key();
7139
+ if (!key) throw new Error("no current personal login");
7140
+ if (!known || known.key !== key || now() - known.at > KEPT_MS) {
7141
+ const [store, { devices }] = await Promise.all([
7142
+ getSessionsStore(options.fetch, options.apiOrigin, key),
7143
+ listDevices((page) => options.api.listMachines(key, page))
7144
+ ]);
7145
+ known = {
7146
+ at: now(),
7147
+ key,
7148
+ workspace: workspaceHex(store.workspace_id),
7149
+ names: new Map(
7150
+ devices.flatMap(
7151
+ (machine) => machine.byoc.device_id ? [[machine.byoc.device_id, machine.name]] : []
7152
+ )
7153
+ )
7154
+ };
7155
+ }
7156
+ const recall = new RecallClient({
7157
+ fetch: options.fetch,
7158
+ tokens: options.gitTokens,
7159
+ key,
7160
+ origin: gitOrigin(options.apiOrigin),
7161
+ workspace: known.workspace
7162
+ });
7163
+ return { rows: await recall.catalog(100), names: known.names };
7164
+ };
7165
+ }
7166
+
7167
+ // src/inbox.ts
7168
+ import { execFile as execFile4 } from "node:child_process";
7169
+ import { randomUUID as randomUUID7 } from "node:crypto";
7170
+ import { appendFile, mkdir as mkdir5, open as open8, readFile as readFile9, rename as rename7, rm as rm7 } from "node:fs/promises";
7171
+ import path15 from "node:path";
7172
+ import { promisify as promisify4 } from "node:util";
7173
+ import { setTimeout as delay4 } from "node:timers/promises";
7174
+
7175
+ // src/handoffs.ts
7176
+ async function listHandoffs(request) {
7177
+ const data = await request("listHandoffs", {});
7178
+ if (!Array.isArray(data)) throw new Error("The handoff list answered an invalid body.");
7179
+ return data;
7180
+ }
7181
+ async function readHandoffEvents(request, after, wait) {
7182
+ const answer = await request("listHandoffEvents", {
7183
+ query: { after: String(after), wait: String(wait) }
7184
+ });
7185
+ if (!Array.isArray(answer?.events) || !Number.isSafeInteger(answer.next_after))
7186
+ throw new Error("The handoff events answered an invalid body.");
7187
+ return answer;
7188
+ }
7189
+ async function cancelHandoff(request, link3) {
7190
+ const id = handoffId(link3);
7191
+ const answer = await request(
7192
+ "cancelPersonalHandoff",
7193
+ { params: { id } }
7194
+ );
7195
+ if (answer.id !== id || answer.state !== "cancelled")
7196
+ throw new Error("The handoff answered an invalid cancellation.");
7197
+ return answer;
7198
+ }
7199
+ function handoffListLine(item, me, name) {
7200
+ const who = (sub) => sub === me ? "you" : name(item.account_id, sub);
7201
+ const parts = [`by ${who(item.author_sub)}`];
7202
+ if (item.target_kind === "person" && item.target_sub) parts.push(`for ${who(item.target_sub)}`);
7203
+ if (item.target_kind === "open") parts.push("for any member");
7204
+ if (item.target_kind === "agent") parts.push(`for ${item.target_harness ?? "another harness"}`);
7205
+ if (item.target_kind === "machine")
7206
+ parts.push(`dispatched to your device on ${item.target_harness ?? "a harness"}`);
7207
+ if (item.claimed_by_sub) parts.push(`taken by ${who(item.claimed_by_sub)}`);
7208
+ return `${item.state.padEnd(10)} ${item.link} ${item.task} (${parts.join(", ")})`;
7209
+ }
7210
+ var VERBS2 = {
7211
+ created: "created",
7212
+ claimed: "took",
7213
+ running: "started",
7214
+ done: "finished",
7215
+ need_human: "needs a human on",
7216
+ failed: "failed",
7217
+ returned: "returned",
7218
+ cancelled: "cancelled"
7219
+ };
7220
+ function handoffEventLine(event, me, name) {
7221
+ const actor = !event.actor_sub ? "someone" : event.actor_sub === me ? "you" : name(event.account_id, event.actor_sub);
7222
+ return `#${event.seq} ${event.at} ${actor} ${VERBS2[event.kind] ?? event.kind} ${event.link} ${event.task}`;
7223
+ }
7224
+
7225
+ // src/inbox.ts
7226
+ var InboxStore = class {
7227
+ constructor(directory2 = stateDirectory()) {
7228
+ this.directory = directory2;
7229
+ }
7230
+ file(name) {
7231
+ return path15.join(this.directory, name);
7232
+ }
7233
+ /** Appends messages to the spool (the daemon). */
7234
+ async append(messages) {
7235
+ if (!messages.length) return;
7236
+ await mkdir5(this.directory, { recursive: true, mode: 448 });
7237
+ await appendFile(
7238
+ this.file("inbox.jsonl"),
7239
+ messages.map((message3) => `${JSON.stringify(message3)}
7240
+ `).join(""),
7241
+ { mode: 384 }
7242
+ );
7243
+ }
7244
+ /** Every message, oldest first, each once; a torn last line is skipped. */
7245
+ async messages() {
7246
+ const text3 = await readFile9(this.file("inbox.jsonl"), "utf8").catch(() => "");
7247
+ const seen = /* @__PURE__ */ new Map();
7248
+ for (const line of text3.split("\n")) {
7249
+ if (!line.trim()) continue;
7250
+ try {
7251
+ const message3 = JSON.parse(line);
7252
+ if (typeof message3?.id === "string" && !seen.has(message3.id)) seen.set(message3.id, message3);
7253
+ } catch {
7254
+ }
7255
+ }
7256
+ return [...seen.values()];
7257
+ }
7258
+ async cursor() {
7259
+ return readJson(this.file("inbox-cursor.json"));
7260
+ }
7261
+ async saveCursor(cursor) {
6660
7262
  await writeJson(this.directory, this.file("inbox-cursor.json"), cursor);
6661
7263
  }
6662
7264
  async marks() {
@@ -6736,7 +7338,7 @@ function messageFor(event, me, actorName) {
6736
7338
  return null;
6737
7339
  case "claimed":
6738
7340
  if (!author || byMe) return null;
6739
- return make("claimed", "you gave this handoff", false, `${actor} took ${event.link}`);
7341
+ return make("claimed", "you gave this handoff", true, `${actor} took ${event.link}`);
6740
7342
  case "done":
6741
7343
  case "failed":
6742
7344
  case "returned": {
@@ -6879,305 +7481,31 @@ function inboxPoll(options) {
6879
7481
  names: new Map(
6880
7482
  data.map((member2) => [
6881
7483
  member2.subject,
6882
- member2.display_name ?? member2.name ?? member2.subject
6883
- ])
6884
- )
6885
- };
6886
- names.set(accountId, entry);
6887
- }
6888
- return entry.names.get(sub) ?? null;
6889
- }
6890
- };
6891
- }
6892
- function inboxLine(message3, status) {
6893
- return `${status.padEnd(11)} ${message3.id} ${message3.at} ${message3.content} (${message3.reason})`;
6894
- }
6895
- async function ackHandoff(store, handoffId2) {
6896
- try {
6897
- const marks = await store.marks();
6898
- const ids = (await store.messages()).filter(
6899
- (message3) => message3.source.kind === "handoff" && message3.source.handoff_id === handoffId2 && unread(statusOf(marks, message3.id))
6900
- ).map((message3) => message3.id);
6901
- if (!ids.length) return;
6902
- const at = (/* @__PURE__ */ new Date()).toISOString();
6903
- for (const id of ids) marks.marks[id] = { ...marks.marks[id], status: "read", at };
6904
- await store.saveMarks(marks);
6905
- } catch {
6906
- }
6907
- }
6908
-
6909
- // src/team-conflicts.ts
6910
- import { createHash as createHash6 } from "node:crypto";
6911
-
6912
- // src/recall.ts
6913
- var str = (value) => typeof value === "string" ? value : null;
6914
- var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
6915
- var flag = (value) => value === true || value === 1;
6916
- function rowsOf(body) {
6917
- const record3 = body && typeof body === "object" ? body : null;
6918
- const rows = Array.isArray(body) ? body : record3?.data ?? record3?.rows ?? record3?.files;
6919
- if (!Array.isArray(rows)) throw new Error("The recall answer holds no rows.");
6920
- return rows.filter((row) => !!row && typeof row === "object");
6921
- }
6922
- function parseFileTurns(body) {
6923
- return rowsOf(body).flatMap((row) => {
6924
- const session = str(row.session);
6925
- const turn = num(row.turn);
6926
- const commit = str(row.commit);
6927
- const at = num(row.at_ms);
6928
- if (!session || turn === null || !commit || at === null) return [];
6929
- return [
6930
- {
6931
- repo: str(row.repo) ?? "",
6932
- path: str(row.path) ?? "",
6933
- change: str(row.change) ?? "?",
6934
- direct: flag(row.direct),
6935
- session,
6936
- turn,
6937
- src: str(row.src),
6938
- device: str(row.device),
6939
- sub: str(row.sub),
6940
- commit,
6941
- base: str(row.base),
6942
- at_ms: at,
6943
- cwd: str(row.cwd),
6944
- git_branch: str(row.git_branch),
6945
- stream_offset_from: str(row.stream_offset_from),
6946
- stream_offset_to: str(row.stream_offset_to)
6947
- }
6948
- ];
6949
- });
6950
- }
6951
- function parseSessionFiles(body) {
6952
- return rowsOf(body).flatMap((row) => {
6953
- const turn = num(row.turn);
6954
- const path34 = str(row.path);
6955
- const at = num(row.at_ms);
6956
- if (turn === null || !path34 || at === null) return [];
6957
- return [
6958
- {
6959
- repo: str(row.repo) ?? "",
6960
- path: path34,
6961
- change: str(row.change) ?? "?",
6962
- direct: flag(row.direct),
6963
- turn,
6964
- commit: str(row.commit) ?? "",
6965
- at_ms: at
6966
- }
6967
- ];
6968
- });
6969
- }
6970
- function parseRepoSessions(body) {
6971
- return rowsOf(body).flatMap((row) => {
6972
- const session = str(row.session);
6973
- if (!session) return [];
6974
- return [
6975
- {
6976
- session,
6977
- src: str(row.src),
6978
- device: str(row.device),
6979
- sub: str(row.sub),
6980
- cwd: str(row.cwd),
6981
- git_branch: str(row.git_branch),
6982
- started_ms: num(row.started_ms),
6983
- last_turn: num(row.last_turn),
6984
- last_ms: num(row.last_ms)
6985
- }
6986
- ];
6987
- });
6988
- }
6989
- function parseCatalog(body) {
6990
- return rowsOf(body).flatMap((row) => {
6991
- const session = str(row.session);
6992
- if (!session) return [];
6993
- const strings = (value) => (Array.isArray(value) ? value : []).filter(
6994
- (path34) => typeof path34 === "string"
6995
- );
6996
- return [
6997
- {
6998
- session,
6999
- src: str(row.src),
7000
- device: str(row.device),
7001
- cwd: str(row.cwd),
7002
- git_root: str(row.git_root),
7003
- git_branch: str(row.git_branch),
7004
- started_ms: num(row.started_ms),
7005
- last_turn: num(row.last_turn),
7006
- last_ms: num(row.last_ms),
7007
- state: str(row.state),
7008
- state_ms: num(row.state_ms),
7009
- paths_written: strings(row.paths_written),
7010
- paths_inferred: strings(row.paths_inferred),
7011
- device_handle: str(row.device_handle),
7012
- author: str(row.author),
7013
- turns: (Array.isArray(row.turns) ? row.turns : []).flatMap((value) => {
7014
- const entry = value && typeof value === "object" ? value : {};
7015
- const turn = num(entry.turn);
7016
- const at = num(entry.at_ms);
7017
- return turn === null || at === null ? [] : [{ turn, at_ms: at, paths_written: strings(entry.paths_written) }];
7018
- })
7019
- }
7020
- ];
7021
- });
7022
- }
7023
- function indexUnavailableReason(status, body) {
7024
- const record3 = body && typeof body === "object" ? body : {};
7025
- if (status !== 503 || record3.code !== "index_unavailable") return null;
7026
- const index = record3.index && typeof record3.index === "object" ? record3.index : {};
7027
- const phase = str(index.phase);
7028
- const kind = str(index.kind) ?? (num(index.kind) === null ? null : String(index.kind));
7029
- const since = str(index.since);
7030
- const time2 = since && Number.isFinite(Date.parse(since)) ? since.slice(11, 16) : null;
7031
- const where = [phase && kind ? `${phase}: ${kind}` : phase, time2 && `since ${time2} UTC`].filter(Boolean).join(" ");
7032
- return `the index is not open yet${where ? ` (${where})` : ""}`;
7033
- }
7034
- function recallLimit(limit = RECALL_DEFAULT_LIMIT) {
7035
- return Math.max(1, Math.min(RECALL_MAX_LIMIT, Math.floor(limit)));
7036
- }
7037
- var RecallClient = class {
7038
- constructor(options) {
7039
- this.options = options;
7040
- }
7041
- async files(repo, path34, limit) {
7042
- return parseFileTurns(
7043
- await this.get("/files", { repo, path: path34, limit: String(recallLimit(limit)) })
7044
- );
7045
- }
7046
- async sessionFiles(session, limit) {
7047
- return parseSessionFiles(
7048
- await this.get(`/sessions/${encodeURIComponent(session)}/files`, {
7049
- limit: String(recallLimit(limit))
7050
- })
7051
- );
7052
- }
7053
- async sessions(repo, limit) {
7054
- return parseRepoSessions(
7055
- await this.get("/sessions", { repo, limit: String(recallLimit(limit)) })
7056
- );
7057
- }
7058
- /** The catalog: the caller's sessions of the index, most recent activity
7059
- * first; `team`, every author's (recall-v1 `GET /sessions?scope=team`). */
7060
- async catalog(limit, options = {}) {
7061
- return parseCatalog(
7062
- await this.get("/sessions", {
7063
- ...options.team ? { scope: "team" } : {},
7064
- limit: String(recallLimit(limit))
7065
- })
7066
- );
7067
- }
7068
- async get(path34, query) {
7069
- const { fetch: fetcher, tokens: tokens2, key, origin, workspace } = this.options;
7070
- const url = `${recallBase(origin, workspace)}${path34}?${new URLSearchParams(query)}`;
7071
- const response = await tokens2.send(
7072
- key,
7073
- (token) => fetcher(url, {
7074
- redirect: "manual",
7075
- signal: AbortSignal.timeout(3e4),
7076
- headers: { authorization: `Bearer ${token}`, accept: "application/json" }
7077
- })
7078
- );
7079
- const body = await response.json().catch(() => null);
7080
- if (response.ok) return body;
7081
- const record3 = body && typeof body === "object" ? body : {};
7082
- const detail = indexUnavailableReason(response.status, body) ?? str(record3.detail) ?? str(record3.title) ?? str(record3.error);
7083
- throw new HttpError(
7084
- response.status === 404 ? "The recall API answered 404: it is not available for this workspace yet." : `The recall API answered ${response.status}${detail ? `: ${detail}` : "."}`,
7085
- response.status,
7086
- body,
7087
- response.headers.get("x-request-id")
7088
- );
7089
- }
7090
- };
7091
-
7092
- // src/team-conflicts.ts
7093
- var TEAM_POLL_MS = 6e4;
7094
- var NAMES_KEPT_MS = 10 * 6e4;
7095
- function liveTeammate(row, now) {
7096
- const [shown] = psRows([row], { device: "", now, all: true });
7097
- return ["running", "waiting", "idle"].includes(shown?.shown_state ?? "");
7098
- }
7099
- function teamConflicts(mine, catalog, options) {
7100
- const found = [];
7101
- for (const row of catalog.rows) {
7102
- if (!row.author || row.author === catalog.me || !liveTeammate(row, options.now)) continue;
7103
- const theirs = /* @__PURE__ */ new Set();
7104
- for (const turn of row.turns)
7105
- if (options.now - turn.at_ms < CONFLICT_WINDOW_MS) {
7106
- for (const file of turn.paths_written) if (!ignoredPath(file)) theirs.add(file);
7107
- }
7108
- if (!theirs.size) continue;
7109
- for (const own of mine) {
7110
- const pair = `t ${own.session} ${row.session}`;
7111
- if (options.alerted(pair)) continue;
7112
- const files = Object.entries(own.paths).filter(([file, ms]) => options.now - ms < CONFLICT_WINDOW_MS && theirs.has(file)).map(([file]) => file).sort();
7113
- if (files.length)
7114
- found.push({
7115
- pair,
7116
- session: own.session,
7117
- teammate: row.session,
7118
- author: row.author,
7119
- files
7120
- });
7484
+ member2.display_name ?? member2.name ?? member2.subject
7485
+ ])
7486
+ )
7487
+ };
7488
+ names.set(accountId, entry);
7489
+ }
7490
+ return entry.names.get(sub) ?? null;
7121
7491
  }
7122
- }
7123
- return found;
7124
- }
7125
- function teamConflictMessage(conflict, name, now) {
7126
- const shown = conflict.files.slice(0, 3).join(", ") + (conflict.files.length > 3 ? `, +${conflict.files.length - 3}` : "");
7127
- const who = name ?? `teammate ${conflict.author.slice(0, 8)}`;
7128
- return {
7129
- id: `c${createHash6("sha256").update(conflict.pair).digest("hex").slice(0, 10)}`,
7130
- kind: "conflict",
7131
- at: new Date(now).toISOString(),
7132
- recipient: { kind: "session", id: conflict.session },
7133
- reason: "a teammate's live session wrote the same file",
7134
- source: { kind: "local", detail: conflict.teammate },
7135
- content: `${who}'s session ${conflict.teammate.slice(0, 8)} in this team Project also wrote ${shown} in the last 2 h`,
7136
- notify: true
7137
7492
  };
7138
7493
  }
7139
- function teamCatalogReader(options) {
7140
- const people = /* @__PURE__ */ new Map();
7141
- return async (accountId) => {
7142
- const file = await options.credentials.load();
7143
- const login = file ? accountLogins(file).find(
7144
- (entry) => entry.account_id === accountId && entry.kind !== "user" && !isExpiredLogin(entry)
7145
- ) : void 0;
7146
- if (!login) throw new Error(`no current login of organization ${accountId}`);
7147
- let known = people.get(accountId);
7148
- if (!known || known.key !== login.key || Date.now() - known.at > NAMES_KEPT_MS) {
7149
- const [account, members, store] = await Promise.all([
7150
- options.api.getAccount(login.key),
7151
- options.api.page(
7152
- "listMembers",
7153
- login.key,
7154
- { limit: 100 }
7155
- ),
7156
- getSessionsStore(options.fetch, options.apiOrigin, login.key, { team: true })
7157
- ]);
7158
- known = {
7159
- at: Date.now(),
7160
- key: login.key,
7161
- me: account.member.subject,
7162
- workspace: workspaceHex(store.workspace_id),
7163
- names: new Map(
7164
- members.data.map((member2) => [
7165
- member2.subject,
7166
- member2.display_name ?? member2.name ?? member2.subject
7167
- ])
7168
- )
7169
- };
7170
- people.set(accountId, known);
7171
- }
7172
- const recall = new RecallClient({
7173
- fetch: options.fetch,
7174
- tokens: options.gitTokens,
7175
- key: login.key,
7176
- origin: gitOrigin(options.apiOrigin),
7177
- workspace: known.workspace
7178
- });
7179
- return { me: known.me, names: known.names, rows: await recall.catalog(100, { team: true }) };
7180
- };
7494
+ function inboxLine(message3, status) {
7495
+ return `${status.padEnd(11)} ${message3.id} ${message3.at} ${message3.content} (${message3.reason})`;
7496
+ }
7497
+ async function ackHandoff(store, handoffId2) {
7498
+ try {
7499
+ const marks = await store.marks();
7500
+ const ids = (await store.messages()).filter(
7501
+ (message3) => message3.source.kind === "handoff" && message3.source.handoff_id === handoffId2 && unread(statusOf(marks, message3.id))
7502
+ ).map((message3) => message3.id);
7503
+ if (!ids.length) return;
7504
+ const at = (/* @__PURE__ */ new Date()).toISOString();
7505
+ for (const id of ids) marks.marks[id] = { ...marks.marks[id], status: "read", at };
7506
+ await store.saveMarks(marks);
7507
+ } catch {
7508
+ }
7181
7509
  }
7182
7510
 
7183
7511
  // src/conflicts.ts
@@ -7301,7 +7629,7 @@ function conflictMessages(state, conflict, now, home) {
7301
7629
  const theirs = state.sessions[other];
7302
7630
  const where = theirs.cwd ? ` in ${tilde(theirs.cwd, home)}` : "";
7303
7631
  return {
7304
- id: `c${createHash7("sha256").update(`${conflict.pair} ${me}`).digest("hex").slice(0, 10)}`,
7632
+ id: `c${createHash8("sha256").update(`${conflict.pair} ${me}`).digest("hex").slice(0, 10)}`,
7305
7633
  kind: "conflict",
7306
7634
  at: new Date(now).toISOString(),
7307
7635
  recipient: { kind: "session", id: me },
@@ -7338,6 +7666,8 @@ var ConflictWatch = class {
7338
7666
  capped = "";
7339
7667
  polled = /* @__PURE__ */ new Map();
7340
7668
  failing = /* @__PURE__ */ new Map();
7669
+ projects = /* @__PURE__ */ new Map();
7670
+ ownPolled = 0;
7341
7671
  /** A turn's writes, which its end clears from the session's state before
7342
7672
  * the next pass sees them. */
7343
7673
  turnEnded(writes) {
@@ -7387,7 +7717,10 @@ var ConflictWatch = class {
7387
7717
  }
7388
7718
  }
7389
7719
  const repository = await this.repository(write.cwd);
7390
- if (repository) remember(state, { ...write, repository });
7720
+ if (!repository) continue;
7721
+ remember(state, { ...write, repository });
7722
+ const project = place ? null : await this.project(write.cwd);
7723
+ if (project) state.sessions[write.session].project = project;
7391
7724
  }
7392
7725
  prune(state, now);
7393
7726
  const live = (session) => liveEntry(entries.get(session));
@@ -7402,10 +7735,23 @@ var ConflictWatch = class {
7402
7735
  now
7403
7736
  );
7404
7737
  await this.teammates(state, live, now);
7738
+ await this.devices(state, live, now);
7405
7739
  } catch (error) {
7406
7740
  log("warn", `conflicts: ${error instanceof Error ? error.message : String(error)}`);
7407
7741
  }
7408
7742
  }
7743
+ /** A personal cwd's Project, looked up again after TEAM_POLL_MS. */
7744
+ project(cwd) {
7745
+ const lookup = this.options.devices?.project;
7746
+ if (!lookup) return Promise.resolve(null);
7747
+ const now = this.options.now();
7748
+ let cached = this.projects.get(cwd);
7749
+ if (!cached || now - cached.at >= TEAM_POLL_MS) {
7750
+ cached = { at: now, found: lookup(cwd).catch(() => null) };
7751
+ this.projects.set(cwd, cached);
7752
+ }
7753
+ return cached.found;
7754
+ }
7409
7755
  /** How many of `found` pairs today's cap still lets alert. */
7410
7756
  room(state, found) {
7411
7757
  const room = Math.max(0, CONFLICT_DAILY_CAP - state.day.count);
@@ -7464,124 +7810,49 @@ var ConflictWatch = class {
7464
7810
  );
7465
7811
  }
7466
7812
  }
7467
- };
7468
-
7469
- // src/generated/machine.ts
7470
- var machineRegisterDefs = {
7471
- "byoc_arch": {
7472
- "description": "A `byoc` device's architecture; grows only by a contract change. The regional machine_byoc_arches() equals it.",
7473
- "enum": [
7474
- "arm64",
7475
- "amd64"
7476
- ]
7477
- },
7478
- "byoc_client": {
7479
- "description": "The client that registered a `byoc` device; grows only by a contract change. The regional machine_byoc_clients() equals it.",
7480
- "enum": [
7481
- "yield"
7482
- ]
7483
- },
7484
- "byoc_client_version": {
7485
- "description": "The client's version, a semantic version of at most 64 octets.",
7486
- "type": "string",
7487
- "maxLength": 64,
7488
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
7489
- },
7490
- "byoc_harness": {
7491
- "description": "An agent harness `yield` captures on a `byoc` device: a key of contracts/sessions/capture-v1.json `sources`, its one authority, which automation/tests/test_machines_machine_v2_contract.py holds this enum equal to. The regional machine_byoc_harnesses() equals it.",
7492
- "enum": [
7493
- "claude-code",
7494
- "codex"
7495
- ]
7496
- },
7497
- "byoc_harnesses": {
7498
- "description": "The harnesses installed on the device that its client captures, without duplicates.",
7499
- "type": "array",
7500
- "uniqueItems": true,
7501
- "items": {
7502
- "$ref": "#/$defs/byoc_harness"
7503
- }
7504
- },
7505
- "byoc_hostname": {
7506
- "description": "The device's hostname as the client reports it, at most 253 octets (the database counts octets). Personal data, answered to the device's owner only (machine-v2.json `byoc.visibility`).",
7507
- "type": "string",
7508
- "minLength": 1,
7509
- "maxLength": 253
7510
- },
7511
- "byoc_os": {
7512
- "description": "A `byoc` device's operating system family; grows only by a contract change. The regional machine_byoc_oses() equals it.",
7513
- "enum": [
7514
- "darwin",
7515
- "linux"
7516
- ]
7517
- },
7518
- "name": {
7519
- "description": "A Machine name: the Agent name grammar (contracts/agents/agent-name-v1.json `$defs.name`, the same pattern and lengths, which the contract test holds equal) without its reserved-name list, which a Machine label cannot collide with (machine-v2.json `names`); 1-40 characters, no `--` and no `.`. Unique within its account across both kinds (ADR-0015 Q5); `<name>-<account>` must also pass the label rule of machine-v2.json `hostnames` for a managed Machine. `self` is reserved, so `/v1/machines/self` never names a Machine; global machine_name_valid follows this definition verbatim.",
7520
- "type": "string",
7521
- "minLength": 1,
7522
- "maxLength": 40,
7523
- "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
7524
- "not": {
7525
- "const": "self"
7526
- }
7527
- },
7528
- "register": {
7529
- "description": "The body of `PUT /v1/machines/self` (machine-v2.json `actions` `register`, ADR-0015 `Registration`), signed by the CLI login key of the computer it registers. It has no `kind`: the server derives `byoc`, and a `kind` is refused as an unknown field. `name` is read only by the registration that creates the device; a device that exists keeps its name.",
7530
- "type": "object",
7531
- "additionalProperties": false,
7532
- "required": [
7533
- "device_id",
7534
- "hostname",
7535
- "os",
7536
- "arch",
7537
- "client",
7538
- "client_version",
7539
- "harnesses"
7540
- ],
7541
- "properties": {
7542
- "device_id": {
7543
- "$ref": "#/$defs/uuid"
7544
- },
7545
- "hostname": {
7546
- "$ref": "#/$defs/byoc_hostname"
7547
- },
7548
- "os": {
7549
- "$ref": "#/$defs/byoc_os"
7550
- },
7551
- "arch": {
7552
- "$ref": "#/$defs/byoc_arch"
7553
- },
7554
- "client": {
7555
- "$ref": "#/$defs/byoc_client"
7556
- },
7557
- "client_version": {
7558
- "$ref": "#/$defs/byoc_client_version"
7559
- },
7560
- "harnesses": {
7561
- "$ref": "#/$defs/byoc_harnesses"
7562
- },
7563
- "name": {
7564
- "$ref": "#/$defs/name"
7813
+ /** Compares my live personal sessions' writes here with my live sessions
7814
+ * of the same Project on my other devices, from my personal catalog, read
7815
+ * at most each TEAM_POLL_MS and only while one of them has writes in the
7816
+ * window. */
7817
+ async devices(state, live, now) {
7818
+ const own = this.options.devices;
7819
+ if (!own) return;
7820
+ const mine = [];
7821
+ for (const [session, entry] of Object.entries(state.sessions)) {
7822
+ if (!entry.project || !live(session)) continue;
7823
+ const paths = {};
7824
+ for (const [key, ms] of Object.entries(entry.paths)) {
7825
+ const file = key.slice(key.indexOf("\n") + 1);
7826
+ paths[file] = Math.max(paths[file] ?? 0, ms);
7565
7827
  }
7566
- }
7567
- },
7568
- "uuid": {
7569
- "description": "A lowercase canonical UUID.",
7570
- "type": "string",
7571
- "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
7828
+ mine.push({ session, project: entry.project, paths });
7829
+ }
7830
+ if (!mine.length || now - this.ownPolled < TEAM_POLL_MS) return;
7831
+ this.ownPolled = now;
7832
+ const catalog = await own.read().catch((error) => {
7833
+ const reason = error instanceof Error ? error.message : String(error);
7834
+ if (this.failing.get("") !== reason)
7835
+ this.options.log("warn", `conflicts across devices: ${reason}`);
7836
+ this.failing.set("", reason);
7837
+ return null;
7838
+ });
7839
+ if (!catalog) return;
7840
+ this.failing.delete("");
7841
+ const found = deviceConflicts(mine, catalog.rows, {
7842
+ device: own.device,
7843
+ now,
7844
+ alerted: (pair) => pair in state.pairs
7845
+ });
7846
+ await this.alert(
7847
+ state,
7848
+ found.slice(0, this.room(state, found.length)).map((conflict) => ({
7849
+ pair: conflict.pair,
7850
+ messages: [deviceConflictMessage(conflict, catalog.names, now, this.options.home)]
7851
+ })),
7852
+ now
7853
+ );
7572
7854
  }
7573
7855
  };
7574
- var byocPresence = {
7575
- "heartbeat": {
7576
- "interval_seconds": 300,
7577
- "jitter_seconds": 30
7578
- },
7579
- "online_window_seconds": 900
7580
- };
7581
- var byocRetryable = [
7582
- "device_registration_conflict",
7583
- "device_report_throttled"
7584
- ];
7585
7856
 
7586
7857
  // src/register.ts
7587
7858
  import { existsSync as existsSync2, readFileSync } from "node:fs";
@@ -7873,7 +8144,7 @@ function message(error) {
7873
8144
  }
7874
8145
 
7875
8146
  // src/snapshot.ts
7876
- import { createHash as createHash8 } from "node:crypto";
8147
+ import { createHash as createHash9 } from "node:crypto";
7877
8148
  import { lstat as lstat4, mkdir as mkdir6, realpath as realpath5 } from "node:fs/promises";
7878
8149
  import path19 from "node:path";
7879
8150
  var MAX_SNAPSHOT_FILE_BYTES = 100 * 1024 * 1024;
@@ -8135,7 +8406,7 @@ var Snapshots = class {
8135
8406
  const index = path19.join(
8136
8407
  this.deps.directory,
8137
8408
  "index",
8138
- createHash8("sha256").update(folder).digest("hex").slice(0, 32)
8409
+ createHash9("sha256").update(folder).digest("hex").slice(0, 32)
8139
8410
  );
8140
8411
  await mkdir6(path19.dirname(index), { recursive: true, mode: 448 });
8141
8412
  const env = { GIT_INDEX_FILE: index };
@@ -8595,7 +8866,18 @@ async function runDaemon(options, signal) {
8595
8866
  fetch: options.fetch ?? fetch,
8596
8867
  apiOrigin: options.apiOrigin,
8597
8868
  gitTokens: options.gitTokens
8598
- }) : void 0
8869
+ }) : void 0,
8870
+ devices: options.apiOrigin && router ? {
8871
+ device,
8872
+ project: async (cwd) => (await router.project(cwd))?.project_id ?? null,
8873
+ read: ownCatalogReader({
8874
+ key: async () => (await captureLogin(options.credentials).catch(() => null))?.key ?? null,
8875
+ api: new ManagementApi(options.fetch ?? fetch, options.apiOrigin),
8876
+ fetch: options.fetch ?? fetch,
8877
+ apiOrigin: options.apiOrigin,
8878
+ gitTokens: options.gitTokens
8879
+ })
8880
+ } : void 0
8599
8881
  }) : void 0;
8600
8882
  const registration = registerOnHeartbeat(options, device, signal);
8601
8883
  const inbox = options.inbox && options.apiOrigin ? pollInbox(
@@ -9136,7 +9418,7 @@ async function acquireLock(filename) {
9136
9418
 
9137
9419
  // src/device.ts
9138
9420
  import { execFile as execFile6 } from "node:child_process";
9139
- import { createHash as createHash9, randomUUID as randomUUID9 } from "node:crypto";
9421
+ import { createHash as createHash10, randomUUID as randomUUID9 } from "node:crypto";
9140
9422
  import { link, mkdir as mkdir8, open as open10, readFile as readFile12, rename as rename8, rm as rm9 } from "node:fs/promises";
9141
9423
  import path23 from "node:path";
9142
9424
  import { promisify as promisify6 } from "node:util";
@@ -9159,7 +9441,7 @@ async function readMachineId(platform = process.platform) {
9159
9441
  return null;
9160
9442
  }
9161
9443
  function machineIdHash(machineId) {
9162
- return createHash9("sha256").update(machineId).digest("hex");
9444
+ return createHash10("sha256").update(machineId).digest("hex");
9163
9445
  }
9164
9446
  var DeviceStore = class {
9165
9447
  constructor(filename = path23.join(stateDirectory(), "device.json"), machineId = readMachineId) {
@@ -9712,44 +9994,6 @@ async function directHandoff(deps, target, options = {}) {
9712
9994
  }
9713
9995
  }
9714
9996
 
9715
- // src/devices.ts
9716
- async function listDevices(list2) {
9717
- const devices = [];
9718
- const unavailable = /* @__PURE__ */ new Set();
9719
- let cursor;
9720
- for (; ; ) {
9721
- const page = await list2(cursor ? { cursor } : {});
9722
- devices.push(
9723
- ...page.data.filter((machine) => machine.kind === "byoc")
9724
- );
9725
- for (const region of page.unavailable_regions ?? []) unavailable.add(region);
9726
- if (!page.next_cursor || page.next_cursor === cursor) break;
9727
- cursor = page.next_cursor;
9728
- }
9729
- return { devices, unavailableRegions: [...unavailable] };
9730
- }
9731
- var DISPATCHABLE = /* @__PURE__ */ new Set(["claude-code", "codex"]);
9732
- function isOnline(lastSeen, now) {
9733
- const seen = Date.parse(lastSeen);
9734
- return Number.isFinite(seen) && now - seen <= byocPresence.online_window_seconds * 1e3;
9735
- }
9736
- function deviceRows(devices, device, now) {
9737
- return devices.map((machine) => ({
9738
- dispatch_target: machine.byoc.device_id !== device && machine.byoc.key_state === "active" && isOnline(machine.byoc.last_seen, now) && machine.byoc.harnesses.some((harness) => DISPATCHABLE.has(harness)),
9739
- name: machine.name,
9740
- os: machine.byoc.os,
9741
- arch: machine.byoc.arch,
9742
- harnesses: machine.byoc.harnesses,
9743
- client_version: machine.byoc.client_version,
9744
- last_seen: machine.byoc.last_seen,
9745
- online: isOnline(machine.byoc.last_seen, now),
9746
- key_state: machine.byoc.key_state,
9747
- this_device: machine.byoc.device_id === device,
9748
- device_id: machine.byoc.device_id ?? null,
9749
- hostname: machine.byoc.hostname ?? null
9750
- })).sort((a, b) => Number(b.this_device) - Number(a.this_device) || a.name.localeCompare(b.name));
9751
- }
9752
-
9753
9997
  // src/dispatch.ts
9754
9998
  import { spawn } from "node:child_process";
9755
9999
  import { realpathSync } from "node:fs";
@@ -10720,7 +10964,7 @@ async function thisMachine(options, key) {
10720
10964
  }
10721
10965
 
10722
10966
  // src/pull.ts
10723
- import { createHash as createHash10, randomUUID as randomUUID12 } from "node:crypto";
10967
+ import { createHash as createHash11, randomUUID as randomUUID12 } from "node:crypto";
10724
10968
  import { open as open13, readFile as readFile16 } from "node:fs/promises";
10725
10969
  import path28 from "node:path";
10726
10970
  var SHA15 = /^[0-9a-f]{40}$/;
@@ -10759,7 +11003,7 @@ async function pullHandoff(deps, link3, cwd = process.cwd(), asPatch = false) {
10759
11003
  if (marker2.patch_file !== path28.basename(patchFile()) || !/^[0-9a-f]{64}$/.test(marker2.patch_sha256 ?? ""))
10760
11004
  throw new Error("The local patch marker is corrupt; no release was acknowledged.");
10761
11005
  const content = await readFile16(patchFile());
10762
- if (createHash10("sha256").update(content).digest("hex") !== marker2.patch_sha256)
11006
+ if (createHash11("sha256").update(content).digest("hex") !== marker2.patch_sha256)
10763
11007
  throw new Error("The pulled patch changed; no release was acknowledged.");
10764
11008
  } else {
10765
11009
  if (marker2.branch !== branch())
@@ -10867,7 +11111,7 @@ async function pullHandoff(deps, link3, cwd = process.cwd(), asPatch = false) {
10867
11111
  handoff_id: id,
10868
11112
  commit: fetched,
10869
11113
  patch_file: path28.basename(patchFile()),
10870
- patch_sha256: createHash10("sha256").update(await readFile16(patchFile())).digest("hex")
11114
+ patch_sha256: createHash11("sha256").update(await readFile16(patchFile())).digest("hex")
10871
11115
  } : { handoff_id: id, commit: fetched, branch: branch() };
10872
11116
  const file = await open13(markerFile(), "wx", 384);
10873
11117
  try {
@@ -10903,7 +11147,7 @@ async function pullHandoff(deps, link3, cwd = process.cwd(), asPatch = false) {
10903
11147
  }
10904
11148
 
10905
11149
  // src/give.ts
10906
- import { createHash as createHash11, randomUUID as randomUUID13 } from "node:crypto";
11150
+ import { createHash as createHash12, randomUUID as randomUUID13 } from "node:crypto";
10907
11151
  import { mkdir as mkdir11, open as open14, readFile as readFile17 } from "node:fs/promises";
10908
11152
  import path29 from "node:path";
10909
11153
  var UUID11 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
@@ -10990,7 +11234,7 @@ async function giveHandoff(deps, target, session, task, assignment = OPEN) {
10990
11234
  target.folder,
10991
11235
  pushEnv
10992
11236
  );
10993
- const requestHash = createHash11("sha256").update(
11237
+ const requestHash = createHash12("sha256").update(
10994
11238
  JSON.stringify(
10995
11239
  assignment.kind === "open" ? { task, target_kind: "open" } : {
10996
11240
  task,
@@ -11510,7 +11754,7 @@ function handoffLines(view, names = /* @__PURE__ */ new Map()) {
11510
11754
  }
11511
11755
 
11512
11756
  // src/skills.ts
11513
- import { createHash as createHash12 } from "node:crypto";
11757
+ import { createHash as createHash13 } from "node:crypto";
11514
11758
  import { mkdir as mkdir13, readFile as readFile18, rm as rm13, rmdir, writeFile as writeFile5 } from "node:fs/promises";
11515
11759
  import os10 from "node:os";
11516
11760
  import path31 from "node:path";
@@ -11520,7 +11764,7 @@ var tonboSkill = "---\nname: tonbo\ndescription: Recall the captured agent sessi
11520
11764
 
11521
11765
  // src/skills.ts
11522
11766
  var SKILL_NAME = "tonbo";
11523
- var sha256 = (value) => createHash12("sha256").update(value).digest("hex");
11767
+ var sha256 = (value) => createHash13("sha256").update(value).digest("hex");
11524
11768
  function skillPaths(home = os10.homedir(), env = process.env) {
11525
11769
  const claude = env.CLAUDE_CONFIG_DIR || path31.join(home, ".claude");
11526
11770
  const codex = env.CODEX_HOME || path31.join(home, ".codex");
@@ -12374,19 +12618,27 @@ function createProgram(dependencies = createDependencies) {
12374
12618
  program.command("login").description("sign in through the browser; shared with the tonbo CLI").option("--account <name>", "the account to log in to; defaults to your personal account").option(
12375
12619
  "--name <name>",
12376
12620
  "the name this computer registers under the first time; <os>-<n> by default"
12377
- ).option("--no-daemon", "only sign in; do not install and start capture on this computer").action(async (options) => {
12378
- const deps = dependencies();
12379
- const login = await loginCommand(deps, { account: options.account });
12380
- if (login.kind === "organization") return;
12381
- await registerThisComputer(deps, options.name);
12382
- if (!options.daemon) return;
12383
- try {
12384
- deps.output((await installCapture()).join("\n"));
12385
- deps.output("Capture is on. `yield status` checks it; `yield ps` lists your sessions.");
12386
- } catch (error) {
12387
- deps.output(error instanceof Error ? error.message : String(error));
12621
+ ).option("--no-daemon", "only sign in; do not install and start capture on this computer").option(
12622
+ "--device",
12623
+ "approve with a code in a browser on any computer, as over SSH without a display"
12624
+ ).action(
12625
+ async (options) => {
12626
+ const deps = dependencies();
12627
+ const login = await loginCommand(deps, {
12628
+ account: options.account,
12629
+ device: options.device
12630
+ });
12631
+ if (login.kind === "organization") return;
12632
+ await registerThisComputer(deps, options.name);
12633
+ if (!options.daemon) return;
12634
+ try {
12635
+ deps.output((await installCapture()).join("\n"));
12636
+ deps.output("Capture is on. `yield status` checks it; `yield ps` lists your sessions.");
12637
+ } catch (error) {
12638
+ deps.output(error instanceof Error ? error.message : String(error));
12639
+ }
12388
12640
  }
12389
- });
12641
+ );
12390
12642
  program.command("give").description("push the latest captured snapshot of a session and create a handoff").argument("<session>", "full captured session id").requiredOption("--task <text>", "what the next person or harness should do").option(
12391
12643
  "--to <who>",
12392
12644
  "a teammate in a team Project (name or subject), claude-code or codex for yourself, or <device>:<harness> for another of your computers"