instar 1.3.586 → 1.3.588

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 (45) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +229 -39
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/IntelligenceRouter.d.ts +10 -0
  5. package/dist/core/IntelligenceRouter.d.ts.map +1 -1
  6. package/dist/core/IntelligenceRouter.js +37 -3
  7. package/dist/core/IntelligenceRouter.js.map +1 -1
  8. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  9. package/dist/core/PostUpdateMigrator.js +17 -0
  10. package/dist/core/PostUpdateMigrator.js.map +1 -1
  11. package/dist/core/WorkEvidence.d.ts +17 -0
  12. package/dist/core/WorkEvidence.d.ts.map +1 -1
  13. package/dist/core/WorkEvidence.js +17 -0
  14. package/dist/core/WorkEvidence.js.map +1 -1
  15. package/dist/core/gapBCommitmentEvidence.d.ts +143 -0
  16. package/dist/core/gapBCommitmentEvidence.d.ts.map +1 -0
  17. package/dist/core/gapBCommitmentEvidence.js +138 -0
  18. package/dist/core/gapBCommitmentEvidence.js.map +1 -0
  19. package/dist/core/internalFrameworkDefault.d.ts +50 -0
  20. package/dist/core/internalFrameworkDefault.d.ts.map +1 -0
  21. package/dist/core/internalFrameworkDefault.js +72 -0
  22. package/dist/core/internalFrameworkDefault.js.map +1 -0
  23. package/dist/core/types.d.ts +39 -0
  24. package/dist/core/types.d.ts.map +1 -1
  25. package/dist/core/types.js.map +1 -1
  26. package/dist/monitoring/CommitmentTracker.d.ts +12 -0
  27. package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
  28. package/dist/monitoring/CommitmentTracker.js +14 -0
  29. package/dist/monitoring/CommitmentTracker.js.map +1 -1
  30. package/dist/monitoring/ReapLog.d.ts +11 -0
  31. package/dist/monitoring/ReapLog.d.ts.map +1 -1
  32. package/dist/monitoring/ReapLog.js +1 -0
  33. package/dist/monitoring/ReapLog.js.map +1 -1
  34. package/dist/monitoring/ResumeQueueDrainer.d.ts +15 -0
  35. package/dist/monitoring/ResumeQueueDrainer.d.ts.map +1 -1
  36. package/dist/monitoring/ResumeQueueDrainer.js +12 -1
  37. package/dist/monitoring/ResumeQueueDrainer.js.map +1 -1
  38. package/dist/scaffold/templates.js +4 -4
  39. package/package.json +1 -1
  40. package/src/data/builtin-manifest.json +19 -19
  41. package/src/scaffold/templates.ts +4 -4
  42. package/upgrades/1.3.587.md +28 -0
  43. package/upgrades/1.3.588.md +27 -0
  44. package/upgrades/side-effects/autonomous-registration-guarantee.md +51 -0
  45. package/upgrades/side-effects/provider-fallback-default-policy.md +52 -0
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkCH,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;AAkH7D,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;AAg4CD,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,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA20ctE;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;AAmCH,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;AAkH7D,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;AAg4CD,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,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAshdtE;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"}
@@ -24,7 +24,8 @@ import { CodexResumeMap } from '../core/CodexResumeMap.js';
24
24
  import { paneIdleWithEmptyInput } from '../core/ModelSwapService.js';
25
25
  import { escalatedModelIds, normalizeTierEscalationConfig } from '../core/ModelTierEscalation.js';
26
26
  import { activeAutonomousJobs, autonomousRunRemainingForTopic } from '../core/AutonomousSessions.js';
27
- import { AGE_LIMIT_ACTIVE_RUN_REASON } from '../core/WorkEvidence.js';
27
+ import { AGE_LIMIT_ACTIVE_RUN_REASON, COMMITMENT_ACTIVE_RUN_REASON } from '../core/WorkEvidence.js';
28
+ import { gapBEligibleForTopic, recentUserMessageFromHistory, resolveGapBInjectionGate, decideGapBInjection } from '../core/gapBCommitmentEvidence.js';
28
29
  import { TopicProfileTransferCarrier, createTopicProfilePullHandler } from '../core/TopicProfileTransferCarrier.js';
