instar 1.3.62 → 1.3.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +132 -8
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +20 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +22 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/ReapGuard.d.ts +79 -0
- package/dist/core/ReapGuard.d.ts.map +1 -0
- package/dist/core/ReapGuard.js +114 -0
- package/dist/core/ReapGuard.js.map +1 -0
- package/dist/core/SessionLivenessOracle.d.ts +104 -0
- package/dist/core/SessionLivenessOracle.d.ts.map +1 -0
- package/dist/core/SessionLivenessOracle.js +201 -0
- package/dist/core/SessionLivenessOracle.js.map +1 -0
- package/dist/core/SessionManager.d.ts +90 -10
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +200 -40
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/types.d.ts +30 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/monitoring/ReapLog.d.ts +51 -0
- package/dist/monitoring/ReapLog.d.ts.map +1 -0
- package/dist/monitoring/ReapLog.js +82 -0
- package/dist/monitoring/ReapLog.js.map +1 -0
- package/dist/monitoring/ReapNotifier.d.ts +66 -0
- package/dist/monitoring/ReapNotifier.d.ts.map +1 -0
- package/dist/monitoring/ReapNotifier.js +112 -0
- package/dist/monitoring/ReapNotifier.js.map +1 -0
- package/dist/monitoring/RevertDetector.d.ts +52 -0
- package/dist/monitoring/RevertDetector.d.ts.map +1 -0
- package/dist/monitoring/RevertDetector.js +219 -0
- package/dist/monitoring/RevertDetector.js.map +1 -0
- package/dist/monitoring/SessionReaper.d.ts +4 -0
- package/dist/monitoring/SessionReaper.d.ts.map +1 -1
- package/dist/monitoring/SessionReaper.js +19 -42
- package/dist/monitoring/SessionReaper.js.map +1 -1
- package/dist/monitoring/StaleSessionBackstop.d.ts +89 -0
- package/dist/monitoring/StaleSessionBackstop.d.ts.map +1 -0
- package/dist/monitoring/StaleSessionBackstop.js +176 -0
- package/dist/monitoring/StaleSessionBackstop.js.map +1 -0
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +5 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +4 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +36 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +33 -11
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/scaffold/templates.ts +5 -0
- package/upgrades/1.3.61.md +9 -14
- package/upgrades/1.3.62.md +43 -45
- package/upgrades/1.3.63.md +87 -0
- package/upgrades/1.3.64.md +27 -0
- package/upgrades/side-effects/failure-learning-revert-source.md +41 -0
- package/upgrades/side-effects/unified-session-lifecycle-robustness.md +241 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA+RH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAksDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA+RH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAksDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAi6NtE;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"}
|
package/dist/commands/server.js
CHANGED
|
@@ -3880,6 +3880,54 @@ export async function startServer(options) {
|
|
|
3880
3880
|
// WorkingMemoryAssembler is initialized after activitySentinel (see below)
|
|
3881
3881
|
// so it can wire episodicMemory from the sentinel.
|
|
3882
3882
|
let workingMemory;
|
|
3883
|
+
// ── Reap observability + notify (UNIFIED-SESSION-LIFECYCLE §P3/§P4) ──
|
|
3884
|
+
// Wired BEFORE the boot purge so even boot-time reaps land in the reap-log
|
|
3885
|
+
// and surface a notice. The awake-checker is also wired here so the boot
|
|
3886
|
+
// purge is lease-gated (a standby never reaps another machine's sessions).
|
|
3887
|
+
// (The KEEP-guard is wired later, once its tracker deps exist — that is safe:
|
|
3888
|
+
// the boot purge passes knownDead and bypasses the guard, and the only other
|
|
3889
|
+
// guard consumers (monitorTick #2 age-limit / #3 idle-zombie) cannot fire a
|
|
3890
|
+
// kill in the first monitoring seconds — they require multi-hour age or 15+m
|
|
3891
|
+
// of observed idle — long after the guard is set below.)
|
|
3892
|
+
const { ReapLog } = await import('../monitoring/ReapLog.js');
|
|
3893
|
+
const { ReapNotifier } = await import('../monitoring/ReapNotifier.js');
|
|
3894
|
+
const reapLog = new ReapLog(config.stateDir, () => (coordinator.enabled ? coordinator.identity?.machineId : undefined));
|
|
3895
|
+
const reapNotifier = new ReapNotifier({
|
|
3896
|
+
resolveTopic: (tmuxSession) => {
|
|
3897
|
+
const t = telegram?.getTopicForSession(tmuxSession);
|
|
3898
|
+
if (t == null)
|
|
3899
|
+
return null;
|
|
3900
|
+
const n = typeof t === 'number' ? t : Number(t);
|
|
3901
|
+
return Number.isFinite(n) ? n : null;
|
|
3902
|
+
},
|
|
3903
|
+
lifelineTopic: () => telegram?.getLifelineTopicId() ?? null,
|
|
3904
|
+
// SUMMARY tier → through the formatter/tone-gate (HTML-escaped, quiet-hours
|
|
3905
|
+
// aware). The notifier already coalesces, so a burst is one message.
|
|
3906
|
+
send: (topicId, text) => notify('SUMMARY', 'session-reap', text, topicId),
|
|
3907
|
+
}, {
|
|
3908
|
+
enabled: config.monitoring?.reapNotify?.enabled ?? true,
|
|
3909
|
+
coalesceWindowMs: config.monitoring?.reapNotify?.coalesceWindowMs ?? 60_000,
|
|
3910
|
+
});
|
|
3911
|
+
sessionManager.setAwakeChecker(() => !coordinator.enabled || coordinator.isAwake);
|
|
3912
|
+
sessionManager.on('sessionReaped', (e) => {
|
|
3913
|
+
reapLog.recordReaped({
|
|
3914
|
+
session: e.session.name,
|
|
3915
|
+
tmuxSession: e.session.tmuxSession,
|
|
3916
|
+
reason: e.reason,
|
|
3917
|
+
disposition: e.disposition,
|
|
3918
|
+
origin: e.origin,
|
|
3919
|
+
});
|
|
3920
|
+
reapNotifier.onReaped({ session: e.session, reason: e.reason, disposition: e.disposition, origin: e.origin });
|
|
3921
|
+
});
|
|
3922
|
+
sessionManager.on('reapBlocked', (e) => {
|
|
3923
|
+
reapLog.recordSkipped({
|
|
3924
|
+
session: e.session.name,
|
|
3925
|
+
tmuxSession: e.session.tmuxSession,
|
|
3926
|
+
reason: e.reason,
|
|
3927
|
+
skipped: e.skipped,
|
|
3928
|
+
origin: e.origin,
|
|
3929
|
+
});
|
|
3930
|
+
});
|
|
3883
3931
|
// Fast startup purge — remove session records for dead tmux sessions BEFORE
|
|
3884
3932
|
// monitoring starts. Prevents the death spiral where stale sessions overwhelm
|
|
3885
3933
|
// the health endpoint (synchronous tmux has-session calls) and cause the
|
|
@@ -7721,14 +7769,15 @@ export async function startServer(options) {
|
|
|
7721
7769
|
const n = typeof t === 'number' ? t : Number(t);
|
|
7722
7770
|
return Number.isFinite(n) ? n : null;
|
|
7723
7771
|
};
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7772
|
+
// Shared stateless KEEP-guard deps (UNIFIED-SESSION-LIFECYCLE §P2). Built
|
|
7773
|
+
// once here and used to back BOTH the SessionReaper and the ReapAuthority's
|
|
7774
|
+
// guard (`sessionManager.setReapGuard`), so a killer cannot end a session the
|
|
7775
|
+
// reaper would have kept — the same chain protects every kill path.
|
|
7776
|
+
const reapGuardDeps = {
|
|
7777
|
+
protectedSessions: () => sessionManager.getProtectedSessions(),
|
|
7729
7778
|
isRecoveryActive: (session) => composedRecoveryActive(session),
|
|
7730
|
-
isRelayLeaseActive: (id) => sessionManager.isRelayLeaseActive(id),
|
|
7731
7779
|
hasPendingInjection: (s) => sessionManager.getPendingInjection(s) != null,
|
|
7780
|
+
isRelayLeaseActive: (id) => sessionManager.isRelayLeaseActive(id),
|
|
7732
7781
|
topicBinding: _resolveTopic,
|
|
7733
7782
|
// Gate I is a v1 stub (returns false): active conversation is already
|
|
7734
7783
|
// covered by the relay-lease + pending-injection gates and by render
|
|
@@ -7765,7 +7814,23 @@ export async function startServer(options) {
|
|
|
7765
7814
|
return true;
|
|
7766
7815
|
return fresh(path.join(config.stateDir, 'state', 'build', 'build-state.json'));
|
|
7767
7816
|
},
|
|
7768
|
-
|
|
7817
|
+
hasActiveProcesses: (s) => sessionManager.hasActiveProcesses(s),
|
|
7818
|
+
};
|
|
7819
|
+
// Wire the ReapAuthority's KEEP-guard. minAgeMs:0 disables spawn-grace here —
|
|
7820
|
+
// the killers that consult this guard (age-limit #2, idle-zombie #3) already
|
|
7821
|
+
// gate on their own age/idle thresholds, so the reaper-specific spawn-grace
|
|
7822
|
+
// would otherwise delay legitimate zombie kills for young-but-stuck sessions.
|
|
7823
|
+
{
|
|
7824
|
+
const { ReapGuard } = await import('../core/ReapGuard.js');
|
|
7825
|
+
sessionManager.setReapGuard(new ReapGuard(reapGuardDeps, { minAgeMs: 0 }));
|
|
7826
|
+
}
|
|
7827
|
+
// (setAwakeChecker is wired earlier, before the boot purge, so the purge is
|
|
7828
|
+
// lease-gated — see the §P3/§P4 block above.)
|
|
7829
|
+
const sessionReaper = new SessionReaper({
|
|
7830
|
+
...reapGuardDeps,
|
|
7831
|
+
listRunningSessions: () => sessionManager.listRunningSessions(),
|
|
7832
|
+
captureOutput: (s, n) => sessionManager.captureOutput(s, n) ?? '',
|
|
7833
|
+
frameworkForSession: (s) => sessionManager.frameworkForSession(s),
|
|
7769
7834
|
pressure: () => {
|
|
7770
7835
|
const total = _os.totalmem();
|
|
7771
7836
|
const freePct = total > 0 ? (_os.freemem() / total) * 100 : 100;
|
|
@@ -7778,6 +7843,65 @@ export async function startServer(options) {
|
|
|
7778
7843
|
audit: fileAuditSink(config.stateDir),
|
|
7779
7844
|
}, config.monitoring?.sessionReaper);
|
|
7780
7845
|
sessionReaper.start();
|
|
7846
|
+
// ── Unkillability backstop (UNIFIED-SESSION-LIFECYCLE §P5) ───────────────
|
|
7847
|
+
// Signal-only: raises ONE deduped Attention item (never auto-kills) when a
|
|
7848
|
+
// session is KEPT forever despite faking work, or is stuck indeterminate.
|
|
7849
|
+
{
|
|
7850
|
+
const { StaleSessionBackstop } = await import('../monitoring/StaleSessionBackstop.js');
|
|
7851
|
+
const { probeTranscript } = await import('../monitoring/transcriptProber.js');
|
|
7852
|
+
const { makeAttentionPoster } = await import('../monitoring/sentinelWiring.js');
|
|
7853
|
+
const _crypto = await import('node:crypto');
|
|
7854
|
+
const staleCfg = config.monitoring?.staleBackstop ?? {};
|
|
7855
|
+
const progressFloorBytes = staleCfg.progressFloorBytes ?? 512;
|
|
7856
|
+
const hash = (s) => _crypto.createHash('sha1').update(s).digest('hex').slice(0, 16);
|
|
7857
|
+
const backstop = new StaleSessionBackstop({
|
|
7858
|
+
listRunningSessions: () => sessionManager.listRunningSessions(),
|
|
7859
|
+
probeLiveness: (names) => sessionManager.probeLivenessBatch(names),
|
|
7860
|
+
snapshot: (session) => {
|
|
7861
|
+
const framework = session.framework ?? sessionManager.frameworkForSession(session.tmuxSession) ?? 'claude-code';
|
|
7862
|
+
const sessionId = framework === 'claude-code' ? (session.claudeSessionId ?? '') : '';
|
|
7863
|
+
const tp = probeTranscript({ framework, sessionId, projectDir: '' });
|
|
7864
|
+
// Tail hash: read the last progressFloorBytes so a heartbeat-byte append
|
|
7865
|
+
// (same tail) is NOT counted as a meaningful advance.
|
|
7866
|
+
let tailHash = null;
|
|
7867
|
+
if (tp.resolved && tp.path) {
|
|
7868
|
+
try {
|
|
7869
|
+
const fd = fs.openSync(tp.path, 'r');
|
|
7870
|
+
try {
|
|
7871
|
+
const start = Math.max(0, tp.size - progressFloorBytes);
|
|
7872
|
+
const len = tp.size - start;
|
|
7873
|
+
const buf = Buffer.alloc(len);
|
|
7874
|
+
fs.readSync(fd, buf, 0, len, start);
|
|
7875
|
+
tailHash = hash(buf.toString('utf-8'));
|
|
7876
|
+
}
|
|
7877
|
+
finally {
|
|
7878
|
+
fs.closeSync(fd);
|
|
7879
|
+
}
|
|
7880
|
+
}
|
|
7881
|
+
catch {
|
|
7882
|
+
// @silent-fallback-ok — an unreadable transcript tail just means
|
|
7883
|
+
// "no meaningful-advance signal this tick"; the backstop treats a
|
|
7884
|
+
// null tail as ambiguous (never as progress), which is safe.
|
|
7885
|
+
tailHash = null;
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
const frame = sessionManager.captureOutput(session.tmuxSession, 8) ?? '';
|
|
7889
|
+
return {
|
|
7890
|
+
transcriptResolved: tp.resolved,
|
|
7891
|
+
transcriptSize: tp.size,
|
|
7892
|
+
transcriptTailHash: tailHash,
|
|
7893
|
+
mainProcessActive: sessionManager.hasActiveProcesses(session.tmuxSession),
|
|
7894
|
+
idleStateToken: hash(frame),
|
|
7895
|
+
};
|
|
7896
|
+
},
|
|
7897
|
+
raiseAttention: makeAttentionPoster({ port: config.port, authToken: config.authToken ?? '' }),
|
|
7898
|
+
setLongIndeterminate: (id, isLong) => sessionManager.markLongIndeterminate(id, isLong),
|
|
7899
|
+
}, staleCfg);
|
|
7900
|
+
backstop.start();
|
|
7901
|
+
if (staleCfg.enabled !== false) {
|
|
7902
|
+
console.log(pc.green(' Unkillability backstop enabled (§P5 — signal-only, never auto-kills)'));
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7781
7905
|
if (config.monitoring?.sessionReaper?.enabled) {
|
|
7782
7906
|
console.log(pc.green(config.monitoring.sessionReaper.dryRun === false
|
|
7783
7907
|
? ' SessionReaper enabled (idle-session reaper — LIVE)'
|
|
@@ -7930,7 +8054,7 @@ export async function startServer(options) {
|
|
|
7930
8054
|
}
|
|
7931
8055
|
}
|
|
7932
8056
|
}
|
|
7933
|
-
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, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, 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, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, unjustifiedStopGate, stopGateDb, stopNotifier });
|
|
8057
|
+
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, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, 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, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, reapLog, unjustifiedStopGate, stopGateDb, stopNotifier });
|
|
7934
8058
|
// Boot-recovery (tunnel-failure-resilience spec Part 6): if the agent
|
|
7935
8059
|
// died mid-relay-episode, the persisted tunnel.json carries
|
|
7936
8060
|
// rotationPending=true. Rotate the dashboard PIN + authToken BEFORE
|