instar 1.3.500 → 1.3.502
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 +943 -14
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +51 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +7 -0
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/DeliverMessageHandler.d.ts +13 -1
- package/dist/core/DeliverMessageHandler.d.ts.map +1 -1
- package/dist/core/DeliverMessageHandler.js +7 -0
- package/dist/core/DeliverMessageHandler.js.map +1 -1
- package/dist/core/MachinePoolRegistry.d.ts +3 -0
- package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
- package/dist/core/MachinePoolRegistry.js +1 -0
- package/dist/core/MachinePoolRegistry.js.map +1 -1
- package/dist/core/OwnerSuspectBreaker.d.ts +37 -0
- package/dist/core/OwnerSuspectBreaker.d.ts.map +1 -1
- package/dist/core/OwnerSuspectBreaker.js +58 -0
- package/dist/core/OwnerSuspectBreaker.js.map +1 -1
- package/dist/core/PendingInboundStore.d.ts +310 -0
- package/dist/core/PendingInboundStore.d.ts.map +1 -0
- package/dist/core/PendingInboundStore.js +715 -0
- package/dist/core/PendingInboundStore.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +44 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/QueueDrainLoop.d.ts +247 -0
- package/dist/core/QueueDrainLoop.d.ts.map +1 -0
- package/dist/core/QueueDrainLoop.js +0 -0
- package/dist/core/QueueDrainLoop.js.map +1 -0
- package/dist/core/ReapGuard.d.ts +24 -0
- package/dist/core/ReapGuard.d.ts.map +1 -1
- package/dist/core/ReapGuard.js +62 -0
- package/dist/core/ReapGuard.js.map +1 -1
- package/dist/core/SessionManager.d.ts +32 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +74 -2
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionRouter.d.ts +36 -2
- package/dist/core/SessionRouter.d.ts.map +1 -1
- package/dist/core/SessionRouter.js +34 -9
- package/dist/core/SessionRouter.js.map +1 -1
- package/dist/core/WorkEvidence.d.ts +45 -0
- package/dist/core/WorkEvidence.d.ts.map +1 -0
- package/dist/core/WorkEvidence.js +88 -0
- package/dist/core/WorkEvidence.js.map +1 -0
- package/dist/core/componentCategories.d.ts.map +1 -1
- package/dist/core/componentCategories.js +3 -0
- package/dist/core/componentCategories.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +15 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/inboundQueueBootSweep.d.ts +61 -0
- package/dist/core/inboundQueueBootSweep.d.ts.map +1 -0
- package/dist/core/inboundQueueBootSweep.js +207 -0
- package/dist/core/inboundQueueBootSweep.js.map +1 -0
- package/dist/core/inboundQueueConfig.d.ts +78 -0
- package/dist/core/inboundQueueConfig.d.ts.map +1 -0
- package/dist/core/inboundQueueConfig.js +135 -0
- package/dist/core/inboundQueueConfig.js.map +1 -0
- package/dist/core/types.d.ts +73 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/messaging/NotificationBatcher.d.ts +12 -0
- package/dist/messaging/NotificationBatcher.d.ts.map +1 -1
- package/dist/messaging/NotificationBatcher.js +36 -0
- package/dist/messaging/NotificationBatcher.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +4 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +12 -0
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/pending-relay-store.d.ts +67 -8
- package/dist/messaging/pending-relay-store.d.ts.map +1 -1
- package/dist/messaging/pending-relay-store.js +151 -14
- package/dist/messaging/pending-relay-store.js.map +1 -1
- package/dist/messaging/reap-notice-delivery-id.d.ts +37 -0
- package/dist/messaging/reap-notice-delivery-id.d.ts.map +1 -0
- package/dist/messaging/reap-notice-delivery-id.js +52 -0
- package/dist/messaging/reap-notice-delivery-id.js.map +1 -0
- package/dist/monitoring/QuotaManager.d.ts +6 -0
- package/dist/monitoring/QuotaManager.d.ts.map +1 -1
- package/dist/monitoring/QuotaManager.js +11 -0
- package/dist/monitoring/QuotaManager.js.map +1 -1
- package/dist/monitoring/ReapLog.d.ts +39 -4
- package/dist/monitoring/ReapLog.d.ts.map +1 -1
- package/dist/monitoring/ReapLog.js +54 -4
- package/dist/monitoring/ReapLog.js.map +1 -1
- package/dist/monitoring/ReapNoticeDrain.d.ts +113 -0
- package/dist/monitoring/ReapNoticeDrain.d.ts.map +1 -0
- package/dist/monitoring/ReapNoticeDrain.js +231 -0
- package/dist/monitoring/ReapNoticeDrain.js.map +1 -0
- package/dist/monitoring/ReapNotifier.d.ts +89 -18
- package/dist/monitoring/ReapNotifier.d.ts.map +1 -1
- package/dist/monitoring/ReapNotifier.js +329 -22
- package/dist/monitoring/ReapNotifier.js.map +1 -1
- package/dist/monitoring/ResumeQueue.d.ts +223 -0
- package/dist/monitoring/ResumeQueue.d.ts.map +1 -0
- package/dist/monitoring/ResumeQueue.js +600 -0
- package/dist/monitoring/ResumeQueue.js.map +1 -0
- package/dist/monitoring/ResumeQueueDrainer.d.ts +151 -0
- package/dist/monitoring/ResumeQueueDrainer.d.ts.map +1 -0
- package/dist/monitoring/ResumeQueueDrainer.js +420 -0
- package/dist/monitoring/ResumeQueueDrainer.js.map +1 -0
- package/dist/monitoring/SessionMigrator.d.ts +10 -0
- package/dist/monitoring/SessionMigrator.d.ts.map +1 -1
- package/dist/monitoring/SessionMigrator.js +43 -1
- package/dist/monitoring/SessionMigrator.js.map +1 -1
- package/dist/monitoring/SessionReaper.d.ts +1 -0
- package/dist/monitoring/SessionReaper.d.ts.map +1 -1
- package/dist/monitoring/SessionReaper.js +5 -0
- package/dist/monitoring/SessionReaper.js.map +1 -1
- package/dist/monitoring/delivery-failure-sentinel.d.ts.map +1 -1
- package/dist/monitoring/delivery-failure-sentinel.js +7 -3
- package/dist/monitoring/delivery-failure-sentinel.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +27 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +10 -3
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +7 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +4 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/routes.d.ts +12 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +119 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/lint-guard-manifest.js +2 -0
- package/src/data/builtin-manifest.json +66 -66
- package/src/data/state-coherence-registry.json +14 -1
- package/src/scaffold/templates.ts +10 -3
- package/upgrades/1.3.501.md +51 -0
- package/upgrades/1.3.502.md +75 -0
- package/upgrades/side-effects/1.3.492.md +20 -0
- package/upgrades/side-effects/durable-inbound-message-queue.md +213 -0
- package/upgrades/side-effects/reap-notify-resume-queue.md +89 -0
package/dist/commands/server.js
CHANGED
|
@@ -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';
|
|
@@ -366,6 +366,20 @@ let _topicResumeMap = null;
|
|
|
366
366
|
// is byte-identical to today's always-local dispatch. Set once in startServer().
|
|
367
367
|
let _sessionRouter = null;
|
|
368
368
|
let _sessionPoolStage = () => 'dark';
|
|
369
|
+
// ── Durable Inbound Message Queue (docs/specs/durable-inbound-message-queue.md) ──
|
|
370
|
+
// The custody engine (null = feature dark / gate failed / invariants violated —
|
|
371
|
+
// every consumer treats null as "refused → today's fall-through").
|
|
372
|
+
let _inboundQueue = null;
|
|
373
|
+
// The store the unconditional boot sweep opened when the queue will run this
|
|
374
|
+
// boot — adopted by the engine construction (one open handle, single-writer).
|
|
375
|
+
let _sweptInboundStore = null;
|
|
376
|
+
// The drain's local-delivery tail (§3.1 via:'drain') — assigned inside
|
|
377
|
+
// wireTelegramRouting where the session primitives live in scope.
|
|
378
|
+
let _drainLocalDeliver = null;
|
|
379
|
+
// Emergency-stop integration (§3.6): marks a topic stopped for the drain's
|
|
380
|
+
// pass/batch/chokepoint consults AND settles its custody (terminal
|
|
381
|
+
// operator-stop + PIS cleanup + loss report). Set with the engine.
|
|
382
|
+
let _inboundQueueStop = null;
|
|
369
383
|
/** This machine's mesh id — lets the inbound dispatch tell a REMOTE placement
|
|
370
384
|
* (forward/spawn on another machine → must NOT also dispatch locally) from a
|
|
371
385
|
* self placement. Set once in startServer()'s mesh block. */
|
|
@@ -516,7 +530,11 @@ async function spawnSessionForTopic(sessionManager, telegram, sessionName, topic
|
|
|
516
530
|
/** Subscription & Auth Standard P1.3 (additive): launch under this account's
|
|
517
531
|
* config home + record its id (the quota-aware account-swap mechanism).
|
|
518
532
|
* Omitted = unchanged behaviour. */
|
|
519
|
-
accountSwap
|
|
533
|
+
accountSwap,
|
|
534
|
+
/** Reap-notify spec R2.8 / L13 — explicit per-spawn working directory (the
|
|
535
|
+
* resume-queue drainer passes a queue entry's recorded cwd so interrupted
|
|
536
|
+
* worktree work resumes in ITS tree). Omitted = module project dir. */
|
|
537
|
+
spawnOpts) {
|
|
520
538
|
const msg = latestMessage || 'Session started — send a message to continue.';
|
|
521
539
|
// If memory is elevated/critical and we have the reaper, try to free memory
|
|
522
540
|
// by cleaning orphans before spawning. Interactive sessions are NEVER blocked
|
|
@@ -787,6 +805,8 @@ accountSwap) {
|
|
|
787
805
|
// this account's config home + record its id. Unset = unchanged.
|
|
788
806
|
...(accountSwap?.configHome ? { configHome: accountSwap.configHome } : {}),
|
|
789
807
|
...(accountSwap?.accountId ? { subscriptionAccountId: accountSwap.accountId } : {}),
|
|
808
|
+
// R2.8/L13: per-spawn cwd from the resume-queue entry. Unset = projectDir.
|
|
809
|
+
...(spawnOpts?.cwd ? { cwd: spawnOpts.cwd } : {}),
|
|
790
810
|
});
|
|
791
811
|
// Clear the resume entry after successful spawn to prevent stale reuse
|
|
792
812
|
if (resumeSessionId) {
|
|
@@ -1740,17 +1760,42 @@ getAttentionTopicId) {
|
|
|
1740
1760
|
}
|
|
1741
1761
|
}
|
|
1742
1762
|
if (_sessionRouter && _sessionPoolStage() !== 'dark') {
|
|
1763
|
+
// Ordering gate (Durable Inbound Message Queue §2.3): a live message for
|
|
1764
|
+
// a session with queued custody enqueues BEHIND the existing entries —
|
|
1765
|
+
// injecting it now would deliver out of order. Gated on a live engine.
|
|
1766
|
+
if (_inboundQueue) {
|
|
1767
|
+
try {
|
|
1768
|
+
if (_inboundQueue.hasQueued(String(topicId))) {
|
|
1769
|
+
const ord = _inboundQueue.enqueueLive({
|
|
1770
|
+
sessionKey: String(topicId),
|
|
1771
|
+
messageId: String(msg.id),
|
|
1772
|
+
payload: text,
|
|
1773
|
+
senderEnvelope: { userId: telegramUserId || undefined, firstName: pipeline.sender.firstName },
|
|
1774
|
+
topicMetadata: _topicPinStore?.asTopicMetadata(String(topicId)),
|
|
1775
|
+
}, 'ordering-behind-queued');
|
|
1776
|
+
if (ord.result === 'queued' || ord.result === 'already-queued') {
|
|
1777
|
+
console.log(`[inbound-queue] topic ${topicId} msg ${msg.id} queued behind existing entries (ordering)`);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
// refused → fall through to route() — delivery beats both loss and
|
|
1781
|
+
// silence; the ordering violation is counted by the engine.
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
catch { /* gate is best-effort; route() owns the message */ }
|
|
1785
|
+
}
|
|
1743
1786
|
try {
|
|
1744
1787
|
const outcome = await _sessionRouter.route({
|
|
1745
1788
|
sessionKey: String(topicId),
|
|
1746
1789
|
messageId: String(msg.id),
|
|
1747
1790
|
payload: text,
|
|
1748
1791
|
topicMetadata: _topicPinStore?.asTopicMetadata(String(topicId)),
|
|
1792
|
+
// §2.2: sender identity captured at ingress, persisted with custody.
|
|
1793
|
+
senderEnvelope: { userId: telegramUserId || undefined, firstName: pipeline.sender.firstName },
|
|
1749
1794
|
});
|
|
1750
1795
|
// Routing-decision observability — the live transfer path is otherwise a black
|
|
1751
1796
|
// box (the recognizer logs its pin, but route()'s actual placement/forward
|
|
1752
1797
|
// decision was invisible; that hid the bug below from the first live test).
|
|
1753
|
-
console.log(`[session-pool] route topic ${topicId} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'}`);
|
|
1798
|
+
console.log(`[session-pool] route topic ${topicId} → action=${outcome.action} owner=${outcome.owner ?? '?'} self=${_meshSelfId ?? '?'} acked=${outcome.acked}`);
|
|
1754
1799
|
// Short-circuit local dispatch whenever the session ended up on ANOTHER machine
|
|
1755
1800
|
// (forward/duplicate, OR a fresh remote 'spawned'/'owner-dead-replaced'). Before
|
|
1756
1801
|
// this, only 'forwarded'/'duplicate' were caught, so a just-moved topic was
|
|
@@ -1760,10 +1805,30 @@ getAttentionTopicId) {
|
|
|
1760
1805
|
console.log(`[session-pool] topic ${topicId} handled by owner ${outcome.owner ?? '?'} (${outcome.action}) — not dispatching locally`);
|
|
1761
1806
|
return;
|
|
1762
1807
|
}
|
|
1763
|
-
//
|
|
1764
|
-
//
|
|
1808
|
+
// Custody-ack short-circuit (§2.2): a queued/placement-blocked verdict
|
|
1809
|
+
// whose enqueue COMMITTED (acked) is the queue's message now — no local
|
|
1810
|
+
// fall-through. Un-custodied (refused/off/dry-run) keeps today's
|
|
1811
|
+
// fall-through. Wiring pins assert both directions.
|
|
1812
|
+
if ((outcome.action === 'queued' || outcome.action === 'placement-blocked') && outcome.acked) {
|
|
1813
|
+
console.log(`[inbound-queue] topic ${topicId} msg ${msg.id} in durable custody (${outcome.detail ?? outcome.action}) — drain will deliver`);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
// 'handled-locally' / 'spawned'(self) / 'owner-dead-replaced'(self) /
|
|
1817
|
+
// un-acked 'queued'/'placement-blocked' → fall through to local dispatch.
|
|
1765
1818
|
}
|
|
1766
1819
|
catch (err) {
|
|
1820
|
+
// Route-throw fail-open is CUSTODY-AWARE (§2.2): a per-MESSAGE point
|
|
1821
|
+
// read against the store — a committed non-terminal row for THIS
|
|
1822
|
+
// message means the queue owns it (skip local dispatch); no row (or
|
|
1823
|
+
// engine dark) → today's fall-through. A point-read ERROR fails OPEN
|
|
1824
|
+
// to fall-through — the bounded duplicate window, §5-enumerated.
|
|
1825
|
+
try {
|
|
1826
|
+
if (_inboundQueue?.hasCommittedRow(String(topicId), String(msg.id))) {
|
|
1827
|
+
console.warn(`[session-pool] route error for topic ${topicId} but custody is committed — not dispatching locally: ${err instanceof Error ? err.message : String(err)}`);
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
catch { /* point-read error → fail open (fall through) */ }
|
|
1767
1832
|
console.warn(`[session-pool] route error for topic ${topicId} — falling back to local dispatch: ${err instanceof Error ? err.message : String(err)}`);
|
|
1768
1833
|
}
|
|
1769
1834
|
}
|
|
@@ -1887,6 +1952,99 @@ getAttentionTopicId) {
|
|
|
1887
1952
|
});
|
|
1888
1953
|
}
|
|
1889
1954
|
};
|
|
1955
|
+
// ── Durable Inbound Message Queue: the drain's local-delivery tail ──
|
|
1956
|
+
// (§3.1 via:'drain'.) Built from the SAME primitives as the live tail above
|
|
1957
|
+
// (injectTelegramMessage / respawnSessionForTopic / spawnSessionForTopic +
|
|
1958
|
+
// the spawningTopics guard) with the drain contract's divergences, each
|
|
1959
|
+
// enumerated in the spec: bypasses the intercept stack (a stored message is
|
|
1960
|
+
// DATA — this function never re-interprets commands or re-binds operators),
|
|
1961
|
+
// bypasses the ingress ledger (the receipt is the at-most-once authority),
|
|
1962
|
+
// suppresses the per-message "✓ Delivered" confirmation, AWAITS the tail
|
|
1963
|
+
// through the receipt write, injects with the STORED sender envelope, and
|
|
1964
|
+
// paces same-session runs at 1s.
|
|
1965
|
+
_drainLocalDeliver = async (dmsg, handover) => {
|
|
1966
|
+
const topicId = Number(dmsg.sessionKey);
|
|
1967
|
+
if (!Number.isFinite(topicId))
|
|
1968
|
+
return { kind: 'failed', error: new Error(`non-numeric sessionKey ${dmsg.sessionKey}`) };
|
|
1969
|
+
if (!_sessionRouter)
|
|
1970
|
+
return { kind: 'un-routable', reason: 'router-not-constructed' };
|
|
1971
|
+
// Routing first — ownership may have moved while the entry waited.
|
|
1972
|
+
const outcome = await _sessionRouter.route({
|
|
1973
|
+
sessionKey: dmsg.sessionKey,
|
|
1974
|
+
messageId: dmsg.messageId,
|
|
1975
|
+
payload: dmsg.payload,
|
|
1976
|
+
topicMetadata: dmsg.topicMetadata
|
|
1977
|
+
?? _topicPinStore?.asTopicMetadata(dmsg.sessionKey),
|
|
1978
|
+
senderEnvelope: dmsg.senderEnvelope,
|
|
1979
|
+
});
|
|
1980
|
+
if (outcome.action === 'forwarded' && outcome.detail === 'sender-rejected') {
|
|
1981
|
+
return { kind: 'sender-rejected' };
|
|
1982
|
+
}
|
|
1983
|
+
if (isRemotelyHandled(outcome, _meshSelfId)) {
|
|
1984
|
+
return { kind: 'remote-delivered' };
|
|
1985
|
+
}
|
|
1986
|
+
if (outcome.action === 'queued' || outcome.action === 'placement-blocked') {
|
|
1987
|
+
// The router re-queued our own entry ('already-queued' against the
|
|
1988
|
+
// claimed row) — un-routable: release + backoff + attempts++ (§3.1).
|
|
1989
|
+
return { kind: 'un-routable', reason: outcome.detail ?? outcome.action };
|
|
1990
|
+
}
|
|
1991
|
+
// Local delivery ('handled-locally' / self 'owner-dead-replaced').
|
|
1992
|
+
const sender = dmsg.senderEnvelope ?? undefined;
|
|
1993
|
+
const senderFirstName = sender?.firstName ?? 'User';
|
|
1994
|
+
const senderUserId = typeof sender?.userId === 'number' ? sender.userId : Number(sender?.userId ?? 0) || 0;
|
|
1995
|
+
const resolvedUser = senderUserId && userManager ? userManager.resolveFromTelegramUserId(senderUserId) : null;
|
|
1996
|
+
const targetSession = telegram.getSessionForTopic(topicId);
|
|
1997
|
+
const topicName = telegram.getTopicName(topicId) || `topic-${topicId}`;
|
|
1998
|
+
if (targetSession && sessionManager.isSessionAlive(targetSession)) {
|
|
1999
|
+
// Direct inject: receipt FIRST (the §3.4 handover point), stop re-check,
|
|
2000
|
+
// then the inject. A caught inject error AFTER the receipt is
|
|
2001
|
+
// local-delivered+injectError (never silent loss).
|
|
2002
|
+
if (!handover.commitReceipt())
|
|
2003
|
+
return { kind: 'handover-refused' };
|
|
2004
|
+
if (handover.stopRecheck())
|
|
2005
|
+
return { kind: 'stopped-before-inject' };
|
|
2006
|
+
try {
|
|
2007
|
+
sessionManager.injectTelegramMessage(targetSession, topicId, dmsg.payload, topicName, senderFirstName, senderUserId, Number(dmsg.messageId.replace(/^\D*/, '')) || undefined);
|
|
2008
|
+
telegram.trackMessageInjection(topicId, targetSession, dmsg.payload);
|
|
2009
|
+
// 1s inter-inject pacing for same-session runs (§3.1, pinned round-6).
|
|
2010
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
2011
|
+
return { kind: 'local-delivered' };
|
|
2012
|
+
}
|
|
2013
|
+
catch (err) {
|
|
2014
|
+
return { kind: 'local-delivered', injectError: err instanceof Error ? err.message : String(err) };
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
// Respawn / auto-spawn path. The spawn-in-progress guard maps to
|
|
2018
|
+
// un-routable (§3.1) — never a silent skip.
|
|
2019
|
+
if (spawningTopics.has(topicId)) {
|
|
2020
|
+
return { kind: 'un-routable', reason: 'spawn-in-progress' };
|
|
2021
|
+
}
|
|
2022
|
+
if (!handover.commitReceipt())
|
|
2023
|
+
return { kind: 'handover-refused' };
|
|
2024
|
+
if (handover.stopRecheck())
|
|
2025
|
+
return { kind: 'stopped-before-inject' };
|
|
2026
|
+
spawningTopics.add(topicId);
|
|
2027
|
+
try {
|
|
2028
|
+
if (targetSession) {
|
|
2029
|
+
// Dead session — respawn, AWAITED through the spawn+inject (the PIS
|
|
2030
|
+
// record is written inside, AFTER our receipt — the round-7 order pin).
|
|
2031
|
+
await respawnSessionForTopic(sessionManager, telegram, targetSession, topicId, dmsg.payload, topicMemory, resolvedUser ?? undefined);
|
|
2032
|
+
}
|
|
2033
|
+
else {
|
|
2034
|
+
const newSessionName = await spawnSessionForTopic(sessionManager, telegram, topicName, topicId, dmsg.payload, topicMemory, resolvedUser ?? undefined);
|
|
2035
|
+
telegram.registerTopicSession(topicId, newSessionName, topicName);
|
|
2036
|
+
}
|
|
2037
|
+
return { kind: 'local-delivered' };
|
|
2038
|
+
}
|
|
2039
|
+
catch (err) {
|
|
2040
|
+
// Receipt already committed — the at-most-once authority forbids a
|
|
2041
|
+
// re-inject; honest disposition: delivered-unconfirmed + report (§3.4).
|
|
2042
|
+
return { kind: 'local-delivered', injectError: err instanceof Error ? err.message : String(err) };
|
|
2043
|
+
}
|
|
2044
|
+
finally {
|
|
2045
|
+
spawningTopics.delete(topicId);
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
1890
2048
|
}
|
|
1891
2049
|
/**
|
|
1892
2050
|
* Wire WhatsApp message routing: incoming messages → Claude sessions.
|
|
@@ -5422,6 +5580,28 @@ export async function startServer(options) {
|
|
|
5422
5580
|
const { ReapLog } = await import('../monitoring/ReapLog.js');
|
|
5423
5581
|
const { ReapNotifier } = await import('../monitoring/ReapNotifier.js');
|
|
5424
5582
|
const reapLog = new ReapLog(config.stateDir, () => (coordinator.enabled ? coordinator.identity?.machineId : undefined));
|
|
5583
|
+
// ── Durable notice lane (reap-notify spec R1.3) ──
|
|
5584
|
+
// The notifier enqueues notices as `reap-notify:<id>` rows in the shared
|
|
5585
|
+
// PendingRelayStore; the ALWAYS-ON ReapNoticeDrain (started after the
|
|
5586
|
+
// Telegram adapter exists, below) delivers them — independent of the
|
|
5587
|
+
// default-OFF DeliveryFailureSentinel. A store open failure degrades the
|
|
5588
|
+
// notifier to its loud direct-send fallback (R1.3 enqueue-failed path).
|
|
5589
|
+
let reapNoticeStore = null;
|
|
5590
|
+
try {
|
|
5591
|
+
const { PendingRelayStore, assertSqliteAvailable } = await import('../messaging/pending-relay-store.js');
|
|
5592
|
+
if (assertSqliteAvailable().ok) {
|
|
5593
|
+
reapNoticeStore = PendingRelayStore.open(config.projectName, config.stateDir);
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
catch (err) {
|
|
5597
|
+
// @silent-fallback-ok — not silent: warns with full context here, and the
|
|
5598
|
+
// notifier's enqueue-failed path reports via DegradationReporter (R1.3).
|
|
5599
|
+
console.warn('[reap-notify] pending-relay store unavailable — notices fall back to direct send:', err);
|
|
5600
|
+
reapNoticeStore = null;
|
|
5601
|
+
}
|
|
5602
|
+
// Wired by the resume-queue boot block (Part B) once the queue exists;
|
|
5603
|
+
// stays false (no "restart queued" lines) until then or in dry-run.
|
|
5604
|
+
let resumeQueuedForSession = () => false;
|
|
5425
5605
|
const reapNotifier = new ReapNotifier({
|
|
5426
5606
|
resolveTopic: (tmuxSession) => {
|
|
5427
5607
|
const t = telegram?.getTopicForSession(tmuxSession);
|
|
@@ -5431,14 +5611,353 @@ export async function startServer(options) {
|
|
|
5431
5611
|
return Number.isFinite(n) ? n : null;
|
|
5432
5612
|
},
|
|
5433
5613
|
lifelineTopic: () => telegram?.getLifelineTopicId() ?? null,
|
|
5434
|
-
//
|
|
5435
|
-
// aware). The notifier already coalesces
|
|
5614
|
+
// Legacy + fallback transport → through the formatter/tone-gate
|
|
5615
|
+
// (HTML-escaped, quiet-hours aware). The notifier already coalesces.
|
|
5436
5616
|
send: (topicId, text) => notify('SUMMARY', 'session-reap', text, topicId),
|
|
5617
|
+
enqueueNotice: reapNoticeStore
|
|
5618
|
+
? (input) => reapNoticeStore.enqueue({
|
|
5619
|
+
delivery_id: input.delivery_id,
|
|
5620
|
+
topic_id: input.topic_id,
|
|
5621
|
+
text_hash: createHash('sha256').update(input.text).digest('hex'),
|
|
5622
|
+
text: input.text,
|
|
5623
|
+
next_attempt_at: input.next_attempt_at,
|
|
5624
|
+
})
|
|
5625
|
+
: undefined,
|
|
5626
|
+
recordNotify: (e) => reapLog.recordNotify(e),
|
|
5627
|
+
quietHoursEndAt: (now) => notificationBatcher.quietHoursEndAt(now),
|
|
5628
|
+
summaryReleaseAt: (now) => notificationBatcher.nextSummaryReleaseAt(now),
|
|
5629
|
+
resumeQueuedFor: (tmuxSession) => resumeQueuedForSession(tmuxSession),
|
|
5630
|
+
reportDegradation: (reason, impact) => {
|
|
5631
|
+
try {
|
|
5632
|
+
DegradationReporter.getInstance().report({
|
|
5633
|
+
feature: 'reap-notice-enqueue',
|
|
5634
|
+
primary: 'durable reap-notice delivery via PendingRelayStore + ReapNoticeDrain',
|
|
5635
|
+
fallback: 'one direct send attempt (fire-and-forget)',
|
|
5636
|
+
reason,
|
|
5637
|
+
impact,
|
|
5638
|
+
});
|
|
5639
|
+
}
|
|
5640
|
+
catch { /* @silent-fallback-ok — guard around the degradation REPORTER itself; the degradation it describes was already logged by the caller */ }
|
|
5641
|
+
},
|
|
5437
5642
|
}, {
|
|
5438
5643
|
enabled: config.monitoring?.reapNotify?.enabled ?? true,
|
|
5439
5644
|
coalesceWindowMs: config.monitoring?.reapNotify?.coalesceWindowMs ?? 60_000,
|
|
5645
|
+
perTopic: config.monitoring?.reapNotify?.perTopic ?? true,
|
|
5646
|
+
maxImmediatePerFlush: config.monitoring?.reapNotify?.maxImmediatePerFlush ?? 5,
|
|
5647
|
+
drainEnabled: config.monitoring?.reapNotify?.drainEnabled ?? true,
|
|
5440
5648
|
});
|
|
5649
|
+
// ReapNoticeDrain — the always-on delivery loop over the reap-notify lane
|
|
5650
|
+
// (R1.3; independent of the default-OFF DeliveryFailureSentinel). The
|
|
5651
|
+
// telegram adapter is referenced lazily: it does not exist yet at this
|
|
5652
|
+
// point in boot, and a tick with no adapter simply retries on backoff.
|
|
5653
|
+
let reapNoticeDrain = null;
|
|
5654
|
+
if (reapNoticeStore && (config.monitoring?.reapNotify?.drainEnabled ?? true)) {
|
|
5655
|
+
const { ReapNoticeDrain } = await import('../monitoring/ReapNoticeDrain.js');
|
|
5656
|
+
const { getCurrentBootId } = await import('../server/boot-id.js');
|
|
5657
|
+
reapNoticeDrain = new ReapNoticeDrain({
|
|
5658
|
+
store: reapNoticeStore,
|
|
5659
|
+
sendToTopic: async (topicId, text) => {
|
|
5660
|
+
if (!telegram)
|
|
5661
|
+
throw new Error('telegram adapter not available');
|
|
5662
|
+
await telegram.sendToTopic(topicId, text);
|
|
5663
|
+
},
|
|
5664
|
+
recordNotify: (e) => reapLog.recordNotify(e),
|
|
5665
|
+
emitAttention: async (item) => {
|
|
5666
|
+
if (!telegram)
|
|
5667
|
+
return;
|
|
5668
|
+
await telegram.createAttentionItem({
|
|
5669
|
+
id: item.id,
|
|
5670
|
+
title: item.title,
|
|
5671
|
+
summary: item.summary ?? item.title,
|
|
5672
|
+
description: item.description,
|
|
5673
|
+
category: item.category ?? 'delivery',
|
|
5674
|
+
priority: item.priority === 'high' ? 'HIGH' : item.priority === 'low' ? 'LOW' : 'NORMAL',
|
|
5675
|
+
sourceContext: item.sourceContext,
|
|
5676
|
+
});
|
|
5677
|
+
},
|
|
5678
|
+
bootId: getCurrentBootId() ?? `boot-${Date.now().toString(36)}`,
|
|
5679
|
+
});
|
|
5680
|
+
reapNoticeDrain.start();
|
|
5681
|
+
console.log(pc.green(' ReapNoticeDrain started (always-on durable reap-notice delivery)'));
|
|
5682
|
+
}
|
|
5441
5683
|
sessionManager.setAwakeChecker(() => !coordinator.enabled || coordinator.isAwake);
|
|
5684
|
+
// Pressure-tier provider for the evidence fallback (reap-notify R2.1):
|
|
5685
|
+
// the SAME shared HostPressureSampler definition the reaper and the
|
|
5686
|
+
// resume-queue drainer read — one definition of "pressure", never two.
|
|
5687
|
+
const { sampleHostPressure: samplePressureShared } = await import('../monitoring/HostPressureSampler.js');
|
|
5688
|
+
const reaperPressureCfg = config.monitoring?.sessionReaper;
|
|
5689
|
+
const sharedPressureTier = () => samplePressureShared({
|
|
5690
|
+
cpuModerateLoadPerCore: reaperPressureCfg?.cpuModerateLoadPerCore ?? 1.0,
|
|
5691
|
+
cpuCriticalLoadPerCore: reaperPressureCfg?.cpuCriticalLoadPerCore ?? 1.5,
|
|
5692
|
+
}).tier;
|
|
5693
|
+
sessionManager.setPressureTierProvider(sharedPressureTier);
|
|
5694
|
+
// ── ResumeQueue + drainer (reap-notify spec Part B, R2.2–R2.11) ──
|
|
5695
|
+
// Ships enabled + dryRun (observe-only) as CODE defaults — deliberately
|
|
5696
|
+
// not in ConfigDefaults so the later fleet flip of the shipped default
|
|
5697
|
+
// takes effect. Classified in DARK_GATE_EXCLUSIONS (cost-bearing).
|
|
5698
|
+
const rqCfg = config.monitoring?.resumeQueue ?? {};
|
|
5699
|
+
let resumeQueue = null;
|
|
5700
|
+
let resumeDrainer = null;
|
|
5701
|
+
// Operator-stop record for the drainer's R2.6 validation (in-memory map +
|
|
5702
|
+
// the durable autonomous-emergency-stop flag file's mtime as global stop).
|
|
5703
|
+
const operatorStopsByTopic = new Map();
|
|
5704
|
+
let globalOperatorStopAt = 0;
|
|
5705
|
+
const recordOperatorStop = (topicId) => {
|
|
5706
|
+
if (topicId == null)
|
|
5707
|
+
globalOperatorStopAt = Date.now();
|
|
5708
|
+
else
|
|
5709
|
+
operatorStopsByTopic.set(topicId, Date.now());
|
|
5710
|
+
};
|
|
5711
|
+
if (rqCfg.enabled ?? true) {
|
|
5712
|
+
const { ResumeQueue } = await import('../monitoring/ResumeQueue.js');
|
|
5713
|
+
const { ResumeQueueDrainer } = await import('../monitoring/ResumeQueueDrainer.js');
|
|
5714
|
+
// Decision-transition audit sink: logs/resume-queue.jsonl, 5MB×2 rotation.
|
|
5715
|
+
const resumeAuditPath = path.join(_projectDir, 'logs', 'resume-queue.jsonl');
|
|
5716
|
+
const auditResumeQueue = (event) => {
|
|
5717
|
+
try {
|
|
5718
|
+
fs.mkdirSync(path.dirname(resumeAuditPath), { recursive: true });
|
|
5719
|
+
try {
|
|
5720
|
+
const st = fs.statSync(resumeAuditPath);
|
|
5721
|
+
if (st.size > 5 * 1024 * 1024) {
|
|
5722
|
+
fs.renameSync(resumeAuditPath, `${resumeAuditPath}.1`); // 5MB×2: .1 replaced each rotation
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
catch { /* no file yet */ }
|
|
5726
|
+
fs.appendFileSync(resumeAuditPath, JSON.stringify(event) + '\n');
|
|
5727
|
+
}
|
|
5728
|
+
catch { /* the audit sink never endangers the queue */ }
|
|
5729
|
+
};
|
|
5730
|
+
// ALL give-up classes fold into ONE rolling deduped attention item (P17).
|
|
5731
|
+
const resumeAggregate = { counts: new Map(), recent: [] };
|
|
5732
|
+
const raiseResumeAggregated = (kind, detail) => {
|
|
5733
|
+
try {
|
|
5734
|
+
resumeAggregate.counts.set(kind, (resumeAggregate.counts.get(kind) ?? 0) + 1);
|
|
5735
|
+
resumeAggregate.recent.push(`[${kind}] ${detail}`);
|
|
5736
|
+
if (resumeAggregate.recent.length > 8)
|
|
5737
|
+
resumeAggregate.recent.shift();
|
|
5738
|
+
if (!telegram)
|
|
5739
|
+
return;
|
|
5740
|
+
const total = [...resumeAggregate.counts.values()].reduce((a, b) => a + b, 0);
|
|
5741
|
+
const breakdown = [...resumeAggregate.counts.entries()].map(([k, c]) => `${k}×${c}`).join(', ');
|
|
5742
|
+
void telegram.createAttentionItem({
|
|
5743
|
+
id: 'resume-queue:aggregate',
|
|
5744
|
+
title: `Resume queue: ${total} notice${total === 1 ? '' : 's'} (${breakdown})`,
|
|
5745
|
+
summary: detail,
|
|
5746
|
+
description: resumeAggregate.recent.join('\n'),
|
|
5747
|
+
category: 'sessions',
|
|
5748
|
+
priority: 'NORMAL', // per-entry HIGH items are forbidden (P17)
|
|
5749
|
+
sourceContext: 'resume-queue',
|
|
5750
|
+
}).catch(() => { });
|
|
5751
|
+
}
|
|
5752
|
+
catch { /* never endanger the caller */ }
|
|
5753
|
+
};
|
|
5754
|
+
resumeQueue = new ResumeQueue({
|
|
5755
|
+
stateDir: path.join(_projectDir, '.instar'),
|
|
5756
|
+
audit: auditResumeQueue,
|
|
5757
|
+
raiseAggregated: raiseResumeAggregated,
|
|
5758
|
+
}, {
|
|
5759
|
+
enabled: rqCfg.enabled ?? true,
|
|
5760
|
+
dryRun: rqCfg.dryRun ?? true, // shipped observe-only (decision 2)
|
|
5761
|
+
maxAttempts: rqCfg.maxAttempts ?? 3,
|
|
5762
|
+
maxResurrections: rqCfg.maxResurrections ?? 2,
|
|
5763
|
+
entryTtlHours: rqCfg.entryTtlHours ?? 24,
|
|
5764
|
+
maxQueueSize: rqCfg.maxQueueSize ?? 50,
|
|
5765
|
+
includeOperatorKills: rqCfg.includeOperatorKills ?? false,
|
|
5766
|
+
});
|
|
5767
|
+
const queueStarted = resumeQueue.start();
|
|
5768
|
+
if (!queueStarted) {
|
|
5769
|
+
console.log(pc.yellow(` ResumeQueue disabled: ${resumeQueue.isDisabled()}`));
|
|
5770
|
+
}
|
|
5771
|
+
else {
|
|
5772
|
+
// Feed the notifier's "restart is queued" line (live, non-dry-run only).
|
|
5773
|
+
const rq = resumeQueue;
|
|
5774
|
+
resumeQueuedForSession = (tmuxSession) => rq.hasLiveQueuedEntryFor(tmuxSession);
|
|
5775
|
+
const resolveTopicForTmux = (tmuxSession) => {
|
|
5776
|
+
try {
|
|
5777
|
+
const t = telegram?.getTopicForSession(tmuxSession);
|
|
5778
|
+
if (t == null)
|
|
5779
|
+
return null;
|
|
5780
|
+
const n = typeof t === 'number' ? t : Number(t);
|
|
5781
|
+
return Number.isFinite(n) ? n : null;
|
|
5782
|
+
}
|
|
5783
|
+
catch {
|
|
5784
|
+
// @silent-fallback-ok — null = "unbound": the notifier then routes
|
|
5785
|
+
// the session to the lifeline index line, never a dropped notice.
|
|
5786
|
+
return null;
|
|
5787
|
+
}
|
|
5788
|
+
};
|
|
5789
|
+
resumeDrainer = new ResumeQueueDrainer({
|
|
5790
|
+
queue: rq,
|
|
5791
|
+
pressureTier: sharedPressureTier,
|
|
5792
|
+
canSpawnSession: () => (quotaManager ? quotaManager.canSpawnSession().allowed : true),
|
|
5793
|
+
sessionCountOk: () => sessionManager.listRunningSessions().length <
|
|
5794
|
+
(config.maxSessions ?? 10),
|
|
5795
|
+
// No catch: a throwing dep resolves to the SAFE side (blocked)
|
|
5796
|
+
// inside the drainer's gate — wrapping it here would flip the
|
|
5797
|
+
// failure to the lenient side.
|
|
5798
|
+
migrationInFlight: () => quotaManager?.isMigrationInFlight() ?? false,
|
|
5799
|
+
liveSessionForTopic: (topicId) => sessionManager
|
|
5800
|
+
.listRunningSessions()
|
|
5801
|
+
.some((s) => resolveTopicForTmux(s.tmuxSession) === topicId),
|
|
5802
|
+
currentResumeUuid: (topicId) => _topicResumeMap?.get(topicId) ?? null,
|
|
5803
|
+
topicOwnerElsewhere: (topicId) => {
|
|
5804
|
+
// Pool not wired → single-machine → always local. No catch: a
|
|
5805
|
+
// registry error propagates to the drainer's validateReality,
|
|
5806
|
+
// which resolves a throwing dep to the SAFE side (invalidated).
|
|
5807
|
+
const reg = sessionOwnershipRegistry;
|
|
5808
|
+
const self = _meshSelfId;
|
|
5809
|
+
if (!reg || !self)
|
|
5810
|
+
return false;
|
|
5811
|
+
const owner = reg.ownerOf(String(topicId));
|
|
5812
|
+
return !!owner && owner !== self;
|
|
5813
|
+
},
|
|
5814
|
+
topicBindingMatches: (topicId, cwd) => {
|
|
5815
|
+
const bindings = scopeVerifier?.loadTopicBindings?.();
|
|
5816
|
+
const binding = bindings?.[String(topicId)];
|
|
5817
|
+
if (!binding?.projectDir)
|
|
5818
|
+
return true; // unbound topic → default project
|
|
5819
|
+
return path.resolve(cwd).startsWith(path.resolve(binding.projectDir));
|
|
5820
|
+
},
|
|
5821
|
+
operatorStopSince: (topicId, sinceIso) => {
|
|
5822
|
+
const since = Date.parse(sinceIso);
|
|
5823
|
+
const perTopic = operatorStopsByTopic.get(topicId) ?? 0;
|
|
5824
|
+
let flagAt = 0;
|
|
5825
|
+
try {
|
|
5826
|
+
flagAt = fs.statSync(path.join(_projectDir, '.instar', 'autonomous-emergency-stop')).mtimeMs;
|
|
5827
|
+
}
|
|
5828
|
+
catch { /* no flag */ }
|
|
5829
|
+
return Math.max(perTopic, globalOperatorStopAt, flagAt) > since;
|
|
5830
|
+
},
|
|
5831
|
+
jobCheck: (slug, queuedAtIso) => {
|
|
5832
|
+
if (!scheduler)
|
|
5833
|
+
return { ok: false, why: 'scheduler-unavailable' };
|
|
5834
|
+
const job = scheduler.getJobs().find((j) => j.slug === slug);
|
|
5835
|
+
if (!job)
|
|
5836
|
+
return { ok: false, why: 'job-missing' };
|
|
5837
|
+
// 'disabled' also covers CrashLoopPauser-paused jobs — the
|
|
5838
|
+
// pauser's mechanism IS setting enabled=false (+ provenance note).
|
|
5839
|
+
if (!job.enabled)
|
|
5840
|
+
return { ok: false, why: 'job-disabled' };
|
|
5841
|
+
const lastRun = state.getJobState(slug)?.lastRun;
|
|
5842
|
+
if (lastRun && Date.parse(lastRun) > Date.parse(queuedAtIso)) {
|
|
5843
|
+
return { ok: false, why: 'job-ran-since' };
|
|
5844
|
+
}
|
|
5845
|
+
return { ok: true };
|
|
5846
|
+
},
|
|
5847
|
+
pathExists: (p) => fs.existsSync(p),
|
|
5848
|
+
respawnTopic: async (entry, continuationPrompt) => {
|
|
5849
|
+
if (!telegram)
|
|
5850
|
+
throw new Error('telegram adapter not available');
|
|
5851
|
+
return await spawnSessionForTopic(sessionManager, telegram, entry.sessionName, entry.topicId, continuationPrompt, topicMemory, undefined, undefined, undefined, { cwd: entry.worktreePath ?? entry.cwd });
|
|
5852
|
+
},
|
|
5853
|
+
triggerJob: async (slug) => {
|
|
5854
|
+
if (!scheduler)
|
|
5855
|
+
return 'skipped';
|
|
5856
|
+
return await scheduler.triggerJob(slug, 'resume-queue');
|
|
5857
|
+
},
|
|
5858
|
+
spawnAliveAfterGrace: async (tmuxSession) => {
|
|
5859
|
+
await new Promise((resolve) => {
|
|
5860
|
+
const t = setTimeout(resolve, 15_000);
|
|
5861
|
+
if (typeof t.unref === 'function')
|
|
5862
|
+
t.unref();
|
|
5863
|
+
});
|
|
5864
|
+
return sessionManager.isSessionAlive(tmuxSession);
|
|
5865
|
+
},
|
|
5866
|
+
notifyResumed: (entry) => {
|
|
5867
|
+
// R2.11 — honest wording: "restarted", never a transcript-resume
|
|
5868
|
+
// claim (--resume can fall back to a fresh conversation in-pane).
|
|
5869
|
+
if (entry.topicId == null)
|
|
5870
|
+
return;
|
|
5871
|
+
notify('SUMMARY', 'session-resume', `🔁 I restarted this session to pick the work back up after it was shut down mid-work.`, entry.topicId);
|
|
5872
|
+
},
|
|
5873
|
+
raiseAggregated: raiseResumeAggregated,
|
|
5874
|
+
audit: auditResumeQueue,
|
|
5875
|
+
tier1Check: async (entry) => {
|
|
5876
|
+
// Observe-only Tier 1 sanity check via the shared LlmQueue
|
|
5877
|
+
// (P7). Throws when the LLM substrate is unavailable — the
|
|
5878
|
+
// drainer audits that as supervision:'shed'.
|
|
5879
|
+
const q = sharedLlmQueue;
|
|
5880
|
+
const intel = _sharedIntelligence;
|
|
5881
|
+
if (!q || !intel)
|
|
5882
|
+
throw new Error('llm-unavailable');
|
|
5883
|
+
const reasonLiteral = entry.reason.slice(0, 200).replace(/`/g, "'");
|
|
5884
|
+
const prompt = `A session was shut down mid-work and is queued for automatic restart. Given ONLY these ` +
|
|
5885
|
+
`recorded fields, is restarting it sensible? Look for internal contradictions (a "mid-work" ` +
|
|
5886
|
+
`entry whose reason describes completed work; a resurrection history that reads as a crash loop).\n` +
|
|
5887
|
+
`Recorded reason (literal data): \`${reasonLiteral}\`\n` +
|
|
5888
|
+
`Work signals: ${entry.workEvidence.join(', ') || '(none)'}\n` +
|
|
5889
|
+
`Queued: ${entry.queuedAt}; attempts so far: ${entry.attempts}.\n` +
|
|
5890
|
+
`Reply with JSON only: {"sensible": true|false, "reasoning": "<one sentence>"}`;
|
|
5891
|
+
const raw = await q.enqueue('background', (signal) => intel.evaluate(prompt, {
|
|
5892
|
+
model: 'fast',
|
|
5893
|
+
maxTokens: 150,
|
|
5894
|
+
temperature: 0,
|
|
5895
|
+
signal,
|
|
5896
|
+
attribution: { component: 'ResumeQueueDrainer' }, // attribution for /metrics/features
|
|
5897
|
+
}));
|
|
5898
|
+
try {
|
|
5899
|
+
const t = String(raw).trim();
|
|
5900
|
+
const j = t.startsWith('```') ? t.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '') : t;
|
|
5901
|
+
const parsed = JSON.parse(j);
|
|
5902
|
+
return { sensible: parsed.sensible !== false, reasoning: parsed.reasoning };
|
|
5903
|
+
}
|
|
5904
|
+
catch {
|
|
5905
|
+
return { sensible: true, reasoning: 'unparseable verdict — treated as no-concern (observe-only)' };
|
|
5906
|
+
}
|
|
5907
|
+
},
|
|
5908
|
+
}, {
|
|
5909
|
+
drainIntervalSec: rqCfg.drainIntervalSec ?? 60,
|
|
5910
|
+
requiredCalmTicks: rqCfg.requiredCalmTicks ?? 3,
|
|
5911
|
+
maxAttempts: rqCfg.maxAttempts ?? 3,
|
|
5912
|
+
breakerThreshold: rqCfg.breakerThreshold ?? 3,
|
|
5913
|
+
breakerCooldownMin: rqCfg.breakerCooldownMin ?? 30,
|
|
5914
|
+
tier1Check: rqCfg.tier1Check ?? true,
|
|
5915
|
+
});
|
|
5916
|
+
resumeDrainer.start();
|
|
5917
|
+
console.log(pc.green(` ResumeQueue started (${rqCfg.dryRun ?? true ? 'dry-run observe-only' : 'LIVE'}; drainer ${rqCfg.drainIntervalSec ?? 60}s tick)`));
|
|
5918
|
+
// Boot reconciliation half 2 (R2.4): re-enqueue recent mid-work reaps
|
|
5919
|
+
// the queue lost to a crash window. Deferred 30s so the Telegram
|
|
5920
|
+
// adapter (topic resolution) exists; topic-bound candidates only —
|
|
5921
|
+
// job entries rely on cron recurrence and opt-in we cannot
|
|
5922
|
+
// reconstruct from the reap-log.
|
|
5923
|
+
const reconcileTimer = setTimeout(() => {
|
|
5924
|
+
try {
|
|
5925
|
+
const ttlMs = (rqCfg.entryTtlHours ?? 24) * 3600_000;
|
|
5926
|
+
const cutoff = Date.now() - ttlMs;
|
|
5927
|
+
const candidates = reapLog
|
|
5928
|
+
.read(1000)
|
|
5929
|
+
.filter((en) => en.type === 'reaped' &&
|
|
5930
|
+
en.midWork === true &&
|
|
5931
|
+
en.disposition === 'terminal' &&
|
|
5932
|
+
en.origin === 'autonomous' &&
|
|
5933
|
+
Date.parse(en.ts) > cutoff)
|
|
5934
|
+
.map((en) => {
|
|
5935
|
+
const topicId = resolveTopicForTmux(en.tmuxSession);
|
|
5936
|
+
return {
|
|
5937
|
+
sessionName: en.session,
|
|
5938
|
+
tmuxSession: en.tmuxSession,
|
|
5939
|
+
topicId,
|
|
5940
|
+
resumeUuid: topicId != null ? (_topicResumeMap?.get(topicId) ?? null) : null,
|
|
5941
|
+
cwd: _projectDir,
|
|
5942
|
+
reason: en.reason,
|
|
5943
|
+
disposition: 'terminal',
|
|
5944
|
+
origin: 'autonomous',
|
|
5945
|
+
workEvidence: en.workEvidence ?? [],
|
|
5946
|
+
};
|
|
5947
|
+
});
|
|
5948
|
+
const enqueued = rq.reconcileFromReapLog(candidates);
|
|
5949
|
+
if (enqueued > 0) {
|
|
5950
|
+
console.log(`[resume-queue] boot reconciliation re-enqueued ${enqueued} lost mid-work reap(s) from the reap-log`);
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
catch (err) {
|
|
5954
|
+
console.warn('[resume-queue] boot reconciliation failed (non-fatal):', err);
|
|
5955
|
+
}
|
|
5956
|
+
}, 30_000);
|
|
5957
|
+
if (typeof reconcileTimer.unref === 'function')
|
|
5958
|
+
reconcileTimer.unref();
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5442
5961
|
sessionManager.on('sessionReaped', (e) => {
|
|
5443
5962
|
reapLog.recordReaped({
|
|
5444
5963
|
session: e.session.name,
|
|
@@ -5453,8 +5972,42 @@ export async function startServer(options) {
|
|
|
5453
5972
|
// which billing lane the reaped session ran on so the soak can confirm
|
|
5454
5973
|
// rerouted sessions reach their completion from the reap-log too.
|
|
5455
5974
|
...(e.session.launchLane ? { launchLane: e.session.launchLane } : {}),
|
|
5975
|
+
// Mid-work stamp (reap-notify spec R2.1) — evidence clamped at the
|
|
5976
|
+
// chokepoint; the reap-log row is the boot-reconciliation source of truth.
|
|
5977
|
+
...(e.midWork !== undefined ? { midWork: e.midWork } : {}),
|
|
5978
|
+
...(e.workEvidence && e.workEvidence.length > 0 ? { workEvidence: e.workEvidence } : {}),
|
|
5456
5979
|
});
|
|
5457
|
-
|
|
5980
|
+
// Enqueue hook (reap-notify R2.2): every terminal autonomous reap is
|
|
5981
|
+
// OFFERED to the resume queue; eligibility (evidence classes, job
|
|
5982
|
+
// opt-in, operator exclusion, resurrection cap) is decided inside.
|
|
5983
|
+
// Runs BEFORE the notifier so the "restart is queued" line can see the
|
|
5984
|
+
// fresh entry. Never endangers the kill path.
|
|
5985
|
+
try {
|
|
5986
|
+
if (resumeQueue && !resumeQueue.isDisabled()) {
|
|
5987
|
+
const rawTopic = telegram?.getTopicForSession(e.session.tmuxSession);
|
|
5988
|
+
const topicId = rawTopic == null ? null : Number.isFinite(Number(rawTopic)) ? Number(rawTopic) : null;
|
|
5989
|
+
const jobDef = e.session.jobSlug
|
|
5990
|
+
? scheduler?.getJobs().find((j) => j.slug === e.session.jobSlug)
|
|
5991
|
+
: undefined;
|
|
5992
|
+
resumeQueue.considerEnqueue({
|
|
5993
|
+
sessionName: e.session.name,
|
|
5994
|
+
tmuxSession: e.session.tmuxSession,
|
|
5995
|
+
topicId,
|
|
5996
|
+
jobSlug: e.session.jobSlug,
|
|
5997
|
+
jobResumeOptIn: jobDef?.resumeOnReap === true,
|
|
5998
|
+
resumeUuid: topicId != null ? (_topicResumeMap?.get(topicId) ?? null) : null,
|
|
5999
|
+
cwd: e.session.cwd ?? _projectDir,
|
|
6000
|
+
reason: e.reason,
|
|
6001
|
+
disposition: e.disposition ?? 'terminal',
|
|
6002
|
+
origin: e.origin ?? 'autonomous',
|
|
6003
|
+
workEvidence: e.workEvidence ?? [],
|
|
6004
|
+
});
|
|
6005
|
+
}
|
|
6006
|
+
}
|
|
6007
|
+
catch (err) {
|
|
6008
|
+
console.warn('[resume-queue] enqueue hook raised (non-fatal):', err);
|
|
6009
|
+
}
|
|
6010
|
+
reapNotifier.onReaped({ session: e.session, reason: e.reason, disposition: e.disposition, origin: e.origin, midWork: e.midWork, workEvidence: e.workEvidence });
|
|
5458
6011
|
// Coherence journal 'reaped' (§3.3): emitted HERE, alongside the
|
|
5459
6012
|
// reap-log append it references — never derived in the saveSession
|
|
5460
6013
|
// funnel (which records the plain killed/completed transition).
|
|
@@ -5484,6 +6037,104 @@ export async function startServer(options) {
|
|
|
5484
6037
|
// lifeline to restart the server in a tight loop.
|
|
5485
6038
|
await sessionManager.purgeDeadSessions();
|
|
5486
6039
|
sessionManager.startMonitoring();
|
|
6040
|
+
// ── Durable Inbound Message Queue: unconditional boot sweep (§5.3) ──
|
|
6041
|
+
// MUST run BEFORE recoverPendingInjects (boot ordering, spec §3.4): the
|
|
6042
|
+
// sweep consults injection receipts and vetoes PIS records for
|
|
6043
|
+
// operator-stop rows — running PIS recovery first would replay an inject
|
|
6044
|
+
// the operator stopped. Keyed on store-file existence; fail-open
|
|
6045
|
+
// (quarantine) on a corrupt store; gate-expires all custody with a NAMED
|
|
6046
|
+
// reason when the drain will not run this boot. The mesh-identity gate is
|
|
6047
|
+
// resolved later (the mesh block) — when the wiring there finds no
|
|
6048
|
+
// identity, it runs the same expire-all with reason no-mesh-identity
|
|
6049
|
+
// before any drain could have started.
|
|
6050
|
+
try {
|
|
6051
|
+
const sweepMod = await import('../core/inboundQueueBootSweep.js');
|
|
6052
|
+
const pisMod = await import('../core/PendingInjectStore.js');
|
|
6053
|
+
const sweepPis = new pisMod.PendingInjectStore(path.join(config.stateDir, 'state'));
|
|
6054
|
+
const pisRecordsForTopic = (sessionKey) => sweepPis.list().records.filter((r) => String(r.telegramTopicId ?? '') === sessionKey);
|
|
6055
|
+
const qRaw = config.multiMachine?.sessionPool?.inboundQueue;
|
|
6056
|
+
const poolRaw = config.multiMachine?.sessionPool;
|
|
6057
|
+
const queueWillRun = qRaw?.enabled !== true ? { run: false, gateReason: 'feature-disabled' }
|
|
6058
|
+
: qRaw?.dryRun !== false ? { run: false, gateReason: 'dry-run' }
|
|
6059
|
+
: (poolRaw?.enabled !== true || (poolRaw?.stage ?? 'dark') === 'dark') ? { run: false, gateReason: 'pool-dark' }
|
|
6060
|
+
: { run: true };
|
|
6061
|
+
const sweepRes = sweepMod.runInboundQueueBootSweep({
|
|
6062
|
+
stateDir: config.stateDir,
|
|
6063
|
+
agentId: config.projectName ?? 'agent',
|
|
6064
|
+
queueWillRun,
|
|
6065
|
+
hasPisRecord: (sk) => pisRecordsForTopic(sk).length > 0,
|
|
6066
|
+
clearPisRecord: (sk) => { for (const r of pisRecordsForTopic(sk))
|
|
6067
|
+
sweepPis.clear(r.tmuxSession); },
|
|
6068
|
+
reportLoss: (items, reason) => {
|
|
6069
|
+
const topics = [...new Set(items.map((i) => i.sessionKey))].join(', ');
|
|
6070
|
+
notify('SUMMARY', 'inbound-queue', `I didn't get to ${items.length} queued message(s) (${reason}; topics: ${topics}) — resend anything still needed.`);
|
|
6071
|
+
},
|
|
6072
|
+
reportPossiblyNotInjected: (items) => {
|
|
6073
|
+
const topics = [...new Set(items.map((i) => i.sessionKey))].join(', ');
|
|
6074
|
+
notify('SUMMARY', 'inbound-queue', `${items.length} message(s) may not have been injected before a crash (topics: ${topics}) — if a message went unanswered, resend it.`);
|
|
6075
|
+
},
|
|
6076
|
+
raiseAttention: (title, body) => notify('IMMEDIATE', 'inbound-queue', `${title}: ${body}`),
|
|
6077
|
+
log: (line) => console.log(pc.dim(` ${line}`)),
|
|
6078
|
+
nowMs: () => Date.now(),
|
|
6079
|
+
});
|
|
6080
|
+
_sweptInboundStore = sweepRes.store;
|
|
6081
|
+
if (sweepRes.storePresent) {
|
|
6082
|
+
console.log(pc.dim(` [inbound-queue] boot sweep: gateExpired=${sweepRes.gateExpired} recovered=${sweepRes.recoveredToQueued} ` +
|
|
6083
|
+
`delivered=${sweepRes.settledDelivered} pni=${sweepRes.possiblyNotInjected} pisVetoed=${sweepRes.pisVetoed} quarantined=${sweepRes.quarantined}`));
|
|
6084
|
+
}
|
|
6085
|
+
// Engine-never-constructed backstop (second-pass concern 1 — the
|
|
6086
|
+
// no-mesh-identity gate, made unreachable-proof): the sweep ran with
|
|
6087
|
+
// run:true expecting the mesh block to construct the drain. If, 90s into
|
|
6088
|
+
// boot, the swept store is still unadopted (no mesh identity, or the
|
|
6089
|
+
// construction threw), expire its custody with the NAMED reason and
|
|
6090
|
+
// close the handle — custody is NEVER silently stranded (§5.3), through
|
|
6091
|
+
// ANY non-construction path. One-shot; a no-op when the engine is live.
|
|
6092
|
+
if (sweepRes.store) {
|
|
6093
|
+
const orphanTimer = setTimeout(() => {
|
|
6094
|
+
if (!_sweptInboundStore || _inboundQueue)
|
|
6095
|
+
return;
|
|
6096
|
+
try {
|
|
6097
|
+
const store = _sweptInboundStore;
|
|
6098
|
+
const nowIso = new Date().toISOString();
|
|
6099
|
+
const rows = store.listNonTerminal();
|
|
6100
|
+
const dropped = [];
|
|
6101
|
+
for (const row of rows) {
|
|
6102
|
+
if (store.transition(row.enqueue_seq, row.state, 'expired', { nowIso, terminalReason: 'gate:no-mesh-identity' })) {
|
|
6103
|
+
dropped.push(row.session_key);
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
if (dropped.length > 0) {
|
|
6107
|
+
notify('SUMMARY', 'inbound-queue', `I didn't get to ${dropped.length} queued message(s) (the queue is enabled but this machine has no mesh identity, so the drain never started; topics: ${[...new Set(dropped)].join(', ')}) — resend anything still needed.`);
|
|
6108
|
+
}
|
|
6109
|
+
store.close();
|
|
6110
|
+
_sweptInboundStore = null;
|
|
6111
|
+
console.warn('[inbound-queue] swept store never adopted by an engine — custody gate-expired (no-mesh-identity) and store closed');
|
|
6112
|
+
}
|
|
6113
|
+
catch (e) {
|
|
6114
|
+
console.warn(`[inbound-queue] orphan-store backstop failed (next boot's sweep retries): ${e instanceof Error ? e.message : String(e)}`);
|
|
6115
|
+
}
|
|
6116
|
+
}, 90_000);
|
|
6117
|
+
orphanTimer.unref?.();
|
|
6118
|
+
}
|
|
6119
|
+
}
|
|
6120
|
+
catch (err) {
|
|
6121
|
+
// The sweep is a recovery backstop — its own failure must never block
|
|
6122
|
+
// boot. The next boot retries; rows are durable.
|
|
6123
|
+
console.error(`[server] inbound-queue boot sweep failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
6124
|
+
}
|
|
6125
|
+
// Hold-for-stability effective-state getter (§4.2) — registered on the
|
|
6126
|
+
// UNCONDITIONAL boot path (same path as the sweep above), as a closure
|
|
6127
|
+
// over the effective-state computation: always-failover default ⇒
|
|
6128
|
+
// enabled:false, so the orphaned-config case (hold on, queue off/dark)
|
|
6129
|
+
// derives /guards off-runtime-divergent instead of on-unverified.
|
|
6130
|
+
try {
|
|
6131
|
+
guardRegistry.register('multiMachine.sessionPool.holdForStability.enabled', () => ({
|
|
6132
|
+
enabled: config.multiMachine?.sessionPool?.holdForStability?.enabled === true &&
|
|
6133
|
+
_inboundQueue !== null,
|
|
6134
|
+
lastTickAt: Date.now(),
|
|
6135
|
+
}));
|
|
6136
|
+
}
|
|
6137
|
+
catch { /* posture observability never blocks boot */ }
|
|
5487
6138
|
// Pending-inject recovery (finding 8d300555): re-deliver initial messages
|
|
5488
6139
|
// orphaned by the previous server process dying in the spawn→ready→inject
|
|
5489
6140
|
// window (the auto-updater restart race). Runs AFTER the purge so dead
|
|
@@ -8248,6 +8899,13 @@ export async function startServer(options) {
|
|
|
8248
8899
|
semanticMemory?.checkpoint();
|
|
8249
8900
|
}
|
|
8250
8901
|
catch { /* non-critical */ }
|
|
8902
|
+
// Durable Inbound Message Queue §6: sleep-shift backoff deadlines by the
|
|
8903
|
+
// nap span + the nap clamp (stale rows expire reported). Low wake
|
|
8904
|
+
// confidence → the conservative branch (clamp without shift).
|
|
8905
|
+
try {
|
|
8906
|
+
void _inboundQueue?.onWake(event.sleepDurationSeconds * 1000, event.lowConfidence ? 'low' : 'high');
|
|
8907
|
+
}
|
|
8908
|
+
catch { /* the backstop tick covers a missed wake trigger */ }
|
|
8251
8909
|
// Re-validate tmux sessions
|
|
8252
8910
|
try {
|
|
8253
8911
|
const tmuxPath = detectTmuxPath();
|
|
@@ -8626,6 +9284,13 @@ export async function startServer(options) {
|
|
|
8626
9284
|
}
|
|
8627
9285
|
return null; // Normal messages pass through
|
|
8628
9286
|
};
|
|
9287
|
+
// Durable Inbound Message Queue §3.6: emergency stop reaches custody.
|
|
9288
|
+
telegram.onSentinelStopCustody = (topicId) => {
|
|
9289
|
+
try {
|
|
9290
|
+
_inboundQueueStop?.(String(topicId));
|
|
9291
|
+
}
|
|
9292
|
+
catch { /* best-effort */ }
|
|
9293
|
+
};
|
|
8629
9294
|
telegram.onSentinelKillSession = (sessionName) => {
|
|
8630
9295
|
// Save resume UUID before killing so respawn can --resume
|
|
8631
9296
|
if (_topicResumeMap) {
|
|
@@ -11214,6 +11879,28 @@ export async function startServer(options) {
|
|
|
11214
11879
|
loadAvg: osMod.loadavg()[0],
|
|
11215
11880
|
quotaState: selfQuotaState(),
|
|
11216
11881
|
guardPosture: selfGuardPosture(),
|
|
11882
|
+
// Durable Inbound Message Queue §5.1: depth + oldest + tenure +
|
|
11883
|
+
// bounded top-K — the survivor's loss-SUSPECTED item, capped
|
|
11884
|
+
// re-placement arm, and supersede-dedupe key all read these.
|
|
11885
|
+
// Absent while the queue is dark — depth honestly unknown.
|
|
11886
|
+
...(() => {
|
|
11887
|
+
try {
|
|
11888
|
+
if (!_inboundQueue)
|
|
11889
|
+
return {};
|
|
11890
|
+
const snap = _inboundQueue.snapshot();
|
|
11891
|
+
return {
|
|
11892
|
+
inboundQueue: {
|
|
11893
|
+
queueDepth: snap.counts.queued + snap.counts.claimed,
|
|
11894
|
+
oldestQueuedAt: snap.counts.oldestQueuedAt,
|
|
11895
|
+
tenure: snap.tenure,
|
|
11896
|
+
topK: _inboundQueue.topKSessionDepths(10),
|
|
11897
|
+
},
|
|
11898
|
+
};
|
|
11899
|
+
}
|
|
11900
|
+
catch {
|
|
11901
|
+
return {};
|
|
11902
|
+
}
|
|
11903
|
+
})(),
|
|
11217
11904
|
});
|
|
11218
11905
|
const hbApi = machineHeartbeat?.api;
|
|
11219
11906
|
if (hbApi) {
|
|
@@ -11318,6 +12005,15 @@ export async function startServer(options) {
|
|
|
11318
12005
|
// cas-pairing lint (scripts/lint-cas-emit-placement.js) fails CI on
|
|
11319
12006
|
// any cas( call site missing this pairing.
|
|
11320
12007
|
const emitPlacement = (sessionKey, r, reason, prevOwner) => {
|
|
12008
|
+
// Durable Inbound Message Queue §3.2 event trigger: an ownership
|
|
12009
|
+
// transition for a session with queued entries makes its head due
|
|
12010
|
+
// now (scoped reset + a drain pass). Fire-and-forget; the engine
|
|
12011
|
+
// no-ops when the session has nothing queued.
|
|
12012
|
+
try {
|
|
12013
|
+
if (r?.ok)
|
|
12014
|
+
void _inboundQueue?.onOwnershipTransition(sessionKey);
|
|
12015
|
+
}
|
|
12016
|
+
catch { /* trigger is best-effort; the backstop tick covers it */ }
|
|
11321
12017
|
try {
|
|
11322
12018
|
if (!coherenceJournal || !r?.ok)
|
|
11323
12019
|
return;
|
|
@@ -11373,6 +12069,15 @@ export async function startServer(options) {
|
|
|
11373
12069
|
const deliverMessageHandler = deliverMod.createDeliverMessageHandler({
|
|
11374
12070
|
ownerEpochOf: (s) => ownReg.read(s)?.ownershipEpoch ?? null,
|
|
11375
12071
|
recordReceipt: (messageId, session) => {
|
|
12072
|
+
// Durable Inbound Message Queue §3.4 remote path: the queue store's
|
|
12073
|
+
// remote receipt (canonical-id keyed, carries the `injected` marker
|
|
12074
|
+
// that makes peer-crash-between-receipt-and-inject boot-detectable —
|
|
12075
|
+
// loss window 6). Recorded ALONGSIDE the existing ledger receipt;
|
|
12076
|
+
// engine dark → the prior posture, named in the spec's skew note.
|
|
12077
|
+
try {
|
|
12078
|
+
_inboundQueue?.recordRemoteReceipt(session, messageId);
|
|
12079
|
+
}
|
|
12080
|
+
catch { /* receipt best-effort; ledger is authoritative for dedupe */ }
|
|
11376
12081
|
if (messageLedger)
|
|
11377
12082
|
return messageLedger.record(messageId, { platform: 'mesh', topic: session }).firstSeen;
|
|
11378
12083
|
if (deliverSeenFallback.has(messageId))
|
|
@@ -11380,6 +12085,23 @@ export async function startServer(options) {
|
|
|
11380
12085
|
deliverSeenFallback.add(messageId);
|
|
11381
12086
|
return true;
|
|
11382
12087
|
},
|
|
12088
|
+
// §3.4 sender re-validation (per-machine registries can diverge during
|
|
12089
|
+
// a deauthorization): a carried envelope whose userId no longer
|
|
12090
|
+
// resolves on THIS machine NACKs `sender-rejected`. Envelope absent
|
|
12091
|
+
// (old peer / live local frame) → not consulted.
|
|
12092
|
+
validateSender: (envelope) => {
|
|
12093
|
+
const uid = Number(envelope.userId);
|
|
12094
|
+
if (!Number.isFinite(uid) || uid === 0)
|
|
12095
|
+
return true;
|
|
12096
|
+
// THIS machine's registry (file-backed; the telegram block's
|
|
12097
|
+
// instance is scoped there — same files, same truth).
|
|
12098
|
+
try {
|
|
12099
|
+
return new UserManager(config.stateDir, config.users).resolveFromTelegramUserId(uid) !== null;
|
|
12100
|
+
}
|
|
12101
|
+
catch {
|
|
12102
|
+
return true;
|
|
12103
|
+
}
|
|
12104
|
+
},
|
|
11383
12105
|
// Owner-side bridge (§L4 handoff): a forwarded message landed → spawn/resume
|
|
11384
12106
|
// the local session for the topic so the conversation continues on THIS machine.
|
|
11385
12107
|
// Only fires for a FIRST-seen forwarded deliverMessage (the ledger dedupes
|
|
@@ -11423,8 +12145,17 @@ export async function startServer(options) {
|
|
|
11423
12145
|
if (existing && sessionManager.isSessionAlive(existing)) {
|
|
11424
12146
|
if (text) {
|
|
11425
12147
|
console.log(pc.green(` [session-pool] owner-side inject for forwarded topic ${topicId} → ${existing}`));
|
|
11426
|
-
|
|
11427
|
-
|
|
12148
|
+
// Loss window 6 (Durable Inbound Message Queue §3.4 remote):
|
|
12149
|
+
// flip the receipt's injected marker on success; a CAUGHT
|
|
12150
|
+
// failure reports at error time — never silent.
|
|
12151
|
+
try {
|
|
12152
|
+
sessionManager.injectTelegramMessage(existing, topicId, text, tg.getTopicName?.(topicId) ?? undefined);
|
|
12153
|
+
tg.trackMessageInjection(topicId, existing, text);
|
|
12154
|
+
_inboundQueue?.markRemoteInjected(cmd.session, cmd.messageId);
|
|
12155
|
+
}
|
|
12156
|
+
catch (err) {
|
|
12157
|
+
_inboundQueue?.reportPeerInjectError(cmd.session, cmd.messageId, err instanceof Error ? err.message : String(err));
|
|
12158
|
+
}
|
|
11428
12159
|
}
|
|
11429
12160
|
return;
|
|
11430
12161
|
}
|
|
@@ -11458,8 +12189,13 @@ export async function startServer(options) {
|
|
|
11458
12189
|
.then((name) => {
|
|
11459
12190
|
tg.registerTopicSession(topicId, name, spawnName);
|
|
11460
12191
|
console.log(pc.green(` [session-pool] owner-side resume for forwarded topic ${topicId} → ${name}`));
|
|
12192
|
+
// Loss window 6: the forwarded message reached a real session.
|
|
12193
|
+
_inboundQueue?.markRemoteInjected(cmd.session, cmd.messageId);
|
|
11461
12194
|
})
|
|
11462
|
-
.catch((err) =>
|
|
12195
|
+
.catch((err) => {
|
|
12196
|
+
console.warn(` [session-pool] owner-side resume failed for topic ${topicId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
12197
|
+
_inboundQueue?.reportPeerInjectError(cmd.session, cmd.messageId, err instanceof Error ? err.message : String(err));
|
|
12198
|
+
});
|
|
11463
12199
|
},
|
|
11464
12200
|
});
|
|
11465
12201
|
// ── Secret-sync inbound handler (cross-machine secret distribution, spec Phase 4) ──
|
|
@@ -12287,6 +13023,15 @@ export async function startServer(options) {
|
|
|
12287
13023
|
// signal per episode.
|
|
12288
13024
|
const ownerSuspectBreaker = new (await import('../core/OwnerSuspectBreaker.js')).OwnerSuspectBreaker({
|
|
12289
13025
|
logger: (m) => console.log(pc.dim(m)),
|
|
13026
|
+
// Durable Inbound Message Queue §3.2: breaker close delivers held
|
|
13027
|
+
// rows instantly. Engine constructed later in this block — the
|
|
13028
|
+
// closure reads at fire time.
|
|
13029
|
+
onClose: () => { void _inboundQueue?.onBreakerClose(); },
|
|
13030
|
+
flapThresholdPerHour: config.multiMachine?.sessionPool?.holdForStability?.flapThresholdPerHour ?? 6,
|
|
13031
|
+
reportFlapEpisode: ({ machineId, episodesLastHour }) => {
|
|
13032
|
+
const nickname = machinePoolRegistry?.getCapacity(machineId)?.nickname ?? machineId;
|
|
13033
|
+
notify('SUMMARY', 'inbound-queue', `"${nickname}" is flapping (${episodesLastHour} suspect episodes in the last hour) — holds are disabled for it until it calms; its conversations move on the usual failover path.`);
|
|
13034
|
+
},
|
|
12290
13035
|
reportSustained: ({ machineId, suspectForMs, marks }) => {
|
|
12291
13036
|
DegradationReporter.getInstance().report({
|
|
12292
13037
|
feature: 'SessionPool.ownerDelivery',
|
|
@@ -12352,7 +13097,10 @@ export async function startServer(options) {
|
|
|
12352
13097
|
const url = peerUrl(target);
|
|
12353
13098
|
if (!url)
|
|
12354
13099
|
throw new Error(`no peer url for ${target}`);
|
|
12355
|
-
|
|
13100
|
+
// senderEnvelope (Durable Inbound Message Queue §2.2): a drained
|
|
13101
|
+
// forward carries the STORED sender frame; an old peer ignores
|
|
13102
|
+
// the extra field (version skew named in the spec).
|
|
13103
|
+
const res = await meshClient.send({ machineId: target, url }, { type: 'deliverMessage', session: env.sessionKey, messageId: env.messageId, payload: env.payload, ownershipEpoch: env.ownershipEpoch, ...(env.senderEnvelope ? { senderEnvelope: env.senderEnvelope } : {}) }, env.ownershipEpoch);
|
|
12356
13104
|
if (res.ok && res.result && typeof res.result === 'object' && 'accepted' in res.result) {
|
|
12357
13105
|
return res.result;
|
|
12358
13106
|
}
|
|
@@ -12365,11 +13113,192 @@ export async function startServer(options) {
|
|
|
12365
13113
|
await meshClient.send({ machineId, url }, { type: 'deliverMessage', session: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch: 0 }, 0);
|
|
12366
13114
|
},
|
|
12367
13115
|
handleLocally: async () => { },
|
|
12368
|
-
|
|
13116
|
+
// Durable Inbound Message Queue §2.2: tri-state custody taking.
|
|
13117
|
+
// Null engine (dark / gate failed / invariants violated) → 'refused'
|
|
13118
|
+
// → the router leaves acked:false → today's fall-through. enqueueLive
|
|
13119
|
+
// never throws (storage failure maps to 'refused' — fail-safe).
|
|
13120
|
+
queueMessage: (msg, reason) => {
|
|
13121
|
+
if (!_inboundQueue)
|
|
13122
|
+
return 'refused';
|
|
13123
|
+
const out = _inboundQueue.enqueueLive({
|
|
13124
|
+
sessionKey: msg.sessionKey,
|
|
13125
|
+
messageId: msg.messageId,
|
|
13126
|
+
payload: typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload ?? ''),
|
|
13127
|
+
senderEnvelope: msg.senderEnvelope ?? null,
|
|
13128
|
+
topicMetadata: msg.topicMetadata,
|
|
13129
|
+
}, reason);
|
|
13130
|
+
return out.result;
|
|
13131
|
+
},
|
|
12369
13132
|
raiseAttention: (title, body) => console.log(pc.dim(` [session-router] attention: ${title} — ${body}`)),
|
|
12370
13133
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
12371
13134
|
log: (line) => console.log(pc.dim(` [session-router] ${line}`)),
|
|
12372
13135
|
});
|
|
13136
|
+
// ── Durable Inbound Message Queue: engine construction (gated) ──
|
|
13137
|
+
// Gate: pool active (this block) + mesh identity (this block) +
|
|
13138
|
+
// inboundQueue.enabled + !dryRun + the six config-seam invariants.
|
|
13139
|
+
// A violated invariant keeps the queue OFF for the boot — one loud
|
|
13140
|
+
// config-error per violated inequality + one attention item, never a
|
|
13141
|
+
// half-configured queue (spec §Config).
|
|
13142
|
+
try {
|
|
13143
|
+
const iqcMod = await import('../core/inboundQueueConfig.js');
|
|
13144
|
+
const qcfg = { ...iqcMod.DEFAULT_INBOUND_QUEUE_CONFIG, ...(config.multiMachine?.sessionPool?.inboundQueue ?? {}) };
|
|
13145
|
+
const hcfg = { ...iqcMod.DEFAULT_HOLD_FOR_STABILITY_CONFIG, ...(config.multiMachine?.sessionPool?.holdForStability ?? {}) };
|
|
13146
|
+
// Dry-run constructs the engine too (second-pass concern 2): the
|
|
13147
|
+
// §2.4 dry-run branch never takes custody, but its durable
|
|
13148
|
+
// wouldEnqueue/wouldHold/wouldRefuse counters ARE the promotion
|
|
13149
|
+
// evidence and /pool/queue must serve them. The boot sweep already
|
|
13150
|
+
// gate-expired any live→dry-run residual rows.
|
|
13151
|
+
if (qcfg.enabled && _sessionPoolStage() !== 'dark') {
|
|
13152
|
+
const inv = iqcMod.validateInboundQueueInvariants(qcfg, hcfg);
|
|
13153
|
+
if (!inv.ok) {
|
|
13154
|
+
for (const v of inv.violations) {
|
|
13155
|
+
console.error(`[inbound-queue] CONFIG ERROR — invariant ${v.invariant} (${v.name}): ${v.message}`);
|
|
13156
|
+
}
|
|
13157
|
+
notify('IMMEDIATE', 'inbound-queue', `Inbound queue NOT started — ${inv.violations.length} config invariant(s) violated (${inv.violations.map((v) => v.name).join(', ')}). The queue stays OFF; messages use today's delivery path.`);
|
|
13158
|
+
_sweptInboundStore?.close();
|
|
13159
|
+
_sweptInboundStore = null;
|
|
13160
|
+
}
|
|
13161
|
+
else {
|
|
13162
|
+
const qdlMod = await import('../core/QueueDrainLoop.js');
|
|
13163
|
+
const pisStoreMod = await import('../core/PendingInboundStore.js');
|
|
13164
|
+
const pisMod2 = await import('../core/PendingInjectStore.js');
|
|
13165
|
+
const queuePis = new pisMod2.PendingInjectStore(path.join(config.stateDir, 'state'));
|
|
13166
|
+
const store = _sweptInboundStore ?? pisStoreMod.PendingInboundStore.open(config.projectName ?? 'agent', config.stateDir);
|
|
13167
|
+
const bootSessionId = `${meshSelfId}:${Date.now()}`;
|
|
13168
|
+
// §4.2 hold verdict — effective state honesty: always-'failover'
|
|
13169
|
+
// when the policy is off or the queue is dry-run (unreachable
|
|
13170
|
+
// here: dryRun gates construction) — config-coupled both ways.
|
|
13171
|
+
const holdOn = hcfg.enabled === true;
|
|
13172
|
+
const holdVerdict = (sessionKey) => {
|
|
13173
|
+
if (!holdOn)
|
|
13174
|
+
return 'failover';
|
|
13175
|
+
const own = ownReg.read(sessionKey);
|
|
13176
|
+
const owner = own ? ownReg.ownerOf(sessionKey) : null;
|
|
13177
|
+
if (!owner || owner === meshSelfId)
|
|
13178
|
+
return 'deliver';
|
|
13179
|
+
const cap = machinePoolRegistry?.getCapacity(owner);
|
|
13180
|
+
if (!cap?.online)
|
|
13181
|
+
return 'failover'; // heartbeat offline — dead is dead
|
|
13182
|
+
if (ownerSuspectBreaker.isFlapping(owner))
|
|
13183
|
+
return 'failover'; // §4.4
|
|
13184
|
+
if (ownerSuspectBreaker.isSuspect(owner))
|
|
13185
|
+
return 'hold'; // suspect + online
|
|
13186
|
+
return 'deliver'; // not suspect (exhaustion site) — enqueue-and-drain
|
|
13187
|
+
};
|
|
13188
|
+
const stoppedTopics = new Set();
|
|
13189
|
+
_inboundQueue = new qdlMod.QueueDrainLoop({
|
|
13190
|
+
store,
|
|
13191
|
+
qcfg,
|
|
13192
|
+
hcfg,
|
|
13193
|
+
selfMachineId: meshSelfId,
|
|
13194
|
+
// The real serving-lease signal (second-pass concern 3): when
|
|
13195
|
+
// a lease coordinator exists, custody is gated on actually
|
|
13196
|
+
// HOLDING the lease (§2.2 — custody only where it can be
|
|
13197
|
+
// drained); a single-machine install with no coordinator
|
|
13198
|
+
// defaults true (the single-router topology).
|
|
13199
|
+
holdsLease: () => (leaseCoordinatorRef ? leaseCoordinatorRef.holdsLease() : true),
|
|
13200
|
+
isStopped: (sk) => stoppedTopics.has(sk),
|
|
13201
|
+
dispatchInbound: async (msg, handover) => {
|
|
13202
|
+
if (!_drainLocalDeliver)
|
|
13203
|
+
return { kind: 'un-routable', reason: 'local-tail-not-wired' };
|
|
13204
|
+
return _drainLocalDeliver(msg, handover);
|
|
13205
|
+
},
|
|
13206
|
+
forceReplace: async (msg) => {
|
|
13207
|
+
if (!_sessionRouter)
|
|
13208
|
+
return false;
|
|
13209
|
+
return _sessionRouter.forceReplace({
|
|
13210
|
+
sessionKey: msg.sessionKey,
|
|
13211
|
+
messageId: msg.messageId,
|
|
13212
|
+
payload: msg.payload,
|
|
13213
|
+
senderEnvelope: msg.senderEnvelope,
|
|
13214
|
+
topicMetadata: msg.topicMetadata,
|
|
13215
|
+
});
|
|
13216
|
+
},
|
|
13217
|
+
holdVerdict,
|
|
13218
|
+
clearPisRecord: (sk) => {
|
|
13219
|
+
for (const r of queuePis.list().records.filter((x) => String(x.telegramTopicId ?? '') === sk)) {
|
|
13220
|
+
queuePis.clear(r.tmuxSession);
|
|
13221
|
+
}
|
|
13222
|
+
},
|
|
13223
|
+
reportLoss: (items, reason) => {
|
|
13224
|
+
const topics = [...new Set(items.map((i) => i.sessionKey))].join(', ');
|
|
13225
|
+
notify('SUMMARY', 'inbound-queue', `I didn't get to ${items.length} queued message(s) (${reason}; topics: ${topics}) — resend anything still needed.`);
|
|
13226
|
+
},
|
|
13227
|
+
reportPossiblyNotInjected: (items) => {
|
|
13228
|
+
const topics = [...new Set(items.map((i) => i.sessionKey))].join(', ');
|
|
13229
|
+
notify('SUMMARY', 'inbound-queue', `${items.length} message(s) may not have been injected (topics: ${topics}) — if a message went unanswered, resend it.`);
|
|
13230
|
+
},
|
|
13231
|
+
log: (line) => console.log(pc.dim(` ${line}`)),
|
|
13232
|
+
reportDegradation: (reason) => {
|
|
13233
|
+
DegradationReporter.getInstance().report({
|
|
13234
|
+
feature: 'InboundQueue.drain',
|
|
13235
|
+
primary: 'Durable inbound custody drain',
|
|
13236
|
+
fallback: 'Rows remain durable; the Eternal-Sentinel tick keeps retrying',
|
|
13237
|
+
reason,
|
|
13238
|
+
impact: 'Queued inbound messages may deliver late until the tick recovers.',
|
|
13239
|
+
});
|
|
13240
|
+
},
|
|
13241
|
+
now: () => Date.now(),
|
|
13242
|
+
mono: () => performance.now(),
|
|
13243
|
+
bootSessionId,
|
|
13244
|
+
});
|
|
13245
|
+
_inboundQueue.onLeaseAcquired(null);
|
|
13246
|
+
// Expose the stop hook for the emergency-stop integration and
|
|
13247
|
+
// the engine for the /pool/queue route via module state.
|
|
13248
|
+
_inboundQueueStop = (sk) => { stoppedTopics.add(sk); _inboundQueue?.onOperatorStop(sk); };
|
|
13249
|
+
// Backstop tick (Eternal Sentinel — declared in QueueDrainLoop).
|
|
13250
|
+
const tickHandle = setInterval(() => { void _inboundQueue?.tick(); }, qcfg.drainTickMs);
|
|
13251
|
+
tickHandle.unref?.();
|
|
13252
|
+
console.log(pc.dim(` [inbound-queue] engine live — tick every ${qcfg.drainTickMs}ms, tenure ${_inboundQueue.currentTenure()}`));
|
|
13253
|
+
// Survivor arm (spec §5.1, loss window 1): the machine that
|
|
13254
|
+
// holds the routing role checks — once heartbeats have had a
|
|
13255
|
+
// cycle to repopulate — for OFFLINE peers whose last capacity
|
|
13256
|
+
// heartbeat reported nonzero queue depth. ONE loss-SUSPECTED
|
|
13257
|
+
// item per (machine + tenure) episode, then capped synthetic
|
|
13258
|
+
// re-placement for the top-K sessions THROUGH the router path
|
|
13259
|
+
// (PlacementExecutor honors pins; CAS; spawn on the chosen
|
|
13260
|
+
// machine) — SESSION RECOVERY WITHOUT MESSAGE REPLAY, framed
|
|
13261
|
+
// exactly so in the copy. A mesh-less/old peer carries no
|
|
13262
|
+
// depth field — honestly unknown, no item.
|
|
13263
|
+
const survivorEpisodesSeen = new Set();
|
|
13264
|
+
const survivorCheck = () => {
|
|
13265
|
+
try {
|
|
13266
|
+
for (const cap of machinePoolRegistry?.getCapacities() ?? []) {
|
|
13267
|
+
const iq = cap.inboundQueue;
|
|
13268
|
+
if (cap.machineId === meshSelfId || cap.online || !iq || iq.queueDepth <= 0)
|
|
13269
|
+
continue;
|
|
13270
|
+
const episodeKey = `${cap.machineId}|${iq.tenure ?? 'unknown'}`;
|
|
13271
|
+
if (survivorEpisodesSeen.has(episodeKey))
|
|
13272
|
+
continue;
|
|
13273
|
+
survivorEpisodesSeen.add(episodeKey);
|
|
13274
|
+
notify('IMMEDIATE', 'inbound-queue', `"${cap.nickname ?? cap.machineId}" went dark holding ~${iq.queueDepth} queued message(s) — SUSPECTED loss (it may have delivered some before going dark; last heartbeat ${cap.selfReportedLastSeen ?? 'unknown'}). I'm re-opening its top conversations on a healthy machine — session recovery WITHOUT message replay: the queued messages themselves are not recovered, so resend anything still needed.`);
|
|
13275
|
+
const respawnCap = Math.min(qcfg.maxFailoverRespawns, iq.topK.length);
|
|
13276
|
+
for (let i = 0; i < respawnCap; i++) {
|
|
13277
|
+
const sk = iq.topK[i].sessionKey;
|
|
13278
|
+
setTimeout(() => {
|
|
13279
|
+
void _sessionRouter?.forceReplace({ sessionKey: sk, messageId: `survivor-replace:${episodeKey}:${sk}`, payload: '', senderEnvelope: null })
|
|
13280
|
+
.catch(() => { });
|
|
13281
|
+
}, 2000 * i).unref?.(); // staggered
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
}
|
|
13285
|
+
catch { /* survivor check is best-effort observability+recovery */ }
|
|
13286
|
+
};
|
|
13287
|
+
const survivorTimer = setInterval(survivorCheck, 60_000);
|
|
13288
|
+
survivorTimer.unref?.();
|
|
13289
|
+
}
|
|
13290
|
+
}
|
|
13291
|
+
else if (_sweptInboundStore) {
|
|
13292
|
+
// Sweep opened the store expecting a live engine but the stage/
|
|
13293
|
+
// dry-run gate says otherwise (mid-boot config nuance) — close it;
|
|
13294
|
+
// the sweep already settled rows per its own gate verdict.
|
|
13295
|
+
_sweptInboundStore.close();
|
|
13296
|
+
_sweptInboundStore = null;
|
|
13297
|
+
}
|
|
13298
|
+
}
|
|
13299
|
+
catch (err) {
|
|
13300
|
+
console.error(`[inbound-queue] engine construction failed (queue stays OFF): ${err instanceof Error ? err.message : String(err)}`);
|
|
13301
|
+
}
|
|
12373
13302
|
// ── B (HTTP presence transport): pull each peer's self-capacity over
|
|
12374
13303
|
// the signed /mesh/rpc channel and record it into the pool registry.
|
|
12375
13304
|
// This is the credential-less presence path — a standby that cannot
|
|
@@ -12763,7 +13692,7 @@ export async function startServer(options) {
|
|
|
12763
13692
|
carrier: _topicProfileCarrier,
|
|
12764
13693
|
}
|
|
12765
13694
|
: 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 });
|
|
13695
|
+
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, getInboundQueue: () => _inboundQueue, 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
13696
|
// Resolve the late-bound topic-operator getter (increment 2e): routing was
|
|
12768
13697
|
// wired before the server existed; from here on inbound binds use the
|
|
12769
13698
|
// server's own store instance.
|