instar 1.3.500 → 1.3.501

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 (94) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +407 -6
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  5. package/dist/config/ConfigDefaults.js +10 -0
  6. package/dist/config/ConfigDefaults.js.map +1 -1
  7. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  8. package/dist/core/PostUpdateMigrator.js +31 -0
  9. package/dist/core/PostUpdateMigrator.js.map +1 -1
  10. package/dist/core/ReapGuard.d.ts +24 -0
  11. package/dist/core/ReapGuard.d.ts.map +1 -1
  12. package/dist/core/ReapGuard.js +62 -0
  13. package/dist/core/ReapGuard.js.map +1 -1
  14. package/dist/core/SessionManager.d.ts +32 -0
  15. package/dist/core/SessionManager.d.ts.map +1 -1
  16. package/dist/core/SessionManager.js +74 -2
  17. package/dist/core/SessionManager.js.map +1 -1
  18. package/dist/core/WorkEvidence.d.ts +45 -0
  19. package/dist/core/WorkEvidence.d.ts.map +1 -0
  20. package/dist/core/WorkEvidence.js +88 -0
  21. package/dist/core/WorkEvidence.js.map +1 -0
  22. package/dist/core/componentCategories.d.ts.map +1 -1
  23. package/dist/core/componentCategories.js +3 -0
  24. package/dist/core/componentCategories.js.map +1 -1
  25. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  26. package/dist/core/devGatedFeatures.js +5 -0
  27. package/dist/core/devGatedFeatures.js.map +1 -1
  28. package/dist/core/types.d.ts +50 -0
  29. package/dist/core/types.d.ts.map +1 -1
  30. package/dist/core/types.js.map +1 -1
  31. package/dist/messaging/NotificationBatcher.d.ts +12 -0
  32. package/dist/messaging/NotificationBatcher.d.ts.map +1 -1
  33. package/dist/messaging/NotificationBatcher.js +36 -0
  34. package/dist/messaging/NotificationBatcher.js.map +1 -1
  35. package/dist/messaging/pending-relay-store.d.ts +67 -8
  36. package/dist/messaging/pending-relay-store.d.ts.map +1 -1
  37. package/dist/messaging/pending-relay-store.js +151 -14
  38. package/dist/messaging/pending-relay-store.js.map +1 -1
  39. package/dist/messaging/reap-notice-delivery-id.d.ts +37 -0
  40. package/dist/messaging/reap-notice-delivery-id.d.ts.map +1 -0
  41. package/dist/messaging/reap-notice-delivery-id.js +52 -0
  42. package/dist/messaging/reap-notice-delivery-id.js.map +1 -0
  43. package/dist/monitoring/QuotaManager.d.ts +6 -0
  44. package/dist/monitoring/QuotaManager.d.ts.map +1 -1
  45. package/dist/monitoring/QuotaManager.js +11 -0
  46. package/dist/monitoring/QuotaManager.js.map +1 -1
  47. package/dist/monitoring/ReapLog.d.ts +39 -4
  48. package/dist/monitoring/ReapLog.d.ts.map +1 -1
  49. package/dist/monitoring/ReapLog.js +54 -4
  50. package/dist/monitoring/ReapLog.js.map +1 -1
  51. package/dist/monitoring/ReapNoticeDrain.d.ts +113 -0
  52. package/dist/monitoring/ReapNoticeDrain.d.ts.map +1 -0
  53. package/dist/monitoring/ReapNoticeDrain.js +231 -0
  54. package/dist/monitoring/ReapNoticeDrain.js.map +1 -0
  55. package/dist/monitoring/ReapNotifier.d.ts +89 -18
  56. package/dist/monitoring/ReapNotifier.d.ts.map +1 -1
  57. package/dist/monitoring/ReapNotifier.js +329 -22
  58. package/dist/monitoring/ReapNotifier.js.map +1 -1
  59. package/dist/monitoring/ResumeQueue.d.ts +223 -0
  60. package/dist/monitoring/ResumeQueue.d.ts.map +1 -0
  61. package/dist/monitoring/ResumeQueue.js +600 -0
  62. package/dist/monitoring/ResumeQueue.js.map +1 -0
  63. package/dist/monitoring/ResumeQueueDrainer.d.ts +151 -0
  64. package/dist/monitoring/ResumeQueueDrainer.d.ts.map +1 -0
  65. package/dist/monitoring/ResumeQueueDrainer.js +420 -0
  66. package/dist/monitoring/ResumeQueueDrainer.js.map +1 -0
  67. package/dist/monitoring/SessionMigrator.d.ts +10 -0
  68. package/dist/monitoring/SessionMigrator.d.ts.map +1 -1
  69. package/dist/monitoring/SessionMigrator.js +43 -1
  70. package/dist/monitoring/SessionMigrator.js.map +1 -1
  71. package/dist/monitoring/SessionReaper.d.ts +1 -0
  72. package/dist/monitoring/SessionReaper.d.ts.map +1 -1
  73. package/dist/monitoring/SessionReaper.js +5 -0
  74. package/dist/monitoring/SessionReaper.js.map +1 -1
  75. package/dist/monitoring/delivery-failure-sentinel.d.ts.map +1 -1
  76. package/dist/monitoring/delivery-failure-sentinel.js +7 -3
  77. package/dist/monitoring/delivery-failure-sentinel.js.map +1 -1
  78. package/dist/scaffold/templates.d.ts.map +1 -1
  79. package/dist/scaffold/templates.js +4 -3
  80. package/dist/scaffold/templates.js.map +1 -1
  81. package/dist/server/AgentServer.d.ts +5 -0
  82. package/dist/server/AgentServer.d.ts.map +1 -1
  83. package/dist/server/AgentServer.js +3 -0
  84. package/dist/server/AgentServer.js.map +1 -1
  85. package/dist/server/routes.d.ts +8 -0
  86. package/dist/server/routes.d.ts.map +1 -1
  87. package/dist/server/routes.js +94 -0
  88. package/dist/server/routes.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/data/builtin-manifest.json +65 -65
  91. package/src/data/state-coherence-registry.json +14 -1
  92. package/src/scaffold/templates.ts +4 -3
  93. package/upgrades/1.3.501.md +51 -0
  94. package/upgrades/side-effects/reap-notify-resume-queue.md +89 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAyG7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAmyCD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CAwVN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyyWtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAyG7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAyyCD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CAwVN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+rXtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -100,7 +100,7 @@ import { MessageProcessingLedger } from '../messaging/MessageProcessingLedger.js