29
30
  import { closeAllSqlite } from '../core/SqliteRegistry.js';
30
31
  import { SessionManager } from '../core/SessionManager.js';
@@ -4372,43 +4373,96 @@ export async function startServer(options) {
4372
4373
  const { IntelligenceRouter } = await import('../core/IntelligenceRouter.js');
4373
4374
  const { buildIntelligenceProvider: buildIP } = await import('../core/intelligenceProviderFactory.js');
4374
4375
  const { LlmCircuitBreaker } = await import('../core/LlmCircuitBreaker.js');
4376
+ const { INTERNAL_FRAMEWORK_PREFERENCE, resolveInternalFrameworkDefault, } = await import('../core/internalFrameworkDefault.js');
4375
4377
  const defaultFw = resolvedFramework;
4376
4378
  const rawDefault = sharedIntelligence;
4379
+ // Build a non-default framework's provider (its OWN breaker → a Claude trip
4380
+ // can't pause Codex). claude-code builds inherit subscription-path routing so a
4381
+ // component routed to Claude under a codex default still honors June-15 routing.
4382
+ const buildFrameworkProvider = (fw) => buildIP({
4383
+ framework: fw,
4384
+ breaker: new LlmCircuitBreaker(),
4385
+ // The per-component routing path is the one the cartographer sweep actually
4386
+ // uses — the kill-switch must reach it too.
4387
+ ...(fw === 'codex-cli' && resolveCodexExecJson ? { resolveExecJson: resolveCodexExecJson } : {}),
4388
+ ...(fw === 'claude-code' && subscriptionPathOption
4389
+ ? { subscriptionPath: subscriptionPathOption }
4390
+ : {}),
4391
+ // pi-cli routing (PI-HARNESS-INTEGRATION-SPEC §4.4): thread the configured
4392
+ // model pattern + the explicit Anthropic override. Absent pattern ⇒ the
4393
+ // factory degrades to null (guarded by design).
4394
+ ...(fw === 'pi-cli'
4395
+ ? {
4396
+ ...(config.sessions?.frameworkDefaultModels?.['pi-cli']
4397
+ ? { piModel: config.sessions.frameworkDefaultModels['pi-cli'] }
4398
+ : {}),
4399
+ ...(config.sessions?.piCliAllowAnthropicProviders !== undefined
4400
+ ? { piAllowAnthropicProviders: config.sessions.piCliAllowAnthropicProviders }
4401
+ : {}),
4402
+ }
4403
+ : {}),
4404
+ });
4405
+ const probeCache = new Map();
4406
+ const cachedBuild = (fw) => {
4407
+ if (probeCache.has(fw))
4408
+ return probeCache.get(fw);
4409
+ let p = null;
4410
+ try {
4411
+ p = buildFrameworkProvider(fw);
4412
+ }
4413
+ catch {
4414
+ p = null;
4415
+ }
4416
+ probeCache.set(fw, p);
4417
+ return p;
4418
+ };
4419
+ // §4.2 active-provider filtering: the active-set is the preference chain
4420
+ // filtered to frameworks whose provider genuinely BUILDS on this machine
4421
+ // (`buildProvider(fw) !== null` — the router's own truth, not a bare `which`).
4422
+ // claude-code is the default framework (always "active" — buildIP not needed),
4423
+ // so it's never dropped from the tail; off-default frameworks are probed.
4424
+ // Memoize ONLY this SET (§4.6 — never a frozen config object). Machine-local
4425
+ // by design (§5): each machine probes its own installed CLIs.
4426
+ const activeFrameworkSet = INTERNAL_FRAMEWORK_PREFERENCE.filter((fw) => fw === defaultFw || cachedBuild(fw) !== null);
4427
+ // §4.4 boot snapshot — load-bearing & mutation-proof: capture WHETHER the
4428
+ // operator set componentFrameworks ONCE here, at construction, BEFORE the only
4429
+ // runtime mutator (CartographerSweep auto-vivify, ~server.ts:11266). If we
4430
+ // tested the live object later, we'd mistake the auto-vivified block for an
4431
+ // operator override and silently disable the default policy (round-1 M5).
4432
+ const operatorSetComponentFrameworks = config.sessions?.componentFrameworks !== undefined;
4433
+ const computedDefault = resolveInternalFrameworkDefault(activeFrameworkSet);
4377
4434
  sharedIntelligence = new IntelligenceRouter({
4378
4435
  defaultProvider: rawDefault,
4379
4436
  defaultFramework: defaultFw,
4380
- // Live read: each call sees the current config object's componentFrameworks
4381
- // (a config edit that mutates the in-memory object is hot; a file reload is
4382
- // out of B1 scope same semantics as the rest of instar's config).
4383
- resolveConfig: () => config.sessions?.componentFrameworks,
4384
- // Each non-default framework gets its OWN breaker → a Claude trip can't
4385
- // pause Codex (the whole point). Default framework keeps the shared one.
4386
- // claude-code builds inherit the subscription-path routing so a
4387
- // component routed to Claude under a codex default still honors
4388
- // the June-15 SDK-pot-vs-subscription decision.
4389
- buildProvider: (fw) => buildIP({
4390
- framework: fw,
4391
- breaker: new LlmCircuitBreaker(),
4392
- // The per-component routing path is the one the cartographer
4393
- // sweep actually uses — the kill-switch must reach it too.
4394
- ...(fw === 'codex-cli' && resolveCodexExecJson ? { resolveExecJson: resolveCodexExecJson } : {}),
4395
- ...(fw === 'claude-code' && subscriptionPathOption
4396
- ? { subscriptionPath: subscriptionPathOption }
4397
- : {}),
4398
- // pi-cli routing (PI-HARNESS-INTEGRATION-SPEC §4.4): thread the
4399
- // configured model pattern + the explicit Anthropic override.
4400
- // Absent pattern the factory degrades to null (guarded by design).
4401
- ...(fw === 'pi-cli'
4402
- ? {
4403
- ...(config.sessions?.frameworkDefaultModels?.['pi-cli']
4404
- ? { piModel: config.sessions.frameworkDefaultModels['pi-cli'] }
4405
- : {}),
4406
- ...(config.sessions?.piCliAllowAnthropicProviders !== undefined
4407
- ? { piAllowAnthropicProviders: config.sessions.piCliAllowAnthropicProviders }
4408
- : {}),
4409
- }
4410
- : {}),
4411
- }),
4437
+ // §4.6 live-read, layered resolveConfig:
4438
+ // - operator-set at boot return the LIVE operator block unchanged (live
4439
+ // edits to its contents flow through; today's behavior).
4440
+ // - else the computed default LAYERED UNDER any live in-memory override
4441
+ // (e.g. CartographerSweep's runtime-injected categories/overrides WIN for
4442
+ // their slot this layering is what stops a frozen default from silently
4443
+ // disabling the freshness sweep, round-3 R3-2). Reads live every call;
4444
+ // memoizes only the active-SET, never a frozen config object.
4445
+ resolveConfig: () => {
4446
+ const live = config.sessions?.componentFrameworks;
4447
+ if (operatorSetComponentFrameworks)
4448
+ return live;
4449
+ if (!live)
4450
+ return computedDefault;
4451
+ // Layer the live in-memory override OVER the computed default.
4452
+ return {
4453
+ ...computedDefault,
4454
+ ...live,
4455
+ categories: { ...computedDefault.categories, ...live.categories },
4456
+ ...(live.overrides ? { overrides: live.overrides } : {}),
4457
+ ...(live.failureSwap !== undefined ? { failureSwap: live.failureSwap } : {}),
4458
+ };
4459
+ },
4460
+ // §4.5: per-attempt swap timeout, inline-defaulted to 5s (no ConfigDefaults
4461
+ // entry codexExecJson precedent; absent ⇒ 5s, present ⇒ operator's value).
4462
+ swapAttemptTimeoutMs: config.intelligence?.swapAttemptTimeoutMs ?? 5000,
4463
+ // Each non-default framework gets its OWN breaker (built once, from the
4464
+ // shared probe-cache so the active-set probe doesn't double-build).
4465
+ buildProvider: (fw) => cachedBuild(fw),
4412
4466
  onDegrade: (info) => {
4413
4467
  try {
4414
4468
  DegradationReporter.getInstance().report({
@@ -6324,6 +6378,56 @@ export async function startServer(options) {
6324
6378
  const rqCfg = config.monitoring?.resumeQueue ?? {};
6325
6379
  let resumeQueue = null;
6326
6380
  let resumeDrainer = null;
6381
+ // GAP-B Part A surface (spec: autonomous-registration-guarantee.md) — the
6382
+ // aggregated-attention chokepoint reference, hoisted to the handler's scope.
6383
+ // Assigned inside the `if (rqCfg.enabled)` block (where raiseResumeAggregated
6384
+ // is built); null otherwise ⇒ Part A is a silent no-op (never blocks).
6385
+ let gapBRaiseAggregated = null;
6386
+ // GAP-B Part B (the commitment-evidence backstop, ships DARK). The gate is
6387
+ // armed ONLY by an explicit `monitoring.resumeQueue.commitmentEvidence.enabled
6388
+ // === true` (omitted ⇒ OFF on fleet AND dev — the containment: no injection ⇒
6389
+ // no revival ⇒ the 2026-06-13 loop is structurally impossible while dark).
6390
+ const gapBCfg = rqCfg.commitmentEvidence ?? {};
6391
+ const gapBGate = resolveGapBInjectionGate(gapBCfg);
6392
+ const gapBFreshWindowMs = gapBCfg.freshCommitmentWindowMs ?? 6 * 60 * 60_000; // D1: 6h
6393
+ // D8 window — reuse SessionReaper's staleCommitmentWindowMinutes (480/8h,
6394
+ // ConfigDefaults) so the GAP-B recent-user-message corroboration uses the SAME
6395
+ // horizon as ReapGuard's commitment-coupled KEEP (no new ConfigDefaults entry).
6396
+ const gapBStaleWindowMs = (config.monitoring?.sessionReaper?.staleCommitmentWindowMinutes ?? 480) * 60_000;
6397
+ // Part D (spec: autonomous-registration-guarantee.md) — the SHARED inbound-
6398
+ // user-message recency predicate. ONE definition, used by BOTH the GAP-B
6399
+ // eligibility helper (D8) below AND the ReapGuard/SessionReaper deps further
6400
+ // down, so a KEEP decision and the revival eligibility are computed from the
6401
+ // identical truth and CANNOT disagree (the loop the agreement prevents).
6402
+ // Store: TelegramAdapter.getTopicHistory (sync in-memory LogEntry tail cache —
6403
+ // NOT the Threadline A2A MessageStore). Inbound user = LogEntry.fromUser ===
6404
+ // true; freshness = its `timestamp` within `withinMs`. Fail-open (D7): a throw
6405
+ // ⇒ false (no KEEP, no inject) — never endangers the kill path.
6406
+ const recentUserMessageShared = (topicId, withinMs) => {
6407
+ try {
6408
+ if (!telegram)
6409
+ return false;
6410
+ // 50-entry tail is within getTopicHistory's TAIL_CACHE_LIMIT fast path
6411
+ // (sync cache read, no file scan for live topics).
6412
+ return recentUserMessageFromHistory(telegram.getTopicHistory(topicId, 50), withinMs);
6413
+ }
6414
+ catch {
6415
+ // @silent-fallback-ok — Part D D7: fail toward today's behavior.
6416
+ return false;
6417
+ }
6418
+ };
6419
+ // GAP-B Part B + D9 — the SHARED qualifying-commitment + D8 agreement verdict,
6420
+ // ONE closure used by BOTH the reaped-session injection (enqueue time) AND the
6421
+ // drainer's drain-time re-validation (`commitmentStillActiveForTopic`), so the
6422
+ // two cannot disagree on "what a qualifying open commitment means". Delegates
6423
+ // to the pure, unit-tested predicate; reads THIS machine's local tracker.
6424
+ // Fail-open (D7): handled inside gapBEligibleForTopic (a throw ⇒ false).
6425
+ const gapBCommitmentQualifies = (topicId) => gapBEligibleForTopic(topicId, commitmentTracker, {
6426
+ ownMachineId: cjOwnMachineId,
6427
+ freshCommitmentWindowMs: gapBFreshWindowMs,
6428
+ staleCommitmentWindowMs: gapBStaleWindowMs,
6429
+ recentUserMessage: recentUserMessageShared,
6430
+ });
6327
6431
  // Operator-stop record for the drainer's R2.6 validation (in-memory map +
6328
6432
  // the durable autonomous-emergency-stop flag file's mtime as global stop).
6329
6433
  const operatorStopsByTopic = new Map();
@@ -6377,6 +6481,10 @@ export async function startServer(options) {
6377
6481
  }
6378
6482
  catch { /* never endanger the caller */ }
6379
6483
  };
6484
+ // GAP-B Part A: route the unregistered-autonomous surface through the SAME
6485
+ // aggregated chokepoint (P17 — the bound lives at the creation chokepoint so
6486
+ // a unique key can't dodge it). Hoist the reference to the handler's scope.
6487
+ gapBRaiseAggregated = raiseResumeAggregated;
6380
6488
  resumeQueue = new ResumeQueue({
6381
6489
  stateDir: path.join(_projectDir, '.instar'),
6382
6490
  audit: auditResumeQueue,
@@ -6482,6 +6590,13 @@ export async function startServer(options) {
6482
6590
  // admitted the entry). A throw resolves to NOT-finished inside the drainer's
6483
6591
  // safeBool (SAFE side — the revival still passes the other reality gates).
6484
6592
  autonomousRunFinished: (topicId) => autonomousRunRemainingForTopic(config.stateDir, topicId) == null,
6593
+ // GAP-B D9: the parallel drain-time re-check for a COMMITMENT-admitted
6594
+ // entry. Re-runs the SAME qualifying + D8 agreement check used at
6595
+ // enqueue; `true` ⇒ still active (spawn proceeds), `false` ⇒ the
6596
+ // commitment closed or user-activity lapsed since enqueue (the drainer
6597
+ // invalidates `commitment-no-longer-active`, never a spawn). A throw
6598
+ // resolves SAFE (still-active) inside the drainer's safeBool.
6599
+ commitmentStillActiveForTopic: (topicId) => gapBCommitmentQualifies(topicId),
6485
6600
  jobCheck: (slug, queuedAtIso) => {
6486
6601
  if (!scheduler)
6487
6602
  return { ok: false, why: 'scheduler-unavailable' };
@@ -6647,6 +6762,46 @@ export async function startServer(options) {
6647
6762
  }
6648
6763
  }
6649
6764
  sessionManager.on('sessionReaped', (e) => {
6765
+ // ── GAP-B Part B (spec: autonomous-registration-guarantee.md) — the
6766
+ // commitment-evidence BACKSTOP for an UNregistered-but-working autonomous
6767
+ // run. Computed ONCE here (cheap, fail-open) so its verdict feeds BOTH the
6768
+ // reap-log `evidenceSource` tag and the enqueue-time injection below.
6769
+ // Pinned to the SAME branch the state-file source uses: an `age-limit`
6770
+ // reap whose per-topic state file is ABSENT (a registered run is handled
6771
+ // by the AGE_LIMIT_ACTIVE_RUN_REASON path; this is the gap it can't see).
6772
+ // Ships DARK (`gapBGate.armed`) — when off it is a strict no-op. ──
6773
+ let gapBDecision = { fired: false, inject: false };
6774
+ try {
6775
+ if (gapBGate.armed && e.reason === 'age-limit') {
6776
+ // Only resolve topic + the cost-bearing eligibility on the armed
6777
+ // age-limit branch (every other reap pays zero added cost).
6778
+ const rawTopicG = telegram?.getTopicForSession(e.session.tmuxSession);
6779
+ const topicG = rawTopicG == null ? null : Number.isFinite(Number(rawTopicG)) ? Number(rawTopicG) : null;
6780
+ if (topicG != null) {
6781
+ gapBDecision = decideGapBInjection({
6782
+ gate: gapBGate,
6783
+ reason: e.reason,
6784
+ // State-file PRESENT ⇒ the registered-run path covers it upstream;
6785
+ // GAP-B fires only when ABSENT.
6786
+ stateFilePresent: autonomousRunRemainingForTopic(config.stateDir, topicG) != null,
6787
+ // D1/D2 qualifying set + D8 recent-user-message agreement (shared with
6788
+ // the drainer's drain-time re-check so the two cannot disagree).
6789
+ eligible: gapBCommitmentQualifies(topicG),
6790
+ });
6791
+ }
6792
+ }
6793
+ }
6794
+ catch (err) {
6795
+ // @silent-fallback-ok — D7 fail-open: any read throwing ⇒ no injection,
6796
+ // the reap proceeds exactly as today. Never endangers the kill path.
6797
+ gapBDecision = { fired: false, inject: false };
6798
+ console.warn('[gap-b] commitment-evidence probe raised (non-fatal):', err);
6799
+ }
6800
+ const gapBCommitmentFired = gapBDecision.fired;
6801
+ // dryRun (default when armed): the verdict is logged but the candidate is
6802
+ // NOT tagged — the dark-soak proving KEEP/eligibility agree before evidence
6803
+ // actually flows. `gapBInject` is the LIVE-injection switch.
6804
+ const gapBInject = gapBDecision.inject;
6650
6805
  reapLog.recordReaped({
6651
6806
  session: e.session.name,
6652
6807
  tmuxSession: e.session.tmuxSession,
@@ -6664,7 +6819,24 @@ export async function startServer(options) {
6664
6819
  // chokepoint; the reap-log row is the boot-reconciliation source of truth.
6665
6820
  ...(e.midWork !== undefined ? { midWork: e.midWork } : {}),
6666
6821
  ...(e.workEvidence && e.workEvidence.length > 0 ? { workEvidence: e.workEvidence } : {}),
6822
+ // GAP-B D3: tag the source only when the LIVE injection fired (dryRun
6823
+ // logs but does not tag — the row must reflect what actually drove
6824
+ // eligibility). No PII: the field carries only the source token.
6825
+ ...(gapBInject ? { evidenceSource: 'commitment' } : {}),
6667
6826
  });
6827
+ if (gapBCommitmentFired) {
6828
+ // Part A (observe-only): surface the unregistered-but-committed run through
6829
+ // the SAME aggregated chokepoint (P17), deduped per topic. Never blocks.
6830
+ // Fires on the VERDICT (even in dryRun) — the surface exists to make the
6831
+ // invisible visible so the run gets registered, independent of injection.
6832
+ try {
6833
+ const rawTopicA = telegram?.getTopicForSession(e.session.tmuxSession);
6834
+ const topicA = rawTopicA == null ? null : Number.isFinite(Number(rawTopicA)) ? Number(rawTopicA) : null;
6835
+ gapBRaiseAggregated?.(`unregistered-autonomous:${topicA ?? '?'}`, `An autonomous-flavored run on topic ${topicA ?? '?'} is proceeding UNREGISTERED (no per-topic state file) — surfaced from a fresh open commitment + recent user activity. Register it so its survival doesn't depend on a commitment existing.`);
6836
+ }
6837
+ catch { /* @silent-fallback-ok — Part A never endangers the reap path */ }
6838
+ console.log(`[gap-b] commitment-evidence ${gapBInject ? 'INJECTED' : 'would inject (dryRun)'} for ${e.session.name} (unregistered autonomous run)`);
6839
+ }
6668
6840
  // Enqueue hook (reap-notify R2.2): every terminal autonomous reap is
6669
6841
  // OFFERED to the resume queue; eligibility (evidence classes, job
6670
6842
  // opt-in, operator exclusion, resurrection cap) is decided inside.
@@ -6697,6 +6869,19 @@ export async function startServer(options) {
6697
6869
  candidateReason = AGE_LIMIT_ACTIVE_RUN_REASON;
6698
6870
  candidateWorkEvidence = [...candidateWorkEvidence, 'build-or-autonomous-active'];
6699
6871
  }
6872
+ else if (gapBInject) {
6873
+ // GAP-B Part B (the committed-unregistered-run backstop, LIVE only when
6874
+ // armed + NOT dryRun — see the verdict computed at the top of the
6875
+ // handler). Mutually exclusive with the branch above by construction:
6876
+ // that requires the per-topic state file PRESENT, gapBCommitmentFired
6877
+ // requires it ABSENT. Tag the DISTINCT reason so the drainer routes
6878
+ // this candidate to its own drain-time re-check (commitment liveness,
6879
+ // not the absent state file). The strong signal then flows through the
6880
+ // already-shipped evidenceEligible gate → ResumeQueue → drainer,
6881
+ // inheriting the resurrection cap (≤2/24h/topic — P19 bound).
6882
+ candidateReason = COMMITMENT_ACTIVE_RUN_REASON;
6883
+ candidateWorkEvidence = [...candidateWorkEvidence, 'build-or-autonomous-active'];
6884
+ }
6700
6885
  resumeQueue.considerEnqueue({
6701
6886
  sessionName: e.session.name,
6702
6887
  tmuxSession: e.session.tmuxSession,
@@ -12658,12 +12843,17 @@ export async function startServer(options) {
12658
12843
  hasPendingInjection: (s) => sessionManager.getPendingInjection(s) != null,
12659
12844
  isRelayLeaseActive: (id) => sessionManager.isRelayLeaseActive(id),
12660
12845
  topicBinding: _resolveTopic,
12661
- // Gate I is a v1 stub (returns false): active conversation is already
12662
- // covered by the relay-lease + pending-injection gates and by render
12663
- // stasis (a session being talked to is not render-static for the full
12664
- // hysteresis+threshold window). Promoting to a real message-recency
12665
- // query is a tracked tuning follow-up.
12666
- recentUserMessage: () => false,
12846
+ // Part D (spec: autonomous-registration-guarantee.md) promoted from the
12847
+ // v1 stub (`() => false`) to the SHARED real inbound-user-message recency
12848
+ // predicate (`recentUserMessageShared`, defined once near the resume-queue
12849
+ // wiring). This dep feeds FIVE live sites at once: ReapGuard's standalone
12850
+ // ~30min KEEP @137, its commitment-coupled 8h KEEP @149, the two terminate-
12851
+ // path mirrors @221/@239, and SessionReaper's staleIdle inversion @489. By
12852
+ // sharing the SAME predicate the GAP-B eligibility uses (D8), the KEEP
12853
+ // decisions and the revival eligibility are computed from identical truth
12854
+ // and cannot disagree (the 2026-06-13 loop the agreement prevents). All five
12855
+ // un-stub in the SAFE (keep-more) direction; fail-open (D7) on a throw.
12856
+ recentUserMessage: recentUserMessageShared,
12667
12857
  activeCommitmentForTopic: (topicId) => {
12668
12858
  try {
12669
12859
  return commitmentTracker.getActive().some(c => c.topicId === topicId);