instar 1.3.651 → 1.3.652

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +120 -1
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/ReapGuard.d.ts +13 -1
  5. package/dist/core/ReapGuard.d.ts.map +1 -1
  6. package/dist/core/ReapGuard.js +86 -30
  7. package/dist/core/ReapGuard.js.map +1 -1
  8. package/dist/core/SessionManager.d.ts +11 -0
  9. package/dist/core/SessionManager.d.ts.map +1 -1
  10. package/dist/core/SessionManager.js +30 -4
  11. package/dist/core/SessionManager.js.map +1 -1
  12. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  13. package/dist/core/devGatedFeatures.js +6 -0
  14. package/dist/core/devGatedFeatures.js.map +1 -1
  15. package/dist/core/gapBCommitmentEvidence.d.ts +9 -0
  16. package/dist/core/gapBCommitmentEvidence.d.ts.map +1 -1
  17. package/dist/core/gapBCommitmentEvidence.js +20 -0
  18. package/dist/core/gapBCommitmentEvidence.js.map +1 -1
  19. package/dist/core/types.d.ts +11 -0
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/core/types.js.map +1 -1
  22. package/dist/monitoring/SessionReaper.d.ts +118 -7
  23. package/dist/monitoring/SessionReaper.d.ts.map +1 -1
  24. package/dist/monitoring/SessionReaper.js +302 -101
  25. package/dist/monitoring/SessionReaper.js.map +1 -1
  26. package/dist/monitoring/closeoutLivenessSnapshot.d.ts +143 -0
  27. package/dist/monitoring/closeoutLivenessSnapshot.d.ts.map +1 -0
  28. package/dist/monitoring/closeoutLivenessSnapshot.js +207 -0
  29. package/dist/monitoring/closeoutLivenessSnapshot.js.map +1 -0
  30. package/package.json +1 -1
  31. package/src/data/builtin-manifest.json +3 -3
  32. package/upgrades/1.3.652.md +18 -0
  33. package/upgrades/side-effects/post-transfer-closeout-correctness.md +43 -0
@@ -103,6 +103,20 @@ export interface SessionReaperConfig {
103
103
  * breaker resets when the topic returns home / a pin-conflict hold engages /
104
104
  * the session ends. Default 5 (~10 min of vetoes at the default 120s tick). */
105
105
  topicMovedVetoBreakerAttempts: number;
106
+ /** Post-transfer closeout CORRECTNESS gate (F1,
107
+ * docs/specs/post-transfer-closeout-correctness.md). When true, the closeout
108
+ * no longer terminates the live local session on a stale/unverified ownership
109
+ * record: it consults `remoteOwnerHasLiveSession` and proceeds ONLY when the
110
+ * owning machine genuinely HAS a live session for the topic (a real
111
+ * duplicate). `false` (owner has no live session) / `'unknown'` / dep-absent →
112
+ * WITHHOLD (fail-closed — never kill the sole live worker). It also re-keys the
113
+ * closeout breaker counters on the stable TOPIC id (so a session-id churn
114
+ * across respawn no longer resets the veto count) and passes the narrow
115
+ * `bypassRecentUserMessageForConfirmedMove` on a liveness-CONFIRMED move so a
116
+ * genuine leftover with a pre-move "recent" message can actually shed.
117
+ * Ships DARK fleet-wide, LIVE on a dev agent via `resolveDevAgentGate`. When
118
+ * OFF the closeout's observable behavior is byte-identical to today. */
119
+ closeoutLivenessGate: boolean;
106
120
  }
107
121
  export declare const DEFAULT_SESSION_REAPER_CONFIG: SessionReaperConfig;
108
122
  /**
@@ -183,6 +197,43 @@ export interface SessionReaperDeps {
183
197
  * is unowned / owned by this machine / the pool is dark. Absent ⇒ the
184
198
  * topic-moved closeout rule is inert. */
185
199
  topicOwnerElsewhere?: (topicId: number) => string | null;