100
100
  import { recoverStuckMessages } from '../messaging/stuckMessageRecovery.js';
101
101
  import { ReplyMarkerTransport } from '../core/ReplyMarkerTransport.js';
102
102
  import { decryptFromSync, encryptForSync } from '../core/SecretStore.js';
103
- import { createPrivateKey, createPublicKey } from 'node:crypto';
103
+ import { createPrivateKey, createPublicKey, createHash } from 'node:crypto';
104
104
  import { sign as signEd25519, verify as verifyEd25519 } from '../core/MachineIdentity.js';
105
105
  import { ProjectMapper } from '../core/ProjectMapper.js';
106
106
  import { CartographerTree } from '../core/CartographerTree.js';
@@ -516,7 +516,11 @@ async function spawnSessionForTopic(sessionManager, telegram, sessionName, topic
516
516
  /** Subscription & Auth Standard P1.3 (additive): launch under this account's
517
517
  * config home + record its id (the quota-aware account-swap mechanism).
518
518
  * Omitted = unchanged behaviour. */
519
- accountSwap) {
519
+ accountSwap,
520
+ /** Reap-notify spec R2.8 / L13 — explicit per-spawn working directory (the
521
+ * resume-queue drainer passes a queue entry's recorded cwd so interrupted
522
+ * worktree work resumes in ITS tree). Omitted = module project dir. */
523
+ spawnOpts) {
520
524
  const msg = latestMessage || 'Session started — send a message to continue.';
521
525
  // If memory is elevated/critical and we have the reaper, try to free memory
522
526
  // by cleaning orphans before spawning. Interactive sessions are NEVER blocked
@@ -787,6 +791,8 @@ accountSwap) {
787
791
  // this account's config home + record its id. Unset = unchanged.
788
792
  ...(accountSwap?.configHome ? { configHome: accountSwap.configHome } : {}),
789
793
  ...(accountSwap?.accountId ? { subscriptionAccountId: accountSwap.accountId } : {}),
794
+ // R2.8/L13: per-spawn cwd from the resume-queue entry. Unset = projectDir.
795
+ ...(spawnOpts?.cwd ? { cwd: spawnOpts.cwd } : {}),
790
796
  });
791
797
  // Clear the resume entry after successful spawn to prevent stale reuse
