borgmcp-server 0.1.5 → 0.1.8

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/src/store.ts CHANGED
@@ -65,7 +65,6 @@ export type ActivityNotificationRecord = EnrichedActivityRecord & (
65
65
  readonly claimant_drone_id?: string;
66
66
  readonly claimant_role?: string | null;
67
67
  }
68
- | { readonly kind: "heartbeat_ping" }
69
68
  );
70
69
 
71
70
  export type ActivityStreamRecord = EnrichedActivityRecord | ActivityNotificationRecord;
@@ -180,11 +179,7 @@ export interface StoreRuntime {
180
179
  }
181
180
 
182
181
  export interface LivenessStore {
183
- readonly scan: (options?: {
184
- readonly silentMs?: number;
185
- readonly cooldownMs?: number;
186
- readonly limit?: number;
187
- }) => ActivityNotificationRecord[];
182
+ readonly scan: () => ActivityNotificationRecord[];
188
183
  }
189
184
 
190
185
  export interface DigestPair {
@@ -237,6 +232,7 @@ export interface StoredDroneSessionDigest extends StoredSecretDigest {
237
232
  readonly droneId: string;
238
233
  readonly expiresAt: string;
239
234
  readonly evictedAt: string | null;
235
+ readonly takenOver: boolean;
240
236
  }
241
237
 
242
238
  export interface CredentialStore {
@@ -289,6 +285,8 @@ export interface ScopedStore {
289
285
  readonly drones: DroneRecord[];
290
286
  readonly since: string;
291
287
  };
288
+ readonly reassignDrone: (cubeId: string, droneId: string, roleId: string) => DroneRecord;
289
+ readonly evictDrone: (cubeId: string, droneId: string) => void;
292
290
  readonly appendLog: (cubeId: string, input: {
293
291
  readonly message: string;
294
292
  readonly visibility?: "broadcast" | "direct";
@@ -360,6 +358,7 @@ export interface SeatAttachInput {
360
358
  readonly credentialId: string;
361
359
  readonly credentialDigest: DigestPair;
362
360
  readonly expiresAt: string;
361
+ readonly renewIfExpiresAtOrBefore: string;
363
362
  }
364
363
 
365
364
  export interface SeatAttachRecord {
@@ -441,6 +440,24 @@ export interface MaintenanceStore {
441
440
  }) => void;
442
441
  readonly revokeClient: (clientId: string) => void;
443
442
  readonly revokeDroneSession: (sessionId: string) => void;
443
+ readonly expireDroneSession: (sessionId: string) => void;
444
+ readonly inspectManagedDrone: (droneId: string) => {
445
+ readonly role_id: string;
446
+ readonly evicted: boolean;
447
+ readonly session_revoked: boolean;
448
+ };
449
+ readonly inspectCubeManagementState: (cubeId: string) => {
450
+ readonly directive: string;
451
+ readonly taxonomy_marker: string | null;
452
+ readonly role_ids: readonly string[];
453
+ readonly active_decision_ids: readonly string[];
454
+ readonly drones: ReadonlyArray<{
455
+ readonly id: string;
456
+ readonly role_id: string;
457
+ readonly evicted: boolean;
458
+ readonly session_revoked: boolean;
459
+ }>;
460
+ };
444
461
  readonly expireActivityCursor: (cubeId: string, cursor: LogCursor) => void;
445
462
  }
446
463
 
@@ -480,6 +497,15 @@ export class RoleSectionConflictError extends Error {
480
497
  }
481
498
  }
482
499
 
500
+ export class RoleInUseError extends Error {
501
+ readonly code = "ROLE_IN_USE";
502
+
503
+ constructor() {
504
+ super("The human-seat role is already occupied.");
505
+ this.name = "RoleInUseError";
506
+ }
507
+ }
508
+
483
509
  export class CursorExpiredError extends Error {
484
510
  readonly code = "CURSOR_EXPIRED";
485
511
 
@@ -498,6 +524,33 @@ export class AttachSessionRejectedError extends Error {
498
524
  }
499
525
  }
500
526
 
527
+ export class AttachSessionExpiredError extends Error {
528
+ readonly code = "AUTH_EXPIRED";
529
+
530
+ constructor() {
531
+ super("Authentication failed.");
532
+ this.name = "AttachSessionExpiredError";
533
+ }
534
+ }
535
+
536
+ export class AttachSessionRevokedError extends Error {
537
+ readonly code = "SESSION_REVOKED";
538
+
539
+ constructor() {
540
+ super("Authentication failed.");
541
+ this.name = "AttachSessionRevokedError";
542
+ }
543
+ }
544
+
545
+ export class AttachDroneEvictedError extends Error {
546
+ readonly code = "DRONE_EVICTED";
547
+
548
+ constructor() {
549
+ super("Authentication failed.");
550
+ this.name = "AttachDroneEvictedError";
551
+ }
552
+ }
553
+
501
554
  export class StorageCapacityError extends Error {
502
555
  readonly code = "CAPACITY_EXCEEDED";
503
556
 
@@ -616,7 +669,7 @@ export async function openStore(options: OpenStoreOptions): Promise<StoreRuntime
616
669
  );
617
670
  const activityHub = new ActivityHub();
618
671
  const maintenance = new SqliteMaintenanceStore(database, clock);
619
- const liveness = new SqliteLivenessStore(database, clock, activityHub, capacityGuard);
672
+ const liveness = new IdleLivenessStore();
620
673
  const credentials = new SqliteCredentialStore(database, clock, capacityGuard, options.mutationHook);
621
674
  return Object.freeze({
622
675
  forPrincipal: (principal: Principal) => {
@@ -1125,6 +1178,85 @@ class SqliteScopedStore implements ScopedStore {
1125
1178
  };
1126
1179
  }
1127
1180
 
1181
+ reassignDrone(cubeId: string, droneId: string, roleId: string): DroneRecord {
1182
+ assertCanonicalUuid(cubeId, "Cube id");
1183
+ assertCanonicalUuid(droneId, "Drone id");
1184
+ assertCanonicalUuid(roleId, "Role id");
1185
+ this.#requireCube(cubeId, "manage");
1186
+ this.#database.exec("BEGIN IMMEDIATE");
1187
+ try {
1188
+ this.#requireCube(cubeId, "manage");
1189
+ const droneRow = this.#database.prepare(`
1190
+ SELECT id, cube_id, role_id, label FROM drones
1191
+ WHERE id = ? AND cube_id = ? AND evicted_at IS NULL
1192
+ `).get(droneId, cubeId);
1193
+ const targetRoleRow = this.#database.prepare(`
1194
+ SELECT id, is_human_seat, role_class FROM roles WHERE id = ? AND cube_id = ?
1195
+ `).get(roleId, cubeId);
1196
+ if (droneRow === undefined || targetRoleRow === undefined) throw new ScopedStoreError();
1197
+ const sourceRoleRow = this.#database.prepare(`
1198
+ SELECT is_human_seat FROM roles WHERE id = ? AND cube_id = ?
1199
+ `).get(requiredText(droneRow, "role_id"), cubeId);
1200
+ if (sourceRoleRow === undefined) throw new ScopedStoreError();
1201
+ if (requiredText(targetRoleRow, "role_class") === "queen" &&
1202
+ requiredInteger(sourceRoleRow, "is_human_seat") !== 1) {
1203
+ throw new AccessDeniedError();
1204
+ }
1205
+ if (requiredInteger(targetRoleRow, "is_human_seat") === 1) {
1206
+ const occupied = this.#database.prepare(`
1207
+ SELECT 1 AS present FROM drones
1208
+ WHERE cube_id = ? AND role_id = ? AND id <> ? AND evicted_at IS NULL
1209
+ `).get(cubeId, roleId, droneId);
1210
+ if (occupied !== undefined) throw new RoleInUseError();
1211
+ }
1212
+ this.#database.prepare("UPDATE drones SET role_id = ? WHERE id = ? AND cube_id = ?")
1213
+ .run(roleId, droneId, cubeId);
1214
+ const row = this.#database.prepare(`
1215
+ SELECT drone.id, drone.cube_id, drone.role_id, drone.label,
1216
+ COALESCE(drone.last_seen, drone.created_at) AS last_seen,
1217
+ drone.hostname, drone.created_at,
1218
+ CASE WHEN client.revoked_at IS NULL AND grant_row.access IN ('write', 'manage')
1219
+ THEN 'participant' ELSE 'observer' END AS posture
1220
+ FROM drones AS drone
1221
+ LEFT JOIN clients AS client ON client.id = drone.client_id
1222
+ LEFT JOIN client_cube_grants AS grant_row
1223
+ ON grant_row.client_id = drone.client_id AND grant_row.cube_id = drone.cube_id
1224
+ WHERE drone.id = ? AND drone.cube_id = ? AND drone.evicted_at IS NULL
1225
+ `).get(droneId, cubeId);
1226
+ if (row === undefined) throw new ScopedStoreError();
1227
+ this.#database.exec("COMMIT");
1228
+ return droneRecord(row);
1229
+ } catch (error) {
1230
+ try { this.#database.exec("ROLLBACK"); } catch { /* Preserve the original failure. */ }
1231
+ throw error;
1232
+ }
1233
+ }
1234
+
1235
+ evictDrone(cubeId: string, droneId: string): void {
1236
+ assertCanonicalUuid(cubeId, "Cube id");
1237
+ assertCanonicalUuid(droneId, "Drone id");
1238
+ this.#requireCube(cubeId, "manage");
1239
+ this.#database.exec("BEGIN IMMEDIATE");
1240
+ try {
1241
+ this.#requireCube(cubeId, "manage");
1242
+ const active = this.#database.prepare(`
1243
+ SELECT 1 AS present FROM drones WHERE id = ? AND cube_id = ? AND evicted_at IS NULL
1244
+ `).get(droneId, cubeId);
1245
+ if (active === undefined) throw new ScopedStoreError();
1246
+ const now = this.#now();
1247
+ this.#database.prepare("UPDATE drones SET evicted_at = ? WHERE id = ? AND cube_id = ?")
1248
+ .run(now, droneId, cubeId);
1249
+ this.#database.prepare(`
1250
+ UPDATE drone_sessions SET revoked_at = ?
1251
+ WHERE drone_id = ? AND cube_id = ? AND revoked_at IS NULL
1252
+ `).run(now, droneId, cubeId);
1253
+ this.#database.exec("COMMIT");
1254
+ } catch (error) {
1255
+ try { this.#database.exec("ROLLBACK"); } catch { /* Preserve the original failure. */ }
1256
+ throw error;
1257
+ }
1258
+ }
1259
+
1128
1260
  appendLog(cubeId: string, input: {
1129
1261
  readonly message: string;
1130
1262
  readonly visibility?: "broadcast" | "direct";
@@ -1175,7 +1307,6 @@ class SqliteScopedStore implements ScopedStore {
1175
1307
  this.#database.prepare(`
1176
1308
  UPDATE drones SET last_seen = ? WHERE id = ? AND cube_id = ? AND evicted_at IS NULL
1177
1309
  `).run(createdAt, droneId, cubeId);
1178
- this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
1179
1310
  }
1180
1311
  if (recipients.length > 0) {
1181
1312
  const valid = this.#database.prepare(`
@@ -1362,6 +1493,7 @@ class SqliteScopedStore implements ScopedStore {
1362
1493
  assertCanonicalUuid(input.credentialId, "Drone session credential id");
1363
1494
  validateDigest(input.credentialDigest);
1364
1495
  validateTimestamp(input.expiresAt);
1496
+ validateTimestamp(input.renewIfExpiresAtOrBefore);
1365
1497
  const scope = this.#scope("read");
1366
1498
  const authorizedRole = this.#database.prepare(`
1367
1499
  SELECT 1 FROM cubes AS c JOIN roles AS role ON role.cube_id = c.id
@@ -1384,7 +1516,7 @@ class SqliteScopedStore implements ScopedStore {
1384
1516
  SELECT credential.verifier_digest, credential.revoked_at AS credential_revoked_at,
1385
1517
  session.id AS session_id, session.client_id, session.cube_id, session.drone_id,
1386
1518
  session.expires_at, session.revoked_at AS session_revoked_at,
1387
- drone.role_id, drone.label, drone.evicted_at
1519
+ session.superseded_at, drone.role_id, drone.label, drone.evicted_at
1388
1520
  FROM drone_session_credentials AS credential
1389
1521
  JOIN drone_sessions AS session ON session.id = credential.session_id
1390
1522
  JOIN drones AS drone ON drone.id = session.drone_id
@@ -1394,10 +1526,6 @@ class SqliteScopedStore implements ScopedStore {
1394
1526
  if (bound !== undefined) {
1395
1527
  const verifier = requiredBuffer(bound, "verifier_digest");
1396
1528
  if (!timingSafeEqual(verifier, input.credentialDigest.verifier) ||
1397
- optionalText(bound, "credential_revoked_at") !== null ||
1398
- optionalText(bound, "session_revoked_at") !== null ||
1399
- optionalText(bound, "evicted_at") !== null ||
1400
- requiredText(bound, "expires_at") <= now ||
1401
1529
  requiredText(bound, "client_id") !== this.#principal.id ||
1402
1530
  requiredText(bound, "cube_id") !== input.cubeId ||
1403
1531
  requiredText(bound, "role_id") !== input.roleId ||
@@ -1405,9 +1533,27 @@ class SqliteScopedStore implements ScopedStore {
1405
1533
  requiredText(bound, "drone_id") !== input.priorDroneId)) {
1406
1534
  throw new AttachSessionRejectedError();
1407
1535
  }
1536
+ if (optionalText(bound, "evicted_at") !== null) throw new AttachDroneEvictedError();
1537
+ if (optionalText(bound, "credential_revoked_at") !== null ||
1538
+ optionalText(bound, "session_revoked_at") !== null) {
1539
+ throw new AttachSessionRevokedError();
1540
+ }
1541
+ if (optionalText(bound, "superseded_at") !== null) {
1542
+ throw new AttachSessionRejectedError();
1543
+ }
1544
+ if (requiredText(bound, "expires_at") <= now) throw new AttachSessionExpiredError();
1408
1545
  const droneId = requiredText(bound, "drone_id");
1546
+ const sessionId = requiredText(bound, "session_id");
1547
+ const previousExpiry = requiredText(bound, "expires_at");
1548
+ const committedExpiry = input.expiresAt > previousExpiry &&
1549
+ previousExpiry <= input.renewIfExpiresAtOrBefore
1550
+ ? input.expiresAt
1551
+ : previousExpiry;
1552
+ if (committedExpiry !== previousExpiry) {
1553
+ this.#database.prepare("UPDATE drone_sessions SET expires_at = ? WHERE id = ?")
1554
+ .run(committedExpiry, sessionId);
1555
+ }
1409
1556
  this.#database.prepare("UPDATE drones SET last_seen = ? WHERE id = ?").run(now, droneId);
1410
- this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
1411
1557
  const attachedRoleRow = this.#database.prepare(`
1412
1558
  SELECT id AS role_id, name AS role_name, role_class, is_human_seat
1413
1559
  FROM roles WHERE id = ? AND cube_id = ?
@@ -1430,33 +1576,53 @@ class SqliteScopedStore implements ScopedStore {
1430
1576
  is_human_seat: requiredInteger(attachedRoleRow, "is_human_seat") === 1,
1431
1577
  },
1432
1578
  drone: { id: droneId, label: requiredText(bound, "label") },
1433
- sessionId: requiredText(bound, "session_id"),
1434
- expiresAt: requiredText(bound, "expires_at"),
1579
+ sessionId,
1580
+ expiresAt: committedExpiry,
1435
1581
  result: "reused",
1436
1582
  };
1437
1583
  }
1438
1584
 
1439
1585
  const priorSeat = input.priorDroneId === undefined ? undefined : this.#database.prepare(`
1440
- SELECT id, role_id, label FROM drones
1441
- WHERE id = ? AND client_id = ? AND cube_id = ? AND evicted_at IS NULL
1586
+ SELECT id, role_id, label, evicted_at FROM drones
1587
+ WHERE id = ? AND client_id = ? AND cube_id = ?
1442
1588
  `).get(input.priorDroneId, this.#principal.id, input.cubeId);
1443
1589
  let droneId: string;
1444
1590
  let droneLabel: string;
1445
- if (priorSeat !== undefined) {
1591
+ if (input.priorDroneId !== undefined) {
1592
+ if (priorSeat === undefined) throw new AttachSessionRejectedError();
1593
+ if (optionalText(priorSeat, "evicted_at") !== null) throw new AttachDroneEvictedError();
1446
1594
  const priorSeatId = requiredText(priorSeat, "id");
1447
1595
  if (requiredText(priorSeat, "role_id") !== input.roleId) {
1448
1596
  throw new AttachSessionRejectedError();
1449
1597
  }
1450
- const occupied = this.#database.prepare(`
1451
- SELECT 1 FROM drone_sessions AS session
1598
+ const latestSession = this.#database.prepare(`
1599
+ SELECT session.id, session.expires_at, session.revoked_at AS session_revoked_at,
1600
+ MAX(CASE WHEN credential.revoked_at IS NOT NULL THEN 1 ELSE 0 END)
1601
+ AS credential_revoked
1602
+ FROM drone_sessions AS session
1452
1603
  JOIN drone_session_credentials AS credential ON credential.session_id = session.id
1453
1604
  WHERE session.drone_id = ? AND session.client_id = ? AND session.cube_id = ?
1454
- AND session.revoked_at IS NULL AND credential.revoked_at IS NULL
1455
- AND session.expires_at > ?
1456
- `).get(priorSeatId, this.#principal.id, input.cubeId, now);
1457
- if (occupied !== undefined) throw new AttachSessionRejectedError();
1605
+ AND session.superseded_at IS NULL
1606
+ GROUP BY session.id
1607
+ LIMIT 1
1608
+ `).get(priorSeatId, this.#principal.id, input.cubeId);
1609
+ if (latestSession === undefined) throw new AttachSessionRejectedError();
1610
+ if (optionalText(latestSession, "session_revoked_at") !== null ||
1611
+ requiredInteger(latestSession, "credential_revoked") === 1) {
1612
+ throw new AttachSessionRevokedError();
1613
+ }
1614
+ if (requiredText(latestSession, "expires_at") > now) {
1615
+ throw new AttachSessionRejectedError();
1616
+ }
1458
1617
  droneId = priorSeatId;
1459
1618
  droneLabel = requiredText(priorSeat, "label");
1619
+ const superseded = this.#database.prepare(`
1620
+ UPDATE drone_sessions SET superseded_at = ?
1621
+ WHERE id = ? AND superseded_at IS NULL
1622
+ `).run(now, requiredText(latestSession, "id"));
1623
+ if (Number(superseded.changes) !== 1) {
1624
+ throw new Error("Expired session supersession was not atomic.");
1625
+ }
1460
1626
  this.#database.prepare("UPDATE drones SET last_seen = ? WHERE id = ?").run(now, droneId);
1461
1627
  } else {
1462
1628
  droneId = input.droneId;
@@ -1468,8 +1634,6 @@ class SqliteScopedStore implements ScopedStore {
1468
1634
  droneId, input.cubeId, input.roleId, this.#principal.id, droneLabel, now, now,
1469
1635
  );
1470
1636
  }
1471
- this.#database.prepare("DELETE FROM silent_seat_ping_state WHERE drone_id = ?").run(droneId);
1472
-
1473
1637
  this.#database.prepare(`
1474
1638
  INSERT INTO drone_sessions (
1475
1639
  id, client_id, cube_id, drone_id, created_at, expires_at
@@ -1990,6 +2154,59 @@ class SqliteMaintenanceStore implements MaintenanceStore {
1990
2154
  ).run(this.#now(), sessionId);
1991
2155
  }
1992
2156
 
2157
+ expireDroneSession(sessionId: string): void {
2158
+ assertCanonicalUuid(sessionId, "Drone session id");
2159
+ this.#database.prepare("UPDATE drone_sessions SET expires_at = ? WHERE id = ?")
2160
+ .run(new Date(0).toISOString(), sessionId);
2161
+ }
2162
+
2163
+ inspectManagedDrone(droneId: string) {
2164
+ assertCanonicalUuid(droneId, "Drone id");
2165
+ const row = this.#database.prepare(`
2166
+ SELECT drone.role_id, drone.evicted_at,
2167
+ EXISTS(SELECT 1 FROM drone_sessions AS session
2168
+ WHERE session.drone_id = drone.id AND session.revoked_at IS NOT NULL) AS session_revoked
2169
+ FROM drones AS drone WHERE drone.id = ?
2170
+ `).get(droneId);
2171
+ if (row === undefined) throw new ScopedStoreError();
2172
+ return {
2173
+ role_id: requiredText(row, "role_id"),
2174
+ evicted: nullableText(row, "evicted_at") !== null,
2175
+ session_revoked: requiredInteger(row, "session_revoked") === 1,
2176
+ };
2177
+ }
2178
+
2179
+ inspectCubeManagementState(cubeId: string) {
2180
+ assertCanonicalUuid(cubeId, "Cube id");
2181
+ const cube = this.#database.prepare(
2182
+ "SELECT directive, message_taxonomy FROM cubes WHERE id = ?",
2183
+ ).get(cubeId);
2184
+ if (cube === undefined) throw new ScopedStoreError();
2185
+ const taxonomy = nullableText(cube, "message_taxonomy");
2186
+ const parsed = taxonomy === null ? null : JSON.parse(taxonomy) as Array<{ class?: unknown }>;
2187
+ return {
2188
+ directive: requiredText(cube, "directive"),
2189
+ taxonomy_marker: typeof parsed?.[0]?.class === "string" ? parsed[0].class : null,
2190
+ role_ids: this.#database.prepare(
2191
+ "SELECT id FROM roles WHERE cube_id = ? ORDER BY id",
2192
+ ).all(cubeId).map((row) => requiredText(row, "id")),
2193
+ active_decision_ids: this.#database.prepare(`
2194
+ SELECT id FROM decisions WHERE cube_id = ? AND status = 'active' ORDER BY id
2195
+ `).all(cubeId).map((row) => requiredText(row, "id")),
2196
+ drones: this.#database.prepare(`
2197
+ SELECT drone.id, drone.role_id, drone.evicted_at,
2198
+ EXISTS(SELECT 1 FROM drone_sessions AS session
2199
+ WHERE session.drone_id = drone.id AND session.revoked_at IS NOT NULL) AS session_revoked
2200
+ FROM drones AS drone WHERE drone.cube_id = ? ORDER BY drone.id
2201
+ `).all(cubeId).map((row) => ({
2202
+ id: requiredText(row, "id"),
2203
+ role_id: requiredText(row, "role_id"),
2204
+ evicted: nullableText(row, "evicted_at") !== null,
2205
+ session_revoked: requiredInteger(row, "session_revoked") === 1,
2206
+ })),
2207
+ };
2208
+ }
2209
+
1993
2210
  expireActivityCursor(cubeId: string, cursor: LogCursor): void {
1994
2211
  assertCanonicalUuid(cubeId, "Cube id");
1995
2212
  assertCanonicalUuid(cursor.id, "Activity cursor id");
@@ -2033,61 +2250,11 @@ class ActivityHub {
2033
2250
  }
2034
2251
  }
2035
2252
 
2036
- class SqliteLivenessStore implements LivenessStore {
2037
- constructor(
2038
- readonly database: DatabaseSync,
2039
- readonly clock: () => Date,
2040
- readonly hub: ActivityHub,
2041
- readonly capacityGuard: StorageCapacityGuard,
2042
- ) {}
2043
-
2044
- scan(options: { silentMs?: number; cooldownMs?: number; limit?: number } = {}): ActivityNotificationRecord[] {
2045
- const now = this.clock();
2046
- const silentBefore = new Date(now.getTime() - (options.silentMs ?? 600_000)).toISOString();
2047
- const cooldownBefore = new Date(now.getTime() - (options.cooldownMs ?? 600_000)).toISOString();
2048
- const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 25), 25));
2049
- const candidates = this.database.prepare(`
2050
- SELECT drone.id AS drone_id, drone.cube_id, COALESCE(state.attempts, 0) AS attempts
2051
- FROM drones AS drone
2052
- JOIN clients AS client ON client.id = drone.client_id AND client.revoked_at IS NULL
2053
- JOIN client_cube_grants AS grant_row ON grant_row.client_id = drone.client_id
2054
- AND grant_row.cube_id = drone.cube_id AND grant_row.access IN ('write', 'manage')
2055
- LEFT JOIN silent_seat_ping_state AS state ON state.drone_id = drone.id
2056
- WHERE drone.evicted_at IS NULL AND COALESCE(drone.last_seen, drone.created_at) < ?
2057
- AND COALESCE(state.attempts, 0) < 3
2058
- AND (state.last_ping_at IS NULL OR state.last_ping_at < ?)
2059
- AND EXISTS (SELECT 1 FROM drone_sessions AS session
2060
- WHERE session.drone_id = drone.id AND session.revoked_at IS NULL AND session.expires_at > ?)
2061
- ORDER BY COALESCE(drone.last_seen, drone.created_at), drone.id LIMIT ?
2062
- `).all(silentBefore, cooldownBefore, now.toISOString(), limit);
2063
- const published: ActivityNotificationRecord[] = [];
2064
- for (const candidate of candidates) {
2065
- const droneId = requiredText(candidate, "drone_id");
2066
- const cubeId = requiredText(candidate, "cube_id");
2067
- const message = "[HEARTBEAT-PING] No recent activity; confirm this seat is responsive.";
2068
- this.capacityGuard.assertCanGrow(256);
2069
- const id = randomUUID();
2070
- const createdAt = now.toISOString();
2071
- this.database.exec("BEGIN IMMEDIATE");
2072
- try {
2073
- this.database.prepare(`INSERT INTO silent_seat_ping_state (drone_id, attempts, last_ping_at)
2074
- VALUES (?, 1, ?) ON CONFLICT(drone_id) DO UPDATE
2075
- SET attempts = attempts + 1, last_ping_at = excluded.last_ping_at
2076
- `).run(droneId, now.toISOString());
2077
- this.database.exec("COMMIT");
2078
- } catch (error) {
2079
- try { this.database.exec("ROLLBACK"); } catch { /* Preserve original failure. */ }
2080
- throw error;
2081
- }
2082
- const entry: ActivityNotificationRecord = {
2083
- kind: "heartbeat_ping",
2084
- id, cube_id: cubeId, drone_id: null, message, visibility: "direct", created_at: createdAt,
2085
- drone_label: null, role_name: null, recipient_drone_ids: [droneId],
2086
- };
2087
- this.hub.publish(cubeId, entry);
2088
- published.push(entry);
2089
- }
2090
- return published;
2253
+ class IdleLivenessStore implements LivenessStore {
2254
+ scan(): ActivityNotificationRecord[] {
2255
+ // Silence is not a delivery failure. Keep the scheduler's lifecycle seam without
2256
+ // creating a directed event that crosses the client/model wake boundary.
2257
+ return [];
2091
2258
  }
2092
2259
  }
2093
2260
 
@@ -2416,7 +2583,7 @@ class SqliteCredentialStore implements CredentialStore {
2416
2583
  credential.session_id, session.client_id, session.cube_id, session.drone_id,
2417
2584
  session.expires_at,
2418
2585
  COALESCE(credential.revoked_at, session.revoked_at, client.revoked_at) AS revoked_at,
2419
- drone.evicted_at
2586
+ drone.evicted_at, session.superseded_at
2420
2587
  FROM drone_session_credentials AS credential
2421
2588
  JOIN drone_sessions AS session ON session.id = credential.session_id
2422
2589
  JOIN clients AS client ON client.id = session.client_id
@@ -2894,6 +3061,7 @@ function storedDroneSessionDigest(row: Record<string, unknown>): StoredDroneSess
2894
3061
  droneId: requiredText(row, "drone_id"),
2895
3062
  expiresAt: requiredText(row, "expires_at"),
2896
3063
  evictedAt: nullableText(row, "evicted_at"),
3064
+ takenOver: nullableText(row, "superseded_at") !== null,
2897
3065
  };
2898
3066
  }
2899
3067