200
+ /** Post-transfer closeout correctness (F1): the combined, ATOMIC owner read
201
+ * the liveness-gated closeout uses — ONE registry read returning BOTH the
202
+ * STABLE `machineId` (the un-nicknamed `reg.ownerOf(...)` value — the
203
+ * liveness/snapshot key, immune to nickname rename/duplication) and the
204
+ * `displayName` (= nickname ?? machineId — audit/operator text). Returning
205
+ * both from one read guarantees the liveness key and the display text describe
206
+ * the SAME owner from the SAME instant (no straddle across an ownership change).
207
+ * null ⇒ the topic is not owned elsewhere. Consulted ONLY when
208
+ * `closeoutLivenessGate` is on; absent-under-gate ⇒ the closeout fail-closes to
209
+ * WITHHOLD (it never silently falls back to the display-only dep). */
210
+ topicOwnerElsewhereInfo?: (topicId: number) => {
211
+ machineId: string;
212
+ displayName: string;
213
+ } | null;
214
+ /** Post-transfer closeout correctness (F1): does the machine that OWNS this
215
+ * topic (per the ownership registry) actually have a LIVE session for it right
216
+ * now? Reads the machine-local liveness snapshot (NOT a live per-tick fetch).
217
+ * state:true → owner genuinely serving → local is a duplicate → may shed.
218
+ * state:false → owner has NO live session → local is the sole worker →
219
+ * WITHHOLD (never terminate it).
220
+ * state:'unknown'→ liveness undeterminable (snapshot missing/stale, peer
221
+ * unreachable) → WITHHOLD (fail-closed; UNKNOWN must NEVER act).
222
+ * `reachableAt` (ms) = the snapshot pass that produced the answer; backs the
223
+ * true-side dwell-advancement (a confirm tick only counts when `reachableAt`
224
+ * ADVANCED). Present on true/false, absent on 'unknown'. Absent dep ⇒ the
225
+ * liveness gate is inert. */
226
+ remoteOwnerHasLiveSession?: (topicId: number, ownerMachineId: string) => {
227
+ state: boolean | 'unknown';
228
+ reachableAt?: number;
229
+ };
230
+ /** Post-transfer closeout correctness (F1, Part E freshest-interaction veto):
231
+ * the LOCAL-RECEIPT timestamp (ms) of the bound topic's most-recent user
232
+ * message, or null. SAME source the existing `recent-user-message` KEEP-guard
233
+ * reads, so the basis is local wall-clock at message receipt. Used to withhold
234
+ * the recent-message bypass when a user message arrived AFTER the liveness
235
+ * snapshot (the local session the user is actively talking to is kept). */
236
+ recentUserMessageAt?: (topicId: number) => number | null;
186
237
  /** WS1.3: does the topic's placement PIN name THIS machine? A pin-conflict
187
238
  * (pin=here, owner=elsewhere) means the divergence is reconciling TOWARD us —
188
239
  * the closeout holds (do-not-act) instead of attacking the session the pin
@@ -210,6 +261,11 @@ export interface SessionReaperDeps {
210
261
  * KEEP-guard is re-checked by the authority and still vetoes. */