792
798
  if (resumeSessionId) {
@@ -5422,6 +5428,28 @@ export async function startServer(options) {
5422
5428
  const { ReapLog } = await import('../monitoring/ReapLog.js');
5423
5429
  const { ReapNotifier } = await import('../monitoring/ReapNotifier.js');
5424
5430
  const reapLog = new ReapLog(config.stateDir, () => (coordinator.enabled ? coordinator.identity?.machineId : undefined));
5431
+ // ── Durable notice lane (reap-notify spec R1.3) ──
5432
+ // The notifier enqueues notices as `reap-notify:<id>` rows in the shared
5433
+ // PendingRelayStore; the ALWAYS-ON ReapNoticeDrain (started after the
5434
+ // Telegram adapter exists, below) delivers them — independent of the
5435
+ // default-OFF DeliveryFailureSentinel. A store open failure degrades the
5436
+ // notifier to its loud direct-send fallback (R1.3 enqueue-failed path).
5437
+ let reapNoticeStore = null;
5438
+ try {
5439
+ const { PendingRelayStore, assertSqliteAvailable } = await import('../messaging/pending-relay-store.js');
5440
+ if (assertSqliteAvailable().ok) {
5441
+ reapNoticeStore = PendingRelayStore.open(config.projectName, config.stateDir);
5442
+ }
5443
+ }
5444
+ catch (err) {
5445
+ // @silent-fallback-ok — not silent: warns with full context here, and the
5446
+ // notifier's enqueue-failed path reports via DegradationReporter (R1.3).
5447
+ console.warn('[reap-notify] pending-relay store unavailable — notices fall back to direct send:', err);
5448
+ reapNoticeStore = null;
5449
+ }
5450
+ // Wired by the resume-queue boot block (Part B) once the queue exists;
5451
+ // stays false (no "restart queued" lines) until then or in dry-run.
5452
+ let resumeQueuedForSession = () => false;
5425
5453
  const reapNotifier = new ReapNotifier({
5426
5454
  resolveTopic: (tmuxSession) => {
5427
5455
  const t = telegram?.getTopicForSession(tmuxSession);
@@ -5431,14 +5459,353 @@ export async function startServer(options) {
5431
5459
  return Number.isFinite(n) ? n : null;
5432
5460
  },
5433
5461
  lifelineTopic: () => telegram?.getLifelineTopicId() ?? null,
5434
- // SUMMARY tier → through the formatter/tone-gate (HTML-escaped, quiet-hours
5435
- // aware). The notifier already coalesces, so a burst is one message.
5462
+ // Legacy + fallback transport → through the formatter/tone-gate
5463
+ // (HTML-escaped, quiet-hours aware). The notifier already coalesces.
5436
5464
  send: (topicId, text) => notify('SUMMARY', 'session-reap', text, topicId),
5465
+ enqueueNotice: reapNoticeStore
5466
+ ? (input) => reapNoticeStore.enqueue({
5467
+ delivery_id: input.delivery_id,
5468
+ topic_id: input.topic_id,
5469
+ text_hash: createHash('sha256').update(input.text).digest('hex'),
5470
+ text: input.text,
5471
+ next_attempt_at: input.next_attempt_at,
5472
+ })
5473
+ : undefined,
5474
+ recordNotify: (e) => reapLog.recordNotify(e),
5475
+ quietHoursEndAt: (now) => notificationBatcher.quietHoursEndAt(now),
5476
+ summaryReleaseAt: (now) => notificationBatcher.nextSummaryReleaseAt(now),
5477
+ resumeQueuedFor: (tmuxSession) => resumeQueuedForSession(tmuxSession),
5478
+ reportDegradation: (reason, impact) => {
5479
+ try {
5480
+ DegradationReporter.getInstance().report({
5481
+ feature: 'reap-notice-enqueue',
5482
+ primary: 'durable reap-notice delivery via PendingRelayStore + ReapNoticeDrain',
5483
+ fallback: 'one direct send attempt (fire-and-forget)',
5484
+ reason,
5485
+ impact,
5486
+ });
5487
+ }
5488
+ catch { /* @silent-fallback-ok — guard around the degradation REPORTER itself; the degradation it describes was already logged by the caller */ }
5489
+ },
5437
5490
  }, {
5438
5491
  enabled: config.monitoring?.reapNotify?.enabled ?? true,
5439
5492
  coalesceWindowMs: config.monitoring?.reapNotify?.coalesceWindowMs ?? 60_000,
5493
+ perTopic: config.monitoring?.reapNotify?.perTopic ?? true,
5494
+ maxImmediatePerFlush: config.monitoring?.reapNotify?.maxImmediatePerFlush ?? 5,
5495
+ drainEnabled: config.monitoring?.reapNotify?.drainEnabled ?? true,
5440
5496
  });
5497
+ // ReapNoticeDrain — the always-on delivery loop over the reap-notify lane
5498
+ // (R1.3; independent of the default-OFF DeliveryFailureSentinel). The
5499
+ // telegram adapter is referenced lazily: it does not exist yet at this
5500
+ // point in boot, and a tick with no adapter simply retries on backoff.
5501
+ let reapNoticeDrain = null;
5502
+ if (reapNoticeStore && (config.monitoring?.reapNotify?.drainEnabled ?? true)) {
5503
+ const { ReapNoticeDrain } = await import('../monitoring/ReapNoticeDrain.js');
5504
+ const { getCurrentBootId } = await import('../server/boot-id.js');
5505
+ reapNoticeDrain = new ReapNoticeDrain({
5506
+ store: reapNoticeStore,
5507
+ sendToTopic: async (topicId, text) => {
5508
+ if (!telegram)
5509
+ throw new Error('telegram adapter not available');
5510
+ await telegram.sendToTopic(topicId, text);
5511
+ },
5512
+ recordNotify: (e) => reapLog.recordNotify(e),
5513
+ emitAttention: async (item) => {
5514
+ if (!telegram)
5515
+ return;
5516
+ await telegram.createAttentionItem({
5517
+ id: item.id,
5518
+ title: item.title,
5519
+ summary: item.summary ?? item.title,
5520
+ description: item.description,
5521
+ category: item.category ?? 'delivery',
5522
+ priority: item.priority === 'high' ? 'HIGH' : item.priority === 'low' ? 'LOW' : 'NORMAL',
5523
+ sourceContext: item.sourceContext,
5524
+ });
5525
+ },
5526
+ bootId: getCurrentBootId() ?? `boot-${Date.now().toString(36)}`,
5527
+ });
5528
+ reapNoticeDrain.start();
5529
+ console.log(pc.green(' ReapNoticeDrain started (always-on durable reap-notice delivery)'));
5530
+ }
5441
5531
  sessionManager.setAwakeChecker(() => !coordinator.enabled || coordinator.isAwake);
5532
+ // Pressure-tier provider for the evidence fallback (reap-notify R2.1):
5533
+ // the SAME shared HostPressureSampler definition the reaper and the
5534
+ // resume-queue drainer read — one definition of "pressure", never two.
5535
+ const { sampleHostPressure: samplePressureShared } = await import('../monitoring/HostPressureSampler.js');
5536
+ const reaperPressureCfg = config.monitoring?.sessionReaper;
5537
+ const sharedPressureTier = () => samplePressureShared({
5538
+ cpuModerateLoadPerCore: reaperPressureCfg?.cpuModerateLoadPerCore ?? 1.0,
5539
+ cpuCriticalLoadPerCore: reaperPressureCfg?.cpuCriticalLoadPerCore ?? 1.5,
5540
+ }).tier;
5541
+ sessionManager.setPressureTierProvider(sharedPressureTier);
5542
+ // ── ResumeQueue + drainer (reap-notify spec Part B, R2.2–R2.11) ──
5543
+ // Ships enabled + dryRun (observe-only) as CODE defaults — deliberately
5544
+ // not in ConfigDefaults so the later fleet flip of the shipped default
5545
+ // takes effect. Classified in DARK_GATE_EXCLUSIONS (cost-bearing).
5546
+ const rqCfg = config.monitoring?.resumeQueue ?? {};
5547
+ let resumeQueue = null;
5548
+ let resumeDrainer = null;
5549
+ // Operator-stop record for the drainer's R2.6 validation (in-memory map +
5550
+ // the durable autonomous-emergency-stop flag file's mtime as global stop).
5551
+ const operatorStopsByTopic = new Map();
5552
+ let globalOperatorStopAt = 0;
5553
+ const recordOperatorStop = (topicId) => {
5554
+ if (topicId == null)
5555
+ globalOperatorStopAt = Date.now();
5556
+ else
5557
+ operatorStopsByTopic.set(topicId, Date.now());
5558
+ };
5559
+ if (rqCfg.enabled ?? true) {
5560
+ const { ResumeQueue } = await import('../monitoring/ResumeQueue.js');
5561
+ const { ResumeQueueDrainer } = await import('../monitoring/ResumeQueueDrainer.js');
5562
+ // Decision-transition audit sink: logs/resume-queue.jsonl, 5MB×2 rotation.
5563
+ const resumeAuditPath = path.join(_projectDir, 'logs', 'resume-queue.jsonl');
5564
+ const auditResumeQueue = (event) => {
5565
+ try {
5566
+ fs.mkdirSync(path.dirname(resumeAuditPath), { recursive: true });
5567
+ try {
5568
+ const st = fs.statSync(resumeAuditPath);
5569
+ if (st.size > 5 * 1024 * 1024) {
5570
+ fs.renameSync(resumeAuditPath, `${resumeAuditPath}.1`); // 5MB×2: .1 replaced each rotation
5571
+ }
5572
+ }
5573
+ catch { /* no file yet */ }
5574
+ fs.appendFileSync(resumeAuditPath, JSON.stringify(event) + '\n');
5575
+ }
5576
+ catch { /* the audit sink never endangers the queue */ }
5577
+ };
5578
+ // ALL give-up classes fold into ONE rolling deduped attention item (P17).
5579
+ const resumeAggregate = { counts: new Map(), recent: [] };
5580
+ const raiseResumeAggregated = (kind, detail) => {
5581
+ try {
5582
+ resumeAggregate.counts.set(kind, (resumeAggregate.counts.get(kind) ?? 0) + 1);
5583
+ resumeAggregate.recent.push(`[${kind}] ${detail}`);
5584
+ if (resumeAggregate.recent.length > 8)
5585
+ resumeAggregate.recent.shift();
5586
+ if (!telegram)
5587
+ return;
5588
+ const total = [...resumeAggregate.counts.values()].reduce((a, b) => a + b, 0);
5589
+ const breakdown = [...resumeAggregate.counts.entries()].map(([k, c]) => `${k}×${c}`).join(', ');
5590
+ void telegram.createAttentionItem({
5591
+ id: 'resume-queue:aggregate',
5592
+ title: `Resume queue: ${total} notice${total === 1 ? '' : 's'} (${breakdown})`,
5593
+ summary: detail,
5594
+ description: resumeAggregate.recent.join('\n'),
5595
+ category: 'sessions',
5596
+ priority: 'NORMAL', // per-entry HIGH items are forbidden (P17)
5597
+ sourceContext: 'resume-queue',
5598
+ }).catch(() => { });
5599
+ }
5600
+ catch { /* never endanger the caller */ }
5601
+ };
5602
+ resumeQueue = new ResumeQueue({
5603
+ stateDir: path.join(_projectDir, '.instar'),
5604
+ audit: auditResumeQueue,
5605
+ raiseAggregated: raiseResumeAggregated,
5606
+ }, {
5607
+ enabled: rqCfg.enabled ?? true,
5608
+ dryRun: rqCfg.dryRun ?? true, // shipped observe-only (decision 2)
5609
+ maxAttempts: rqCfg.maxAttempts ?? 3,
5610
+ maxResurrections: rqCfg.maxResurrections ?? 2,
5611
+ entryTtlHours: rqCfg.entryTtlHours ?? 24,
5612
+ maxQueueSize: rqCfg.maxQueueSize ?? 50,
5613
+ includeOperatorKills: rqCfg.includeOperatorKills ?? false,
5614
+ });
5615
+ const queueStarted = resumeQueue.start();
5616
+ if (!queueStarted) {
5617
+ console.log(pc.yellow(` ResumeQueue disabled: ${resumeQueue.isDisabled()}`));
5618
+ }
5619
+ else {
5620
+ // Feed the notifier's "restart is queued" line (live, non-dry-run only).
5621
+ const rq = resumeQueue;
5622
+ resumeQueuedForSession = (tmuxSession) => rq.hasLiveQueuedEntryFor(tmuxSession);
5623
+ const resolveTopicForTmux = (tmuxSession) => {
5624
+ try {
5625
+ const t = telegram?.getTopicForSession(tmuxSession);
5626
+ if (t == null)
5627
+ return null;
5628
+ const n = typeof t === 'number' ? t : Number(t);
5629
+ return Number.isFinite(n) ? n : null;
5630
+ }
5631
+ catch {
5632
+ // @silent-fallback-ok — null = "unbound": the notifier then routes
5633
+ // the session to the lifeline index line, never a dropped notice.
5634
+ return null;
5635
+ }
5636
+ };
5637
+ resumeDrainer = new ResumeQueueDrainer({
5638
+ queue: rq,
5639
+ pressureTier: sharedPressureTier,
5640
+ canSpawnSession: () => (quotaManager ? quotaManager.canSpawnSession().allowed : true),
5641
+ sessionCountOk: () => sessionManager.listRunningSessions().length <
5642
+ (config.maxSessions ?? 10),
5643
+ // No catch: a throwing dep resolves to the SAFE side (blocked)
5644
+ // inside the drainer's gate — wrapping it here would flip the
5645
+ // failure to the lenient side.
5646
+ migrationInFlight: () => quotaManager?.isMigrationInFlight() ?? false,
5647
+ liveSessionForTopic: (topicId) => sessionManager
5648
+ .listRunningSessions()
5649
+ .some((s) => resolveTopicForTmux(s.tmuxSession) === topicId),
5650
+ currentResumeUuid: (topicId) => _topicResumeMap?.get(topicId) ?? null,
5651
+ topicOwnerElsewhere: (topicId) => {
5652
+ // Pool not wired → single-machine → always local. No catch: a
5653
+ // registry error propagates to the drainer's validateReality,
5654
+ // which resolves a throwing dep to the SAFE side (invalidated).
5655
+ const reg = sessionOwnershipRegistry;
5656
+ const self = _meshSelfId;
5657
+ if (!reg || !self)
5658
+ return false;
5659
+ const owner = reg.ownerOf(String(topicId));
5660
+ return !!owner && owner !== self;
5661
+ },
5662
+ topicBindingMatches: (topicId, cwd) => {
5663
+ const bindings = scopeVerifier?.loadTopicBindings?.();
5664
+ const binding = bindings?.[String(topicId)];
5665
+ if (!binding?.projectDir)
5666
+ return true; // unbound topic → default project
5667
+ return path.resolve(cwd).startsWith(path.resolve(binding.projectDir));
5668
+ },
5669
+ operatorStopSince: (topicId, sinceIso) => {
5670
+ const since = Date.parse(sinceIso);
5671
+ const perTopic = operatorStopsByTopic.get(topicId) ?? 0;
5672
+ let flagAt = 0;
5673
+ try {
5674
+ flagAt = fs.statSync(path.join(_projectDir, '.instar', 'autonomous-emergency-stop')).mtimeMs;
5675
+ }
5676
+ catch { /* no flag */ }
5677
+ return Math.max(perTopic, globalOperatorStopAt, flagAt) > since;
5678
+ },
5679
+ jobCheck: (slug, queuedAtIso) => {
5680
+ if (!scheduler)
5681
+ return { ok: false, why: 'scheduler-unavailable' };
5682
+ const job = scheduler.getJobs().find((j) => j.slug === slug);
5683
+ if (!job)
5684
+ return { ok: false, why: 'job-missing' };
5685
+ // 'disabled' also covers CrashLoopPauser-paused jobs — the
5686
+ // pauser's mechanism IS setting enabled=false (+ provenance note).
5687
+ if (!job.enabled)
5688
+ return { ok: false, why: 'job-disabled' };
5689
+ const lastRun = state.getJobState(slug)?.lastRun;
5690
+ if (lastRun && Date.parse(lastRun) > Date.parse(queuedAtIso)) {
5691
+ return { ok: false, why: 'job-ran-since' };
5692
+ }
5693
+ return { ok: true };
5694
+ },
5695
+ pathExists: (p) => fs.existsSync(p),
5696
+ respawnTopic: async (entry, continuationPrompt) => {
5697
+ if (!telegram)
5698
+ throw new Error('telegram adapter not available');
5699
+ return await spawnSessionForTopic(sessionManager, telegram, entry.sessionName, entry.topicId, continuationPrompt, topicMemory, undefined, undefined, undefined, { cwd: entry.worktreePath ?? entry.cwd });
5700
+ },
5701
+ triggerJob: async (slug) => {
5702
+ if (!scheduler)
5703
+ return 'skipped';
5704
+ return await scheduler.triggerJob(slug, 'resume-queue');
5705
+ },
5706
+ spawnAliveAfterGrace: async (tmuxSession) => {
5707
+ await new Promise((resolve) => {
5708
+ const t = setTimeout(resolve, 15_000);
5709
+ if (typeof t.unref === 'function')
5710
+ t.unref();
5711
+ });
5712
+ return sessionManager.isSessionAlive(tmuxSession);
5713
+ },
5714
+ notifyResumed: (entry) => {
5715
+ // R2.11 — honest wording: "restarted", never a transcript-resume
5716
+ // claim (--resume can fall back to a fresh conversation in-pane).
5717
+ if (entry.topicId == null)
5718
+ return;
5719
+ notify('SUMMARY', 'session-resume', `🔁 I restarted this session to pick the work back up after it was shut down mid-work.`, entry.topicId);
5720
+ },
5721
+ raiseAggregated: raiseResumeAggregated,
5722
+ audit: auditResumeQueue,
5723
+ tier1Check: async (entry) => {
5724
+ // Observe-only Tier 1 sanity check via the shared LlmQueue
5725
+ // (P7). Throws when the LLM substrate is unavailable — the
5726
+ // drainer audits that as supervision:'shed'.
5727
+ const q = sharedLlmQueue;
5728
+ const intel = _sharedIntelligence;
5729
+ if (!q || !intel)
5730
+ throw new Error('llm-unavailable');
5731
+ const reasonLiteral = entry.reason.slice(0, 200).replace(/`/g, "'");
5732
+ const prompt = `A session was shut down mid-work and is queued for automatic restart. Given ONLY these ` +
5733
+ `recorded fields, is restarting it sensible? Look for internal contradictions (a "mid-work" ` +
5734
+ `entry whose reason describes completed work; a resurrection history that reads as a crash loop).\n` +
5735
+ `Recorded reason (literal data): \`${reasonLiteral}\`\n` +
5736
+ `Work signals: ${entry.workEvidence.join(', ') || '(none)'}\n` +
5737
+ `Queued: ${entry.queuedAt}; attempts so far: ${entry.attempts}.\n` +
5738
+ `Reply with JSON only: {"sensible": true|false, "reasoning": "<one sentence>"}`;
5739
+ const raw = await q.enqueue('background', (signal) => intel.evaluate(prompt, {
5740
+ model: 'fast',
5741
+ maxTokens: 150,
5742
+ temperature: 0,
5743
+ signal,
5744
+ attribution: { component: 'ResumeQueueDrainer' }, // attribution for /metrics/features
5745
+ }));
5746
+ try {
5747
+ const t = String(raw).trim();
5748
+ const j = t.startsWith('```') ? t.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '') : t;
5749
+ const parsed = JSON.parse(j);
5750
+ return { sensible: parsed.sensible !== false, reasoning: parsed.reasoning };
5751
+ }
5752
+ catch {
5753
+ return { sensible: true, reasoning: 'unparseable verdict — treated as no-concern (observe-only)' };
5754
+ }
5755
+ },
5756
+ }, {
5757
+ drainIntervalSec: rqCfg.drainIntervalSec ?? 60,
5758
+ requiredCalmTicks: rqCfg.requiredCalmTicks ?? 3,
5759
+ maxAttempts: rqCfg.maxAttempts ?? 3,
5760
+ breakerThreshold: rqCfg.breakerThreshold ?? 3,
5761
+ breakerCooldownMin: rqCfg.breakerCooldownMin ?? 30,
5762
+ tier1Check: rqCfg.tier1Check ?? true,
5763
+ });
5764
+ resumeDrainer.start();
5765
+ console.log(pc.green(` ResumeQueue started (${rqCfg.dryRun ?? true ? 'dry-run observe-only' : 'LIVE'}; drainer ${rqCfg.drainIntervalSec ?? 60}s tick)`));
5766
+ // Boot reconciliation half 2 (R2.4): re-enqueue recent mid-work reaps
5767
+ // the queue lost to a crash window. Deferred 30s so the Telegram
5768
+ // adapter (topic resolution) exists; topic-bound candidates only —
5769
+ // job entries rely on cron recurrence and opt-in we cannot
5770
+ // reconstruct from the reap-log.
5771
+ const reconcileTimer = setTimeout(() => {
5772
+ try {
5773
+ const ttlMs = (rqCfg.entryTtlHours ?? 24) * 3600_000;
5774
+ const cutoff = Date.now() - ttlMs;
5775
+ const candidates = reapLog
5776
+ .read(1000)
5777
+ .filter((en) => en.type === 'reaped' &&
5778
+ en.midWork === true &&
5779
+ en.disposition === 'terminal' &&
5780
+ en.origin === 'autonomous' &&
5781
+ Date.parse(en.ts) > cutoff)
5782
+ .map((en) => {
5783
+ const topicId = resolveTopicForTmux(en.tmuxSession);
5784
+ return {
5785
+ sessionName: en.session,
5786
+ tmuxSession: en.tmuxSession,
5787
+ topicId,
5788
+ resumeUuid: topicId != null ? (_topicResumeMap?.get(topicId) ?? null) : null,
5789
+ cwd: _projectDir,
5790
+ reason: en.reason,
5791
+ disposition: 'terminal',
5792
+ origin: 'autonomous',
5793
+ workEvidence: en.workEvidence ?? [],
5794
+ };
5795
+ });
5796
+ const enqueued = rq.reconcileFromReapLog(candidates);
5797
+ if (enqueued > 0) {
5798
+ console.log(`[resume-queue] boot reconciliation re-enqueued ${enqueued} lost mid-work reap(s) from the reap-log`);
5799
+ }
5800
+ }
5801
+ catch (err) {
5802
+ console.warn('[resume-queue] boot reconciliation failed (non-fatal):', err);
5803
+ }
5804
+ }, 30_000);
5805
+ if (typeof reconcileTimer.unref === 'function')
5806
+ reconcileTimer.unref();
5807
+ }
5808
+ }
5442
5809
  sessionManager.on('sessionReaped', (e) => {
5443
5810
  reapLog.recordReaped({
5444
5811
  session: e.session.name,
@@ -5453,8 +5820,42 @@ export async function startServer(options) {
5453
5820
  // which billing lane the reaped session ran on so the soak can confirm
5454
5821
  // rerouted sessions reach their completion from the reap-log too.
5455
5822
  ...(e.session.launchLane ? { launchLane: e.session.launchLane } : {}),
5823
+ // Mid-work stamp (reap-notify spec R2.1) — evidence clamped at the
5824
+ // chokepoint; the reap-log row is the boot-reconciliation source of truth.
5825
+ ...(e.midWork !== undefined ? { midWork: e.midWork } : {}),
5826
+ ...(e.workEvidence && e.workEvidence.length > 0 ? { workEvidence: e.workEvidence } : {}),
5456
5827
  });
5457
- reapNotifier.onReaped({ session: e.session, reason: e.reason, disposition: e.disposition, origin: e.origin });
5828
+ // Enqueue hook (reap-notify R2.2): every terminal autonomous reap is
5829
+ // OFFERED to the resume queue; eligibility (evidence classes, job
5830
+ // opt-in, operator exclusion, resurrection cap) is decided inside.
5831
+ // Runs BEFORE the notifier so the "restart is queued" line can see the
5832
+ // fresh entry. Never endangers the kill path.
5833
+ try {
5834
+ if (resumeQueue && !resumeQueue.isDisabled()) {
5835
+ const rawTopic = telegram?.getTopicForSession(e.session.tmuxSession);
5836
+ const topicId = rawTopic == null ? null : Number.isFinite(Number(rawTopic)) ? Number(rawTopic) : null;
5837
+ const jobDef = e.session.jobSlug
5838
+ ? scheduler?.getJobs().find((j) => j.slug === e.session.jobSlug)
5839
+ : undefined;
5840
+ resumeQueue.considerEnqueue({
5841
+ sessionName: e.session.name,
5842
+ tmuxSession: e.session.tmuxSession,
5843
+ topicId,
5844
+ jobSlug: e.session.jobSlug,
5845
+ jobResumeOptIn: jobDef?.resumeOnReap === true,
5846
+ resumeUuid: topicId != null ? (_topicResumeMap?.get(topicId) ?? null) : null,
5847
+ cwd: e.session.cwd ?? _projectDir,
5848
+ reason: e.reason,
5849
+ disposition: e.disposition ?? 'terminal',
5850
+ origin: e.origin ?? 'autonomous',
5851
+ workEvidence: e.workEvidence ?? [],
5852
+ });
5853
+ }
5854
+ }
5855
+ catch (err) {
5856
+ console.warn('[resume-queue] enqueue hook raised (non-fatal):', err);
5857
+ }
5858
+ reapNotifier.onReaped({ session: e.session, reason: e.reason, disposition: e.disposition, origin: e.origin, midWork: e.midWork, workEvidence: e.workEvidence });
5458
5859
  // Coherence journal 'reaped' (§3.3): emitted HERE, alongside the
5459
5860
  // reap-log append it references — never derived in the saveSession
5460
5861
  // funnel (which records the plain killed/completed transition).
@@ -12763,7 +13164,7 @@ export async function startServer(options) {
12763
13164
  carrier: _topicProfileCarrier,
12764
13165
  }
12765
13166
  : null;
12766
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
13167
+ const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, resumeQueue, resumeDrainer, operatorStopRecorder: recordOperatorStop, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
12767
13168
  // Resolve the late-bound topic-operator getter (increment 2e): routing was
12768
13169
  // wired before the server existed; from here on inbound binds use the
12769
13170
  // server's own store instance.