211
262
  terminate: (sessionId: string, reason: string, opts?: {
212
263
  bypassActiveProcessKeep?: boolean;
264
+ /** Post-transfer closeout correctness (F1, Part E): lifts ONLY the
265
+ * `recent-user-message` keep-reason (every other guard re-checked), passed
266
+ * ONLY on a liveness-CONFIRMED genuine move so a duplicate leftover with a
267
+ * pre-move "recent" message can shed. */
268
+ bypassRecentUserMessageForConfirmedMove?: boolean;
213
269
  workEvidence?: string[];
214
270
  }) => Promise<{
215
271
  terminated: boolean;
@@ -255,6 +311,27 @@ export interface Obs {
255
311
  /** When this session entered reap-pending (two-phase), if it has. */
256
312
  reapPendingSince?: number;
257
313
  }
314
+ /**
315
+ * The closeout dwell-streak value. OFF mode uses the legacy `number | -1` (-1 =
316
+ * held sentinel). ON (gated) mode uses the richer struct so the dwell advances
317
+ * only across DISTINCT snapshot generations and the GC grace window applies to
318
+ * held + counting episodes uniformly:
319
+ * - `{ kind: 'counting'; count; lastTrueReachableAt; lastSeenAt }` — `count` is
320
+ * the consecutive-distinct-generation dwell; a `true` tick advances it only
321
+ * when `reachableAt > lastTrueReachableAt`.
322
+ * - `{ kind: 'held'; lastSeenAt }` — REPLACES the legacy `-1` sentinel for the
323
+ * WITHHOLD / pin-conflict episodes (audited once; never confirms terminate);
324
+ * `lastSeenAt` lets the GC grace window evict a held topic uniformly.
325
+ */
326
+ export type TopicMovedStreakValue = number | {
327
+ kind: 'counting';
328
+ count: number;
329
+ lastTrueReachableAt: number;
330
+ lastSeenAt: number;
331
+ } | {
332
+ kind: 'held';
333
+ lastSeenAt: number;
334
+ };
258
335
  export declare class SessionReaper extends EventEmitter {
259
336
  private readonly cfg;
260
337
  private readonly deps;
@@ -268,14 +345,22 @@ export declare class SessionReaper extends EventEmitter {
268
345
  /** Consecutive busy-orphan-suspect ticks per session (observe-only dwell for
269
346
  * `busyOrphanDetection`). Resets to 0 on any non-suspect tick. GC'd with obs. */
270
347
  private busyOrphanStreak;
271
- /** Consecutive VETOED closeout terminate attempts per session (the WS1.2
272
- * P19 breaker counter). Distinct from the dwell streak: this counts only
273
- * real terminate() calls that came back vetoed. At
274
- * `topicMovedVetoBreakerAttempts` the closeout stops retrying for the
275
- * episode. Reset on success / topic-home / pin-conflict hold. GC'd with obs. */
348
+ /** Consecutive VETOED closeout terminate attempts (the WS1.2 P19 breaker
349
+ * counter). Distinct from the dwell streak: this counts only real terminate()
350
+ * calls that came back vetoed. At `topicMovedVetoBreakerAttempts` the closeout
351
+ * stops retrying for the episode. Reset on success / topic-home / pin-conflict
352
+ * hold. GC'd with obs.
353
+ * Keyed on session.id when `closeoutLivenessGate` is OFF (legacy); on the
354
+ * STABLE topic id when ON (so a session-id churn across respawn no longer
355
+ * resets the count — the Secondary fix). Value is always a plain number. */
276
356
  private topicMovedVetoes;
277
- /** Consecutive owned-elsewhere ticks per session (the topicMovedCloseout
278
- * dwell). Resets when the topic returns to this machine / unowned. GC'd with obs. */
357
+ /** Consecutive owned-elsewhere ticks (the topicMovedCloseout dwell). Resets
358
+ * when the topic returns home / unowned. GC'd with obs.
359
+ * OFF (legacy): keyed on session.id, value `number | -1` (-1 = held sentinel).
360
+ * ON (gated): keyed on the stable topic id, value the richer struct below —
361
+ * `lastTrueReachableAt` enforces dwell advancement (a confirm tick counts only
362
+ * when the snapshot `reachableAt` advanced), `lastSeenAt` backs the GC grace
363
+ * window so a held topic participates in eviction uniformly. */
279
364
  private topicMovedStreak;
280
365
  /** Last audited `verdict:keptBy` per session — so the decision audit logs only
281
366
  * on a CHANGE, not every tick (auditability without per-tick log spam). */
@@ -334,6 +419,32 @@ export declare class SessionReaper extends EventEmitter {
334
419
  /** Active idle threshold (ms) for the current pressure tier. */
335
420
  private thresholdMs;
336
421
  private hourlyBudgetRemaining;
422
+ /**
423
+ * Post-transfer closeout — LEGACY path (closeoutLivenessGate OFF). Byte-identical
424
+ * observable behavior to the pre-correctness-fix code: session-id-keyed maps, no
425
+ * liveness check, no Part E bypass. Returns whether the session was terminated +
426
+ * the running reap count.
427
+ */
428
+ private runCloseoutLegacy;
429
+ /**
430
+ * Post-transfer closeout — GATED path (closeoutLivenessGate ON). The
431
+ * correctness fix: NEVER terminate the live local session on a stale/unverified
432
+ * ownership record. Resolves the owner ATOMICALLY (machineId + display), then
433
+ * consults `remoteOwnerHasLiveSession`:
434
+ * true → genuine move (real duplicate) → dwell→terminate (with Part E bypass).
435
+ * false → owner has no live session → WITHHOLD (neutral once-per-episode audit).
436
+ * 'unknown'/throw/dep-absent → WITHHOLD (fail-closed; UNKNOWN must NEVER act).
437
+ * Maps are keyed on the stable TOPIC id (Secondary fix) with the richer streak
438
+ * struct so the dwell advances only across DISTINCT snapshot generations.
439
+ */
440
+ private runCloseoutGated;
441
+ /**
442
+ * Shared closeout terminate machinery — the dwell has been met. Handles the P19
443
+ * breaker, dry-run, budget caps, the guarded terminate, and the veto/breaker
444
+ * audit. `key` is session.id (legacy) or the topic id (gated); `bypassRecentForMove`
445
+ * is passed through to terminate ONLY on a liveness-confirmed genuine move.
446
+ */
447
+ private attemptCloseoutTerminate;
337
448
  tick(): Promise<void>;
338
449
  private performReap;
339
450
  /** Emit a `decision` audit row only when a session's (verdict, keptBy) differs
@@ -1 +1 @@
1
- {"version":3,"file":"SessionReaper.d.ts","sourceRoot":"","sources":["../../src/monitoring/SessionReaper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"SessionReaper.d.ts","sourceRoot":"","sources":["../../src/monitoring/SessionReaper.ts"],"names":[],"mappings":"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"}
@@ -48,6 +48,7 @@ export const DEFAULT_SESSION_REAPER_CONFIG = {
48
48
  topicMovedCloseout: true,
49
49
  topicMovedConfirmTicks: 2,
50
50
  topicMovedVetoBreakerAttempts: 5,
51
+ closeoutLivenessGate: false,
51
52
  };
52
53
  /** Memory-pressure thresholds (freePct). Kept as constants — the existing
53
54
  * behavior surface; CPU thresholds are the configurable addition. */
@@ -96,14 +97,22 @@ export class SessionReaper extends EventEmitter {
96
97
  /** Consecutive busy-orphan-suspect ticks per session (observe-only dwell for
97
98
  * `busyOrphanDetection`). Resets to 0 on any non-suspect tick. GC'd with obs. */
98
99
  busyOrphanStreak = new Map();
99
- /** Consecutive VETOED closeout terminate attempts per session (the WS1.2
100
- * P19 breaker counter). Distinct from the dwell streak: this counts only
101
- * real terminate() calls that came back vetoed. At
102
- * `topicMovedVetoBreakerAttempts` the closeout stops retrying for the
103
- * episode. Reset on success / topic-home / pin-conflict hold. GC'd with obs. */
100
+ /** Consecutive VETOED closeout terminate attempts (the WS1.2 P19 breaker
101
+ * counter). Distinct from the dwell streak: this counts only real terminate()
102
+ * calls that came back vetoed. At `topicMovedVetoBreakerAttempts` the closeout
103
+ * stops retrying for the episode. Reset on success / topic-home / pin-conflict
104
+ * hold. GC'd with obs.
105
+ * Keyed on session.id when `closeoutLivenessGate` is OFF (legacy); on the
106
+ * STABLE topic id when ON (so a session-id churn across respawn no longer
107
+ * resets the count — the Secondary fix). Value is always a plain number. */
104
108
  topicMovedVetoes = new Map();
105
- /** Consecutive owned-elsewhere ticks per session (the topicMovedCloseout
106
- * dwell). Resets when the topic returns to this machine / unowned. GC'd with obs. */
109
+ /** Consecutive owned-elsewhere ticks (the topicMovedCloseout dwell). Resets
110
+ * when the topic returns home / unowned. GC'd with obs.
111
+ * OFF (legacy): keyed on session.id, value `number | -1` (-1 = held sentinel).
112
+ * ON (gated): keyed on the stable topic id, value the richer struct below —
113
+ * `lastTrueReachableAt` enforces dwell advancement (a confirm tick counts only
114
+ * when the snapshot `reachableAt` advanced), `lastSeenAt` backs the GC grace
115
+ * window so a held topic participates in eviction uniformly. */
107
116
  topicMovedStreak = new Map();
108
117
  /** Last audited `verdict:keptBy` per session — so the decision audit logs only
109
118
  * on a CHANGE, not every tick (auditability without per-tick log spam). */
@@ -383,6 +392,237 @@ export class SessionReaper extends EventEmitter {
383
392
  this.reapTimestamps = this.reapTimestamps.filter(t => t >= cutoff);
384
393
  return this.cfg.maxReapsPerHour - this.reapTimestamps.length;
385
394
  }
395
+ /**
396
+ * Post-transfer closeout — LEGACY path (closeoutLivenessGate OFF). Byte-identical
397
+ * observable behavior to the pre-correctness-fix code: session-id-keyed maps, no
398
+ * liveness check, no Part E bypass. Returns whether the session was terminated +
399
+ * the running reap count.
400
+ */
401
+ async runCloseoutLegacy(session, reapedThisTick) {
402
+ let otherOwner = null;
403
+ let pinnedHere = false;
404
+ try {
405
+ const topicId = this.deps.topicBinding(session.tmuxSession);
406
+ otherOwner = topicId != null ? (this.deps.topicOwnerElsewhere?.(topicId) ?? null) : null;
407
+ // WS1.3: pin-conflict = do-not-act (reconcile mid-flight TOWARD us).
408
+ pinnedHere = topicId != null && (this.deps.topicPinnedHere?.(topicId) ?? false);
409
+ }
410
+ catch {
411
+ otherOwner = null; /* @silent-fallback-ok — ownership signal failed → cannot reason → skip rule (safe withhold direction) */
412
+ }
413
+ if (otherOwner && pinnedHere) {
414
+ const prior = this.topicMovedStreak.get(session.id) ?? 0;
415
+ if (prior !== -1) {
416
+ this.audit('reap-skipped-topic-moved', session, { rule: 'topic-moved-away', otherOwner, skipped: 'pin-conflict-pending-reconcile' });
417
+ this.topicMovedStreak.set(session.id, -1);
418
+ }
419
+ this.topicMovedVetoes.delete(session.id);
420
+ return { terminated: false, reapedThisTick };
421
+ }
422
+ if (otherOwner) {
423
+ const streak = (this.topicMovedStreak.get(session.id) ?? 0) + 1;
424
+ this.topicMovedStreak.set(session.id, streak);
425
+ if (streak >= this.cfg.topicMovedConfirmTicks) {
426
+ const reason = `topic moved to ${otherOwner} — closing the leftover session on this machine (post-transfer closeout)`;
427
+ return this.attemptCloseoutTerminate({
428
+ session, otherOwner, reason, streak,
429
+ key: session.id, reapedThisTick, bypassRecentForMove: false,
430
+ });
431
+ }
432
+ return { terminated: false, reapedThisTick };
433
+ }
434
+ // owned-by-self / unowned — clear the episode (counting streak AND -1 sentinel).
435
+ if ((this.topicMovedStreak.get(session.id) ?? 0) !== 0) {
436
+ this.topicMovedStreak.set(session.id, 0);
437
+ this.topicMovedVetoes.delete(session.id);
438
+ }
439
+ return { terminated: false, reapedThisTick };
440
+ }
441
+ /**
442
+ * Post-transfer closeout — GATED path (closeoutLivenessGate ON). The
443
+ * correctness fix: NEVER terminate the live local session on a stale/unverified
444
+ * ownership record. Resolves the owner ATOMICALLY (machineId + display), then
445
+ * consults `remoteOwnerHasLiveSession`:
446
+ * true → genuine move (real duplicate) → dwell→terminate (with Part E bypass).
447
+ * false → owner has no live session → WITHHOLD (neutral once-per-episode audit).
448
+ * 'unknown'/throw/dep-absent → WITHHOLD (fail-closed; UNKNOWN must NEVER act).
449
+ * Maps are keyed on the stable TOPIC id (Secondary fix) with the richer streak
450
+ * struct so the dwell advances only across DISTINCT snapshot generations.
451
+ */
452
+ async runCloseoutGated(session, reapedThisTick) {
453
+ let topicId = null;
454
+ let owner = null;
455
+ let pinnedHere = false;
456
+ try {
457
+ topicId = this.deps.topicBinding(session.tmuxSession);
458
+ // The gated path REQUIRES the combined dep — it never falls back to the
459
+ // display-only one. Absent ⇒ owner stays null ⇒ withhold (fail-closed below).
460
+ owner = topicId != null ? (this.deps.topicOwnerElsewhereInfo?.(topicId) ?? null) : null;
461
+ pinnedHere = topicId != null && (this.deps.topicPinnedHere?.(topicId) ?? false);
462
+ }
463
+ catch {
464
+ owner = null; /* @silent-fallback-ok — ownership signal failed → cannot reason → skip rule (safe withhold direction) */
465
+ }
466
+ // topicId null → no participation (also covers the topicOwnerElsewhereInfo-absent
467
+ // dep window: owner is null → the "no owner-elsewhere" reset arm below runs).
468
+ if (topicId == null)
469
+ return { terminated: false, reapedThisTick };
470
+ const key = topicId; // stable topic-keyed breaker state (Secondary fix)
471
+ const now = this.now();
472
+ if (owner && pinnedHere) {
473
+ // Pin-conflict hold WINS ahead of the liveness gate (unchanged WS1.3 rule).
474
+ const prior = this.topicMovedStreak.get(key);
475
+ const isHeld = typeof prior === 'object' && prior.kind === 'held';
476
+ if (!isHeld) {
477
+ this.audit('reap-skipped-topic-moved', session, { rule: 'topic-moved-away', otherOwner: owner.displayName, skipped: 'pin-conflict-pending-reconcile' });
478
+ }
479
+ this.topicMovedStreak.set(key, { kind: 'held', lastSeenAt: now });
480
+ this.topicMovedVetoes.delete(key);
481
+ return { terminated: false, reapedThisTick };
482
+ }
483
+ if (owner) {
484
+ // ── Part C: liveness gate ──────────────────────────────────────────────
485
+ let liveness;
486
+ try {
487
+ liveness = this.deps.remoteOwnerHasLiveSession
488
+ ? this.deps.remoteOwnerHasLiveSession(topicId, owner.machineId)
489
+ : { state: 'unknown' }; // dep absent under gate → fail-closed
490
+ }
491
+ catch {
492
+ liveness = { state: 'unknown' }; // a throw is treated as 'unknown'
493
+ }
494
+ if (liveness.state !== true) {
495
+ // false / 'unknown' → WITHHOLD. Once-per-episode neutral audit + held
496
+ // sentinel; a withheld episode never accrues breaker vetoes.
497
+ const prior = this.topicMovedStreak.get(key);
498
+ const isHeld = typeof prior === 'object' && prior.kind === 'held';
499
+ if (!isHeld) {
500
+ const ownerEntry = liveness.reachableAt;
501
+ this.audit('reap-skipped-topic-moved', session, {
502
+ rule: 'topic-moved-away',
503
+ otherOwner: owner.displayName,
504
+ ownerMachineId: owner.machineId,
505
+ skipped: liveness.state === false ? 'no-live-remote-session' : 'remote-liveness-unknown',
506
+ // Neutral, non-directional observational evidence (NO reconcileToward).
507
+ remoteOwnerListedSession: false,
508
+ withheldCloseout: true,
509
+ possibleStaleOwner: true,
510
+ snapshotAgeMs: ownerEntry != null ? now - ownerEntry : undefined,
511
+ });
512
+ }
513
+ this.topicMovedStreak.set(key, { kind: 'held', lastSeenAt: now });
514
+ this.topicMovedVetoes.delete(key);
515
+ return { terminated: false, reapedThisTick };
516
+ }
517
+ // state === true: genuine move. Advance the dwell ONLY across a DISTINCT
518
+ // snapshot generation (reachableAt strictly newer than lastTrueReachableAt).
519
+ const reachableAt = liveness.reachableAt ?? 0;
520
+ const prior = this.topicMovedStreak.get(key);
521
+ let count;
522
+ let lastTrueReachableAt;
523
+ if (typeof prior === 'object' && prior.kind === 'counting') {
524
+ if (reachableAt > prior.lastTrueReachableAt) {
525
+ count = prior.count + 1;
526
+ lastTrueReachableAt = reachableAt;
527
+ }
528
+ else {
529
+ // Same generation re-read — the tick does not advance the streak.
530
+ count = prior.count;
531
+ lastTrueReachableAt = prior.lastTrueReachableAt;
532
+ }
533
+ }
534
+ else {
535
+ // First counting tick OR transition out of a held (-1) episode → start at 1.
536
+ count = 1;
537
+ lastTrueReachableAt = reachableAt;
538
+ }
539
+ this.topicMovedStreak.set(key, { kind: 'counting', count, lastTrueReachableAt, lastSeenAt: now });
540
+ if (count >= this.cfg.topicMovedConfirmTicks) {
541
+ const reason = `topic moved to ${owner.displayName} — closing the leftover session on this machine (post-transfer closeout)`;
542
+ // Part E: pass the narrow bypass ONLY when the topic's freshest LOCAL user
543
+ // message is OLDER than the snapshot reachableAt (freshest-interaction veto).
544
+ const lastUserMsgAt = (() => {
545
+ try {
546
+ return this.deps.recentUserMessageAt?.(topicId) ?? null;
547
+ }
548
+ catch {
549
+ return null; /* @silent-fallback-ok — recent-msg signal failed → treat as no recent message → bypass not granted (safe) */
550
+ }
551
+ })();
552
+ const bypassRecentForMove = lastUserMsgAt == null || lastUserMsgAt <= reachableAt;
553
+ return this.attemptCloseoutTerminate({
554
+ session, otherOwner: owner.displayName, reason, streak: count,
555
+ key, reapedThisTick, bypassRecentForMove,
556
+ confirmedMove: true, snapshotReachableAt: reachableAt,
557
+ });
558
+ }
559
+ return { terminated: false, reapedThisTick };
560
+ }
561
+ // No owner-elsewhere — clear the episode (counting OR held), so a FUTURE move
562
+ // starts clean. Mirrors the legacy reset arm under the topic key.
563
+ if (this.topicMovedStreak.has(key)) {
564
+ this.topicMovedStreak.delete(key);
565
+ this.topicMovedVetoes.delete(key);
566
+ }
567
+ return { terminated: false, reapedThisTick };
568
+ }
569
+ /**
570
+ * Shared closeout terminate machinery — the dwell has been met. Handles the P19
571
+ * breaker, dry-run, budget caps, the guarded terminate, and the veto/breaker
572
+ * audit. `key` is session.id (legacy) or the topic id (gated); `bypassRecentForMove`
573
+ * is passed through to terminate ONLY on a liveness-confirmed genuine move.
574
+ */
575
+ async attemptCloseoutTerminate(args) {
576
+ const { session, otherOwner, reason, streak, key, bypassRecentForMove } = args;
577
+ let { reapedThisTick } = args;
578
+ const vetoes = this.topicMovedVetoes.get(key) ?? 0;
579
+ if (vetoes >= this.cfg.topicMovedVetoBreakerAttempts) {
580
+ // P19 breaker OPEN — stop retrying for the episode (audited+escalated once).
581
+ return { terminated: false, reapedThisTick };
582
+ }
583
+ if (!this.killsEnabled) {
584
+ if (streak === this.cfg.topicMovedConfirmTicks) {
585
+ this.audit('would-reap', session, { rule: 'topic-moved-away', otherOwner, dryRun: true });
586
+ }
587
+ return { terminated: false, reapedThisTick };
588
+ }
589
+ if (reapedThisTick < this.cfg.maxReapsPerTick && this.hourlyBudgetRemaining() > 0) {
590
+ const res = await this.deps.terminate(session.id, reason, bypassRecentForMove
591
+ ? { bypassRecentUserMessageForConfirmedMove: true, workEvidence: [] }
592
+ : undefined);
593
+ if (res.terminated) {
594
+ reapedThisTick++;
595
+ this.reapTimestamps.push(this.now());
596
+ this.audit('reaped', session, {
597
+ rule: 'topic-moved-away', otherOwner,
598
+ ...(args.confirmedMove ? { confirmedMove: true, snapshotReachableAt: args.snapshotReachableAt } : {}),
599
+ });
600
+ this.topicMovedStreak.delete(key);
601
+ this.topicMovedVetoes.delete(key);
602
+ return { terminated: true, reapedThisTick };
603
+ }
604
+ // Guard veto / already-terminal — audit once per streak crossing, keep the
605
+ // streak so next tick retries (bounded by the breaker).
606
+ const v = vetoes + 1;
607
+ this.topicMovedVetoes.set(key, v);
608
+ if (streak === this.cfg.topicMovedConfirmTicks) {
609
+ this.audit('reap-skipped-topic-moved', session, { rule: 'topic-moved-away', otherOwner, skipped: res.skipped });
610
+ }
611
+ if (v === this.cfg.topicMovedVetoBreakerAttempts) {
612
+ this.audit('closeout-breaker-open', session, {
613
+ rule: 'topic-moved-away', otherOwner, vetoedAttempts: v, lastSkipped: res.skipped ?? 'keep-guard',
614
+ });
615
+ const topicId = this.deps.topicBinding(session.tmuxSession);
616
+ this.deps.raiseAttention?.({
617
+ id: `closeout-breaker:${session.id}`,
618
+ title: `Topic ${topicId ?? '?'} moved to ${otherOwner}, but the old session won't close`,
619
+ summary: `Post-transfer closeout gave up after ${v} vetoed attempts (held by: ${res.skipped ?? 'keep-guard'}).`,
620
+ description: `The conversation for topic ${topicId ?? '?'} now lives on ${otherOwner}, but the leftover session on this machine (${session.tmuxSession}) refused to close ${v} times in a row — a KEEP-guard reports it is still working (${res.skipped ?? 'keep-guard'}). Closeout retries have stopped (P19 breaker); the session will close via the normal idle path when it finishes, or you can close it from the dashboard.`,
621
+ });
622
+ }
623
+ }
624
+ return { terminated: false, reapedThisTick };
625
+ }
386
626
  async tick() {
387
627
  if (!this.cfg.enabled || this.running)
388
628
  return;
@@ -408,12 +648,54 @@ export class SessionReaper extends EventEmitter {
408
648
  for (const id of [...this.busyOrphanStreak.keys()])
409
649
  if (!live.has(id))
410
650
  this.busyOrphanStreak.delete(id);
411
- for (const id of [...this.topicMovedStreak.keys()])
412
- if (!live.has(id))
413
- this.topicMovedStreak.delete(id);
414
- for (const id of [...this.topicMovedVetoes.keys()])
415
- if (!live.has(id))
416
- this.topicMovedVetoes.delete(id);
651
+ if (this.cfg.closeoutLivenessGate) {
652
+ // Gated: the closeout maps are keyed on the stable TOPIC id, so a naive
653
+ // "GC any topic without a live session THIS tick" would erase the count
654
+ // during the brief gap between a session dying and its same-topic respawn
655
+ // appearing — defeating the topic key. Instead evict a topic entry only
656
+ // when it has had no live binding AND was not owned-elsewhere for a grace
657
+ // window (2× tickIntervalSec — survives one full respawn gap). `lastSeenAt`
658
+ // is stamped on every entry whenever its topic is bound/owned-elsewhere.
659
+ const liveTopics = new Set();
660
+ for (const s of sessions) {
661
+ const t = this.deps.topicBinding(s.tmuxSession);
662
+ if (t != null)
663
+ liveTopics.add(t);
664
+ }
665
+ const graceMs = 2 * this.cfg.tickIntervalSec * 1000;
666
+ const now = this.now();
667
+ for (const [key, val] of [...this.topicMovedStreak.entries()]) {
668
+ if (typeof key !== 'number') {
669
+ this.topicMovedStreak.delete(key);
670
+ continue;
671
+ } // stale OFF-mode key
672
+ if (liveTopics.has(key))
673
+ continue; // still bound — keep
674
+ const lastSeen = typeof val === 'object' ? val.lastSeenAt : 0;
675
+ if (now - lastSeen > graceMs) {
676
+ this.topicMovedStreak.delete(key);
677
+ this.topicMovedVetoes.delete(key);
678
+ }
679
+ }
680
+ // topicMovedVetoes with no streak entry left (and not a live topic) → drop.
681
+ for (const key of [...this.topicMovedVetoes.keys()]) {
682
+ if (typeof key !== 'number') {
683
+ this.topicMovedVetoes.delete(key);
684
+ continue;
685
+ }
686
+ if (!liveTopics.has(key) && !this.topicMovedStreak.has(key))
687
+ this.topicMovedVetoes.delete(key);
688
+ }
689
+ }
690
+ else {
691
+ // OFF (legacy): keyed on session.id — GC any id not in the live set.
692
+ for (const id of [...this.topicMovedStreak.keys()])
693
+ if (typeof id === 'number' || !live.has(id))
694
+ this.topicMovedStreak.delete(id);
695
+ for (const id of [...this.topicMovedVetoes.keys()])
696
+ if (typeof id === 'number' || !live.has(id))
697
+ this.topicMovedVetoes.delete(id);
698
+ }
417
699
  let reapedThisTick = 0;
418
700
  for (const session of sessions) {
419
701
  // ── Post-transfer closeout (operator-named issue, 2026-06-05) ──────
@@ -425,94 +707,13 @@ export class SessionReaper extends EventEmitter {
425
707
  // veto is audited and retried next tick (eventual closeout, never a
426
708
  // forced kill). Dwell of `topicMovedConfirmTicks` absorbs ownership
427
709
  // churn mid-transfer.
428
- if (this.cfg.topicMovedCloseout && this.deps.topicOwnerElsewhere) {
429
- let otherOwner = null;
430
- let pinnedHere = false;
431
- try {
432
- const topicId = this.deps.topicBinding(session.tmuxSession);
433
- otherOwner = topicId != null ? this.deps.topicOwnerElsewhere(topicId) : null;
434
- // WS1.3 (MULTI-MACHINE-SEAMLESSNESS-SPEC): pin-conflict = do-not-act.
435
- // When the topic's PIN names THIS machine while ownership still says
436
- // another, the divergence is mid-reconcile TOWARD us — the
437
- // OwnershipReconciler is bringing the record back, and closing the
438
- // local session now would kill the exact session the pin wants here
439
- // (the 2026-06-12 incident: the closeout attacked the working laptop
440
- // session every 2 minutes for hours during a stuck transfer-back).
441
- pinnedHere = topicId != null && (this.deps.topicPinnedHere?.(topicId) ?? false);
442
- }
443
- catch {
444
- otherOwner = null; /* signal failed → cannot reason → skip rule */
445
- }
446
- if (otherOwner && pinnedHere) {
447
- // -1 is the held-and-audited sentinel: audit ONCE per conflict
448
- // episode, hold (never act) for as long as the pin names us.
449
- const prior = this.topicMovedStreak.get(session.id) ?? 0;
450
- if (prior !== -1) {
451
- this.audit('reap-skipped-topic-moved', session, { rule: 'topic-moved-away', otherOwner, skipped: 'pin-conflict-pending-reconcile' });
452
- this.topicMovedStreak.set(session.id, -1);
453
- }
454
- // The closeout intent is withdrawn while the pin holds — a fresh
455
- // episode after reconcile starts with a clean breaker.
456
- this.topicMovedVetoes.delete(session.id);
457
- }
458
- else if (otherOwner) {
459
- const streak = (this.topicMovedStreak.get(session.id) ?? 0) + 1;
460
- this.topicMovedStreak.set(session.id, streak);
461
- if (streak >= this.cfg.topicMovedConfirmTicks) {
462
- const reason = `topic moved to ${otherOwner} — closing the leftover session on this machine (post-transfer closeout)`;
463
- const vetoes = this.topicMovedVetoes.get(session.id) ?? 0;
464
- if (vetoes >= this.cfg.topicMovedVetoBreakerAttempts) {
465
- // P19 breaker OPEN (WS1.2): this session vetoed the closeout
466
- // `topicMovedVetoBreakerAttempts` times in a row — stop
467
- // retrying for the episode. NOT a stranded session: it falls
468
- // through to the idle pipeline below every tick, so it is
469
- // still reaped normally when its work actually finishes. The
470
- // open transition was audited + escalated exactly once.
471
- }
472
- else if (!this.killsEnabled) {
473
- if (streak === this.cfg.topicMovedConfirmTicks) {
474
- this.audit('would-reap', session, { rule: 'topic-moved-away', otherOwner, dryRun: true });
475
- }
476
- }
477
- else if (reapedThisTick < this.cfg.maxReapsPerTick && this.hourlyBudgetRemaining() > 0) {
478
- const res = await this.deps.terminate(session.id, reason);
479
- if (res.terminated) {
480
- reapedThisTick++;
481
- this.reapTimestamps.push(this.now());
482
- this.audit('reaped', session, { rule: 'topic-moved-away', otherOwner });
483
- this.topicMovedStreak.delete(session.id);
484
- this.topicMovedVetoes.delete(session.id);
485
- continue; // session is gone — skip the idle pipeline
486
- }
487
- // Guard veto / already-terminal — audit once per streak crossing,
488
- // keep the streak so next tick retries (bounded by the breaker).
489
- const v = vetoes + 1;
490
- this.topicMovedVetoes.set(session.id, v);
491
- if (streak === this.cfg.topicMovedConfirmTicks) {
492
- this.audit('reap-skipped-topic-moved', session, { rule: 'topic-moved-away', otherOwner, skipped: res.skipped });
493
- }
494
- if (v === this.cfg.topicMovedVetoBreakerAttempts) {
495
- // Breaker opens NOW: one audit row + one deduped escalation.
496
- this.audit('closeout-breaker-open', session, {
497
- rule: 'topic-moved-away', otherOwner, vetoedAttempts: v, lastSkipped: res.skipped ?? 'keep-guard',
498
- });
499
- const topicId = this.deps.topicBinding(session.tmuxSession);
500
- this.deps.raiseAttention?.({
501
- id: `closeout-breaker:${session.id}`,
502
- title: `Topic ${topicId ?? '?'} moved to ${otherOwner}, but the old session won't close`,
503
- summary: `Post-transfer closeout gave up after ${v} vetoed attempts (held by: ${res.skipped ?? 'keep-guard'}).`,
504
- description: `The conversation for topic ${topicId ?? '?'} now lives on ${otherOwner}, but the leftover session on this machine (${session.tmuxSession}) refused to close ${v} times in a row — a KEEP-guard reports it is still working (${res.skipped ?? 'keep-guard'}). Closeout retries have stopped (P19 breaker); the session will close via the normal idle path when it finishes, or you can close it from the dashboard.`,
505
- });
506
- }
507
- }
508
- }
509
- }
510
- else if ((this.topicMovedStreak.get(session.id) ?? 0) !== 0) {
511
- // Clears both a counting streak AND the -1 pin-conflict sentinel,
512
- // so a FUTURE genuine move starts its dwell from a clean slate.
513
- this.topicMovedStreak.set(session.id, 0);
514
- this.topicMovedVetoes.delete(session.id);
515
- }
710
+ if (this.cfg.topicMovedCloseout && (this.deps.topicOwnerElsewhere || this.deps.topicOwnerElsewhereInfo)) {
711
+ const outcome = this.cfg.closeoutLivenessGate
712
+ ? await this.runCloseoutGated(session, reapedThisTick)
713
+ : await this.runCloseoutLegacy(session, reapedThisTick);
714
+ reapedThisTick = outcome.reapedThisTick;
715
+ if (outcome.terminated)
716
+ continue; // session is gone skip the idle pipeline
516
717
  }
517
718
  // CPU-progress probe for the active-process keep-tightening. Sampled here
518
719
  // (once per session per tick) so the cross-tick delta lives in one place;