instar 1.3.1339 → 1.3.1341
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/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +323 -15
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +15 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/BackupManager.d.ts.map +1 -1
- package/dist/core/BackupManager.js +8 -0
- package/dist/core/BackupManager.js.map +1 -1
- package/dist/core/FileClassifier.d.ts.map +1 -1
- package/dist/core/FileClassifier.js +6 -0
- package/dist/core/FileClassifier.js.map +1 -1
- package/dist/core/MachineIdentity.d.ts.map +1 -1
- package/dist/core/MachineIdentity.js +3 -0
- package/dist/core/MachineIdentity.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +58 -2
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/keyMaterialPaths.d.ts +73 -0
- package/dist/core/keyMaterialPaths.d.ts.map +1 -0
- package/dist/core/keyMaterialPaths.js +139 -0
- package/dist/core/keyMaterialPaths.js.map +1 -0
- package/dist/core/types.d.ts +20 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +19 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +47 -9
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginAwareness.d.ts +2 -0
- package/dist/messaging/telegram-origin/OriginAwareness.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginAwareness.js +4 -0
- package/dist/messaging/telegram-origin/OriginAwareness.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginConfig.d.ts +4 -0
- package/dist/messaging/telegram-origin/OriginConfig.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginConfig.js +26 -0
- package/dist/messaging/telegram-origin/OriginConfig.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginForwardToHolder.d.ts +236 -0
- package/dist/messaging/telegram-origin/OriginForwardToHolder.d.ts.map +1 -0
- package/dist/messaging/telegram-origin/OriginForwardToHolder.js +337 -0
- package/dist/messaging/telegram-origin/OriginForwardToHolder.js.map +1 -0
- package/dist/messaging/telegram-origin/OriginMesh.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginMesh.js +17 -2
- package/dist/messaging/telegram-origin/OriginMesh.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginMeshRelay.d.ts +7 -0
- package/dist/messaging/telegram-origin/OriginMeshRelay.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginMeshRelay.js +15 -15
- package/dist/messaging/telegram-origin/OriginMeshRelay.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginStore.d.ts +38 -1
- package/dist/messaging/telegram-origin/OriginStore.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginStore.js +8 -1
- package/dist/messaging/telegram-origin/OriginStore.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginStore.worker.js +1 -1
- package/dist/messaging/telegram-origin/OriginStore.worker.js.map +1 -1
- package/dist/messaging/telegram-origin/OriginStoreBackend.d.ts +58 -1
- package/dist/messaging/telegram-origin/OriginStoreBackend.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/OriginStoreBackend.js +209 -23
- package/dist/messaging/telegram-origin/OriginStoreBackend.js.map +1 -1
- package/dist/messaging/telegram-origin/StoreTypes.d.ts +24 -0
- package/dist/messaging/telegram-origin/StoreTypes.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginBoot.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginBoot.js +7 -0
- package/dist/messaging/telegram-origin/TelegramOriginBoot.js.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginRuntime.d.ts +49 -7
- package/dist/messaging/telegram-origin/TelegramOriginRuntime.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginRuntime.js +178 -2
- package/dist/messaging/telegram-origin/TelegramOriginRuntime.js.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginService.d.ts +8 -0
- package/dist/messaging/telegram-origin/TelegramOriginService.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/TelegramOriginService.js +19 -3
- package/dist/messaging/telegram-origin/TelegramOriginService.js.map +1 -1
- package/dist/messaging/telegram-origin/types.d.ts +12 -0
- package/dist/messaging/telegram-origin/types.d.ts.map +1 -1
- package/dist/messaging/telegram-origin/types.js +6 -0
- package/dist/messaging/telegram-origin/types.js.map +1 -1
- package/dist/monitoring/A2ARedeliverySentinel.d.ts +145 -6
- package/dist/monitoring/A2ARedeliverySentinel.d.ts.map +1 -1
- package/dist/monitoring/A2ARedeliverySentinel.js +451 -11
- package/dist/monitoring/A2ARedeliverySentinel.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +7 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/fileRoutes.d.ts +26 -0
- package/dist/server/fileRoutes.d.ts.map +1 -1
- package/dist/server/fileRoutes.js +204 -56
- package/dist/server/fileRoutes.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +103 -9
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +128 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/dist/threadline/A2ADeliveryTracker.d.ts +55 -1
- package/dist/threadline/A2ADeliveryTracker.d.ts.map +1 -1
- package/dist/threadline/A2ADeliveryTracker.js +97 -5
- package/dist/threadline/A2ADeliveryTracker.js.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.d.ts +15 -0
- package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
- package/dist/threadline/ThreadlineMCPServer.js +3 -0
- package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
- package/dist/threadline/client/ThreadlineClient.d.ts +47 -0
- package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
- package/dist/threadline/client/ThreadlineClient.js +93 -0
- package/dist/threadline/client/ThreadlineClient.js.map +1 -1
- package/dist/threadline/mcp-http-client.d.ts.map +1 -1
- package/dist/threadline/mcp-http-client.js +2 -0
- package/dist/threadline/mcp-http-client.js.map +1 -1
- package/dist/threadline/peerDark.d.ts +165 -0
- package/dist/threadline/peerDark.d.ts.map +1 -0
- package/dist/threadline/peerDark.js +204 -0
- package/dist/threadline/peerDark.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +64 -64
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/scaffold/templates.ts +7 -1
- package/upgrades/1.3.1340.md +32 -0
- package/upgrades/1.3.1341.md +94 -0
- package/upgrades/side-effects/a2a-peer-dark-reporting.md +149 -0
- package/upgrades/side-effects/files-never-serve-key-material.md +399 -0
- package/upgrades/side-effects/telegram-origin-forward-to-holder.md +151 -0
package/dist/commands/server.js
CHANGED
|
@@ -18060,17 +18060,57 @@ export async function startServer(options) {
|
|
|
18060
18060
|
// backoff (body recovered from the canonical outbox), and after the attempt
|
|
18061
18061
|
// cap raises ONE aggregated attention item per dark peer. Recording/sending
|
|
18062
18062
|
// only — no blocking authority. Ships OFF (it re-sends + escalates).
|
|
18063
|
+
// REWORKED (a2a-single-agent-identity §3): constructed when EITHER
|
|
18064
|
+
// `monitoring.a2aRedelivery.enabled` OR the resolved `threadline.peerDarkNotice`
|
|
18065
|
+
// gate is on; `redeliver` only under the former (escalate-only otherwise).
|
|
18066
|
+
// Under the peer-dark gate the operator notice is per PEER with the
|
|
18067
|
+
// deterministic id `a2a-peer-dark:<agent>:<peerFp>`, raised by the AWAKE
|
|
18068
|
+
// machine after it healed its own side, resolved from pool-scope evidence,
|
|
18069
|
+
// 12 h cooldown; a local relay outage collapses to ONE aggregated item.
|
|
18070
|
+
// Dry-run (default) logs would-raise rows to logs/a2a-peer-dark.jsonl.
|
|
18063
18071
|
try {
|
|
18064
18072
|
const a2aDelivCfg = config.monitoring?.a2aRedelivery ?? {};
|
|
18065
|
-
|
|
18073
|
+
const { resolvePeerDarkNoticeConfig, createPeerDarkAuditWriter, resolvePeerDarkAuditPath } = await import('../threadline/peerDark.js');
|
|
18074
|
+
const peerDarkCfg = resolvePeerDarkNoticeConfig(config);
|
|
18075
|
+
if ((a2aDelivCfg.enabled || peerDarkCfg.enabled) && a2aDeliveryTracker) {
|
|
18066
18076
|
const { A2ARedeliverySentinel, DEFAULT_A2A_REDELIVERY_CONFIG } = await import('../monitoring/A2ARedeliverySentinel.js');
|
|
18077
|
+
const priorityMap = {
|
|
18078
|
+
high: 'HIGH', medium: 'NORMAL', low: 'LOW',
|
|
18079
|
+
};
|
|
18080
|
+
// Pool-scope read of one peer's health on my OTHER machines (raise AND
|
|
18081
|
+
// resolve both read it — a peer that answered on another machine is not
|
|
18082
|
+
// dark). Bounded per peer; a dark/slow peer machine is simply absent.
|
|
18083
|
+
const poolPeerHealth = async (peerFp) => {
|
|
18084
|
+
const urls = _resolvePeerUrls?.() ?? [];
|
|
18085
|
+
if (urls.length === 0)
|
|
18086
|
+
return [];
|
|
18087
|
+
const rows = await Promise.allSettled(urls.map(async ({ machineId, url }) => {
|
|
18088
|
+
const ac = new AbortController();
|
|
18089
|
+
const t = setTimeout(() => ac.abort(), 5000);
|
|
18090
|
+
try {
|
|
18091
|
+
const r = await fetch(`${url}/threadline/peers/${encodeURIComponent(peerFp)}/health`, {
|
|
18092
|
+
headers: { Authorization: `Bearer ${config.authToken ?? ''}`, 'X-Instar-AgentId': config.projectName ?? '' },
|
|
18093
|
+
signal: ac.signal,
|
|
18094
|
+
});
|
|
18095
|
+
if (!r.ok)
|
|
18096
|
+
throw new Error(`HTTP ${r.status}`);
|
|
18097
|
+
const b = await r.json();
|
|
18098
|
+
return { machineId, lastAckedAt: b.lastAckedAt ?? null, lastInboundAt: b.lastInboundAt ?? null, lastDeliveredAt: b.lastDeliveredAt ?? null };
|
|
18099
|
+
}
|
|
18100
|
+
finally {
|
|
18101
|
+
clearTimeout(t);
|
|
18102
|
+
}
|
|
18103
|
+
}));
|
|
18104
|
+
return rows.flatMap((r) => (r.status === 'fulfilled' ? [r.value] : []));
|
|
18105
|
+
};
|
|
18067
18106
|
const a2aRedeliverySentinel = new A2ARedeliverySentinel({
|
|
18068
18107
|
tracker: a2aDeliveryTracker,
|
|
18108
|
+
agentId: config.projectName,
|
|
18069
18109
|
// Re-send: recover the body from the canonical outbox by messageId,
|
|
18070
18110
|
// then re-emit via the relay. Body missing → return false (the
|
|
18071
18111
|
// sentinel leaves it awaiting-ack and escalates at the cap, never
|
|
18072
|
-
// fabricating a send).
|
|
18073
|
-
redeliver: (threadlineRelayClient && listenerManager)
|
|
18112
|
+
// fabricating a send). Only under the legacy gate.
|
|
18113
|
+
redeliver: (a2aDelivCfg.enabled && threadlineRelayClient && listenerManager)
|
|
18074
18114
|
? (entry) => {
|
|
18075
18115
|
const stored = listenerManager.readCanonicalOutboxEntry(entry.messageId);
|
|
18076
18116
|
if (!stored)
|
|
@@ -18081,27 +18121,93 @@ export async function startServer(options) {
|
|
|
18081
18121
|
: undefined,
|
|
18082
18122
|
raiseAttention: telegram
|
|
18083
18123
|
? async (item) => {
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18124
|
+
// Legacy per-message escalation keeps its stamped id; the §3
|
|
18125
|
+
// items carry a deterministic id and REOPEN on a later episode.
|
|
18126
|
+
if (!item.id) {
|
|
18127
|
+
return telegram.createAttentionItem({
|
|
18128
|
+
id: `a2a-redelivery-${Date.now()}`,
|
|
18129
|
+
title: item.title,
|
|
18130
|
+
summary: item.body.slice(0, 160),
|
|
18131
|
+
description: item.body,
|
|
18132
|
+
category: 'a2a-redelivery',
|
|
18133
|
+
priority: priorityMap[item.priority ?? 'medium'] ?? 'NORMAL',
|
|
18134
|
+
sourceContext: item.source ?? 'a2a-redelivery',
|
|
18135
|
+
});
|
|
18136
|
+
}
|
|
18137
|
+
return telegram.upsertAttentionItem({
|
|
18138
|
+
id: item.id,
|
|
18089
18139
|
title: item.title,
|
|
18090
18140
|
summary: item.body.slice(0, 160),
|
|
18091
18141
|
description: item.body,
|
|
18092
|
-
category: 'a2a-
|
|
18142
|
+
category: 'a2a-peer-dark',
|
|
18093
18143
|
priority: priorityMap[item.priority ?? 'medium'] ?? 'NORMAL',
|
|
18094
|
-
sourceContext: item.source ?? 'a2a-
|
|
18144
|
+
sourceContext: item.source ?? 'a2a-peer-dark',
|
|
18145
|
+
});
|
|
18146
|
+
}
|
|
18147
|
+
: undefined,
|
|
18148
|
+
// Resolve: the resolve line lands on the hub through the same upsert
|
|
18149
|
+
// (body changed → re-posted), then the item is closed silently.
|
|
18150
|
+
resolveAttention: telegram
|
|
18151
|
+
? async (id, line) => {
|
|
18152
|
+
const existing = telegram.getAttentionItem(id);
|
|
18153
|
+
if (!existing)
|
|
18154
|
+
return false;
|
|
18155
|
+
await telegram.upsertAttentionItem({
|
|
18156
|
+
id,
|
|
18157
|
+
title: existing.title,
|
|
18158
|
+
summary: line.slice(0, 160),
|
|
18159
|
+
description: line,
|
|
18160
|
+
category: existing.category,
|
|
18161
|
+
priority: existing.priority,
|
|
18162
|
+
sourceContext: existing.sourceContext,
|
|
18095
18163
|
});
|
|
18164
|
+
return telegram.updateAttentionStatus(id, 'DONE', { silent: true });
|
|
18165
|
+
}
|
|
18166
|
+
: undefined,
|
|
18167
|
+
attentionState: telegram
|
|
18168
|
+
? (id) => {
|
|
18169
|
+
const it = telegram.getAttentionItem(id);
|
|
18170
|
+
return it ? { status: it.status, updatedAt: it.updatedAt, createdAt: it.createdAt } : null;
|
|
18096
18171
|
}
|
|
18097
18172
|
: undefined,
|
|
18098
|
-
|
|
18173
|
+
relayState: () => {
|
|
18174
|
+
const rc = threadlineRelayClient;
|
|
18175
|
+
if (!rc)
|
|
18176
|
+
return 'not-configured';
|
|
18177
|
+
if (rc.connectionState === 'connected')
|
|
18178
|
+
return 'connected';
|
|
18179
|
+
return threadlineGetLastRelayEvent?.()?.terminal === true ? 'displaced' : 'disconnected';
|
|
18180
|
+
},
|
|
18181
|
+
// §3.3: a standby's relay is disconnected BY DESIGN; only the awake
|
|
18182
|
+
// (telegram-polling) machine may heal or raise. The boot-time standby
|
|
18183
|
+
// fact is the same one the relay-forward route reads.
|
|
18184
|
+
isAwake: () => !(_a2aRelayForwardCtx?.relaySuppressedByStandby === true),
|
|
18185
|
+
reconnectRelay: threadlineRelayClient ? () => threadlineRelayClient.reconnectRelay() : undefined,
|
|
18186
|
+
refreshPresence: threadlineRelayClient ? () => threadlineRelayClient.refreshPresence() : undefined,
|
|
18187
|
+
peerConnectedNow: threadlineRelayClient ? (fp) => threadlineRelayClient.peerConnectedNow(fp) : undefined,
|
|
18188
|
+
peerPresence: threadlineRelayClient ? (fp) => threadlineRelayClient.peerPresence(fp) : undefined,
|
|
18189
|
+
// identitySelfCheck (§2) is wired by the identity-coherence check once
|
|
18190
|
+
// it lands; absent, the heal records `unknown` and never supersedes.
|
|
18191
|
+
poolPeerHealth,
|
|
18192
|
+
audit: createPeerDarkAuditWriter(resolvePeerDarkAuditPath(config.stateDir), (l) => console.warn(l)),
|
|
18193
|
+
}, {
|
|
18194
|
+
...mergeDefaults(DEFAULT_A2A_REDELIVERY_CONFIG, a2aDelivCfg),
|
|
18195
|
+
enabled: a2aDelivCfg.enabled === true,
|
|
18196
|
+
peerDark: {
|
|
18197
|
+
enabled: peerDarkCfg.enabled,
|
|
18198
|
+
dryRun: peerDarkCfg.dryRun,
|
|
18199
|
+
queuedDarkAfterMs: peerDarkCfg.queuedDarkAfterMs,
|
|
18200
|
+
perPeer: peerDarkCfg.perPeer,
|
|
18201
|
+
cooldownMs: peerDarkCfg.cooldownMs,
|
|
18202
|
+
},
|
|
18203
|
+
});
|
|
18099
18204
|
a2aRedeliverySentinel.start();
|
|
18100
18205
|
globalThis.__instarA2ARedeliverySentinel = a2aRedeliverySentinel;
|
|
18101
|
-
console.log(pc.green(
|
|
18206
|
+
console.log(pc.green(` A2ARedeliverySentinel: armed (redelivery ${a2aDelivCfg.enabled ? 'on' : 'off'}; ` +
|
|
18207
|
+
`peer-dark notice ${peerDarkCfg.enabled ? (peerDarkCfg.dryRun ? 'dry-run' : 'LIVE') : 'off'})`));
|
|
18102
18208
|
}
|
|
18103
18209
|
else {
|
|
18104
|
-
console.log(pc.dim(' A2ARedeliverySentinel: disabled (monitoring.a2aRedelivery.enabled=false; the ship-OFF default)'));
|
|
18210
|
+
console.log(pc.dim(' A2ARedeliverySentinel: disabled (monitoring.a2aRedelivery.enabled=false and threadline.peerDarkNotice dark; the ship-OFF default)'));
|
|
18105
18211
|
}
|
|
18106
18212
|
}
|
|
18107
18213
|
catch (err) {
|
|
@@ -25216,15 +25322,217 @@ export async function startServer(options) {
|
|
|
25216
25322
|
const v = config.multiMachine?.relayTimeoutMs;
|
|
25217
25323
|
return typeof v === 'number' && v > 0 ? v : 15_000;
|
|
25218
25324
|
})();
|
|
25325
|
+
// ── a2a-single-agent-identity §4 (ACT-058): forward-to-holder ──
|
|
25326
|
+
// A token-holding machine that does NOT hold the serving lease (a
|
|
25327
|
+
// pool standby that legitimately owns a topic) forwards its reply to
|
|
25328
|
+
// the lease holder instead of holding it silently; a failed forward
|
|
25329
|
+
// becomes a DURABLE, reported hold. The forward itself is switchable
|
|
25330
|
+
// (`messaging[].config.messageOrigin.forwardToHolder.enabled`); the
|
|
25331
|
+
// durable held state, its reason and the notice are not.
|
|
25332
|
+
const originForwardMod = await import('../messaging/telegram-origin/OriginForwardToHolder.js');
|
|
25333
|
+
const originConfigMod = await import('../messaging/telegram-origin/OriginConfig.js');
|
|
25334
|
+
const hasUsableBotToken = typeof originBotToken === 'string' && originBotToken.length > 0;
|
|
25335
|
+
const forwardSwitchOn = () => {
|
|
25336
|
+
try {
|
|
25337
|
+
const messaging = liveConfig.get('messaging', config.messaging);
|
|
25338
|
+
const entry = Array.isArray(messaging) ? messaging.find(m => m?.type === 'telegram') : undefined;
|
|
25339
|
+
return originConfigMod.originForwardToHolderEnabled(entry?.config?.messageOrigin);
|
|
25340
|
+
}
|
|
25341
|
+
catch {
|
|
25342
|
+
return true;
|
|
25343
|
+
} // @silent-fallback-ok — an unreadable switch keeps the reachability floor on
|
|
25344
|
+
};
|
|
25345
|
+
const forwardLease = {
|
|
25346
|
+
selfMachineId: meshSelfId ?? '',
|
|
25347
|
+
leaseHolder: () => coordinator.getSyncStatus().leaseHolder ?? null,
|
|
25348
|
+
holdsLease: () => coordinator.holdsLease(),
|
|
25349
|
+
isHolderHealthy: (machineId) => { try {
|
|
25350
|
+
return leaseCoordinatorRef ? leaseCoordinatorRef.isHolderHealthy(machineId) : false;
|
|
25351
|
+
}
|
|
25352
|
+
catch {
|
|
25353
|
+
return false;
|
|
25354
|
+
} }, // @silent-fallback-ok — an unreadable lease reads as NOT healthy: settling, never a blind forward
|
|
25355
|
+
};
|
|
25356
|
+
const holderNickname = (machineId) => {
|
|
25357
|
+
try {
|
|
25358
|
+
return coordinator.managers.identityManager.loadRegistry().machines[machineId]?.nickname ?? `machine ${machineId.slice(0, 8)}`;
|
|
25359
|
+
}
|
|
25360
|
+
catch {
|
|
25361
|
+
return `machine ${machineId.slice(0, 8)}`;
|
|
25362
|
+
}
|
|
25363
|
+
};
|
|
25364
|
+
const heldAuditPath = path.join(config.stateDir, 'logs', 'telegram-origin-held.jsonl');
|
|
25365
|
+
const heldAudit = (row) => {
|
|
25366
|
+
try {
|
|
25367
|
+
fs.mkdirSync(path.dirname(heldAuditPath), { recursive: true });
|
|
25368
|
+
fs.appendFileSync(heldAuditPath, JSON.stringify({ machineId: meshSelfId, ...row }) + '\n');
|
|
25369
|
+
}
|
|
25370
|
+
catch { /* @silent-fallback-ok — audit is observability only */ }
|
|
25371
|
+
};
|
|
25372
|
+
const heldCollapser = new originForwardMod.HeldForwardItemCollapser({ now: Date.now });
|
|
25373
|
+
const raiseHeldItem = async (input) => {
|
|
25374
|
+
const verdict = heldCollapser.record(input.topicId);
|
|
25375
|
+
const holderName = input.holder ? holderNickname(input.holder) : 'no machine currently holds the serving lease';
|
|
25376
|
+
try {
|
|
25377
|
+
await telegram.createAttentionItem({ id: verdict.itemId, category: 'telegram-origin', priority: 'HIGH',
|
|
25378
|
+
title: verdict.aggregate ? `Replies held on ${holderNickname(meshSelfId ?? '')}: ${heldCollapser.topics().length} topics` : `Reply to topic ${input.topicId} is held`,
|
|
25379
|
+
summary: verdict.aggregate
|
|
25380
|
+
? `Replies for topics ${heldCollapser.topics().join(', ')} could not be forwarded to the lease holder (${holderName}). They are held durably and retried; see GET /telegram/origins/status → held.`
|
|
25381
|
+
: `My reply to topic ${input.topicId} could not be forwarded to the lease holder (${holderName}): ${input.reason}. It is held durably (hold_reason: lease-not-held) and retried on the recovery schedule.`,
|
|
25382
|
+
sourceContext: 'telegram-origin-forward-to-holder' }, { hubOnly: true });
|
|
25383
|
+
}
|
|
25384
|
+
catch (error) {
|
|
25385
|
+
heldAudit({ phase: 'item-failed', topicId: input.topicId, error: error instanceof Error ? error.message : String(error) });
|
|
25386
|
+
}
|
|
25387
|
+
};
|
|
25388
|
+
const tgConfig = () => originTelegramConfig.config;
|
|
25389
|
+
const sendToHolder = (holder, timeoutMs = Math.min(relayTimeoutMs, originForwardMod.FORWARD_RPC_TIMEOUT_MS)) => {
|
|
25390
|
+
const url = peerUrl(holder);
|
|
25391
|
+
return (command) => {
|
|
25392
|
+
if (!url)
|
|
25393
|
+
return Promise.reject(new Error('origin-peer-unreachable'));
|
|
25394
|
+
// §4.2 — `capabilities` + `submit` share the 30 s attempt budget.
|
|
25395
|
+
return meshClient.send({ machineId: holder, url }, command, 0, { timeoutMs });
|
|
25396
|
+
};
|
|
25397
|
+
};
|
|
25398
|
+
const forwardDeps = (runtime) => ({
|
|
25399
|
+
lease: forwardLease,
|
|
25400
|
+
prepare: async (holder, input, operationId) => {
|
|
25401
|
+
const service = runtime.service;
|
|
25402
|
+
const { applyTelegramFormatter } = await import('../messaging/TelegramAdapter.js');
|
|
25403
|
+
const params = applyTelegramFormatter('sendMessage', { chat_id: input.chatId, text: input.text, parse_mode: 'Markdown',
|
|
25404
|
+
...(input.topicId > 1 ? { message_thread_id: input.topicId } : {}), ...(input.silent ? { disable_notification: true } : {}) }, input.formatMode ?? tgConfig().formatMode).outgoingParams;
|
|
25405
|
+
const accountId = String(originBotToken).split(':')[0];
|
|
25406
|
+
const prepare = () => service.prepareBot({ method: 'sendMessage', accountId, params, executionOwnerMachineId: holder,
|
|
25407
|
+
...(operationId === undefined ? {} : { operationId }), policyInput: service.currentSendPolicyInput(input.text, input.kindMetadata) });
|
|
25408
|
+
const operation = service.hasProducerContext() ? prepare() : service.runAsUnboundAutomation('telegram-server', prepare);
|
|
25409
|
+
service.authorizeSendPolicyDispatch(operation.record);
|
|
25410
|
+
await service.recordIntent(operation);
|
|
25411
|
+
return operation;
|
|
25412
|
+
},
|
|
25413
|
+
submit: (holder, operation) => originForwardMod.submitOriginToHolder({ operation, send: sendToHolder(holder), expectedOwner: holder }),
|
|
25414
|
+
hold: async (operation, detail) => {
|
|
25415
|
+
// Admit locally (the row + sealed payload) then name the reason
|
|
25416
|
+
// through the EXISTING operation-state writer; the recovery tick
|
|
25417
|
+
// re-forwards this same operation id.
|
|
25418
|
+
await runtime.service.admit(operation);
|
|
25419
|
+
const recorded = await runtime.store.recordOperationState({ operationId: operation.record.operationId, state: 'held', holdReason: originForwardMod.HOLD_REASON_LEASE_NOT_HELD, holdDetail: detail });
|
|
25420
|
+
await runtime.refreshHeldForward({ force: true });
|
|
25421
|
+
return recorded;
|
|
25422
|
+
},
|
|
25423
|
+
notify: async (holder, topicId) => {
|
|
25424
|
+
const text = originForwardMod.heldForwardNoticeText(holderNickname(holder));
|
|
25425
|
+
const { relayOriginBot } = await import('../messaging/telegram-origin/OriginMeshRelay.js');
|
|
25426
|
+
// Three short RPCs (5 s each): the notice must never push the reply
|
|
25427
|
+
// request past its route timeout — worst case settle 15 s + attempt 30 s + notice 15 s.
|
|
25428
|
+
const r = await runtime.service.runAsUnboundAutomation('telegram-server', () => relayOriginBot({ runtime, topicId, text, chatId: tgConfig().chatId,
|
|
25429
|
+
formatMode: tgConfig().formatMode, send: sendToHolder(holder, 5_000), executionOwnerMachineId: holder }));
|
|
25430
|
+
return Number.isSafeInteger(r.messageId);
|
|
25431
|
+
},
|
|
25432
|
+
audit: heldAudit, sleep: ms => new Promise(resolve => setTimeout(resolve, ms)), now: Date.now,
|
|
25433
|
+
});
|
|
25434
|
+
if (telegramOriginBoot && hasUsableBotToken && meshSelfId) {
|
|
25435
|
+
const runtime = telegramOriginBoot.runtime;
|
|
25436
|
+
telegram.holderForwardPolicy = {
|
|
25437
|
+
decide: () => {
|
|
25438
|
+
if (!forwardSwitchOn() || _sessionPoolStage() === 'dark')
|
|
25439
|
+
return 'local';
|
|
25440
|
+
const route = originForwardMod.classifyLeaseRoute(forwardLease);
|
|
25441
|
+
return route.kind === 'self' ? 'local' : route.kind === 'peer' ? 'forward' : 'settling';
|
|
25442
|
+
},
|
|
25443
|
+
};
|
|
25444
|
+
// §4.2 — recovery re-forwards the SAME operation; an outcome-unknown
|
|
25445
|
+
// attempt is resolved at the old holder's receipt, never re-sent blind.
|
|
25446
|
+
runtime.options.forwardRecovery = (operation, row) => originForwardMod.recoverForwardedHold({
|
|
25447
|
+
lease: forwardLease, prepare: forwardDeps(runtime).prepare, submit: forwardDeps(runtime).submit, audit: heldAudit, now: Date.now,
|
|
25448
|
+
receipt: async (machineId, operationId) => {
|
|
25449
|
+
try {
|
|
25450
|
+
const reply = await sendToHolder(machineId)({ type: 'telegram-origin', protocol: 'instar-telegram-origin-v1', action: 'receipt', operationId });
|
|
25451
|
+
const result = reply.result;
|
|
25452
|
+
if (reply.ok && result?.ok && result.operationId === operationId) {
|
|
25453
|
+
const state = String(result.state);
|
|
25454
|
+
if (state === 'accepted' && result.originReceiptConfirmed === true)
|
|
25455
|
+
return { state: 'accepted', receiptJson: JSON.stringify({ confirmedVia: 'receipt', deliveryMachineId: machineId, operationId }) };
|
|
25456
|
+
// Definite non-admission: never seen, evidence only, or terminal without delivery.
|
|
25457
|
+
if (['no-record', 'not-admitted', 'known-failed', 'expired', 'suppressed', 'superseded'].includes(state))
|
|
25458
|
+
return { state: 'not-accepted' };
|
|
25459
|
+
// The holder has custody (its own recovery delivers it): wait, never re-forward.
|
|
25460
|
+
if (['admitted', 'held', 'partial', 'accepted'].includes(state))
|
|
25461
|
+
return { state: 'owned-by-holder' };
|
|
25462
|
+
return { state: 'unreachable' };
|
|
25463
|
+
}
|
|
25464
|
+
// `origin-receipt-unavailable` / any refusal is an UNKNOWN (a store outage looks the same as absence to an old holder): keep waiting.
|
|
25465
|
+
return { state: 'unreachable' };
|
|
25466
|
+
}
|
|
25467
|
+
catch {
|
|
25468
|
+
return { state: 'unreachable' };
|
|
25469
|
+
}
|
|
25470
|
+
},
|
|
25471
|
+
resolve: (operationId, deliveryMachineId, receiptJson) => runtime.store.recordForwardedAcceptance({ operationId, deliveryMachineId, receiptJson }),
|
|
25472
|
+
rehold: async (operationId, detail) => { await runtime.store.recordOperationState({ operationId, state: 'held', holdReason: originForwardMod.HOLD_REASON_LEASE_NOT_HELD, holdDetail: detail }); },
|
|
25473
|
+
supersede: (operationId, next, detail) => runtime.store.supersedeWithAdmission({ operationId, admission: next.admission,
|
|
25474
|
+
holdReason: originForwardMod.HOLD_REASON_LEASE_NOT_HELD, holdDetail: detail }),
|
|
25475
|
+
sendLocal: async (detail) => {
|
|
25476
|
+
const sent = await telegram.sendToTopic(detail.topicId, detail.text, { silent: detail.silent, kindMetadata: detail.kindMetadata, provenance: 'automation' });
|
|
25477
|
+
return { messageId: sent.messageId };
|
|
25478
|
+
},
|
|
25479
|
+
}, row, operation);
|
|
25480
|
+
// §4.2 — a DIRECT-path hold (`authorize` refused while not holding the lease:
|
|
25481
|
+
// pool dark, or the lease moved between the adapter decision and authorize).
|
|
25482
|
+
// Its sealed record is owned by THIS machine, so it cannot be forwarded; it is
|
|
25483
|
+
// re-admitted and replayed the moment the lease returns (runtime.recoverHeld).
|
|
25484
|
+
// Report it exactly like a failed forward: the fixed notice through the
|
|
25485
|
+
// holder when one is reachable, otherwise the collapsed attention item.
|
|
25486
|
+
runtime.options.onLeaseHoldObserved = async (row) => {
|
|
25487
|
+
const topicId = Number(row.destination.topicId);
|
|
25488
|
+
heldAudit({ phase: 'direct-hold-observed', topicId: row.destination.topicId, operationId: row.operationId });
|
|
25489
|
+
if (!Number.isSafeInteger(topicId))
|
|
25490
|
+
return;
|
|
25491
|
+
const route = originForwardMod.classifyLeaseRoute(forwardLease);
|
|
25492
|
+
let noticeDelivered = false;
|
|
25493
|
+
if (route.kind === 'peer' && forwardSwitchOn()) {
|
|
25494
|
+
try {
|
|
25495
|
+
noticeDelivered = await forwardDeps(runtime).notify(route.holder, topicId);
|
|
25496
|
+
}
|
|
25497
|
+
catch {
|
|
25498
|
+
noticeDelivered = false;
|
|
25499
|
+
} // @silent-fallback-ok — an undelivered notice falls through to the attention item below
|
|
25500
|
+
}
|
|
25501
|
+
heldAudit({ phase: noticeDelivered ? 'notice-sent' : 'notice-not-sent', topicId, operationId: row.operationId, holder: route.kind === 'peer' ? route.holder : null });
|
|
25502
|
+
if (!noticeDelivered)
|
|
25503
|
+
await raiseHeldItem({ topicId, holder: route.kind === 'peer' ? route.holder : null, operationId: row.operationId, lastAttempt: 'refused', reason: 'lease-not-held (direct path)' });
|
|
25504
|
+
};
|
|
25505
|
+
runtime.options.onHeldForwardExpired = async (row) => {
|
|
25506
|
+
const topicId = Number(row.destination.topicId);
|
|
25507
|
+
heldAudit({ phase: 'expired', topicId: row.destination.topicId, operationId: row.operationId, lastAttempt: row.holdDetail?.lastAttempt ?? null });
|
|
25508
|
+
if (Number.isSafeInteger(topicId))
|
|
25509
|
+
heldCollapser.clear(topicId);
|
|
25510
|
+
await telegram.createAttentionItem({ id: `telegram-origin-held:${row.destination.topicId ?? row.operationId}:expired`, category: 'telegram-origin', priority: 'HIGH',
|
|
25511
|
+
title: `Held reply to topic ${row.destination.topicId ?? '?'} expired`, summary: originForwardMod.expiredForwardWording(row),
|
|
25512
|
+
sourceContext: 'telegram-origin-forward-to-holder' }, { hubOnly: true });
|
|
25513
|
+
};
|
|
25514
|
+
}
|
|
25219
25515
|
telegram.outboundRelay = async (topicId, text, opts) => {
|
|
25220
25516
|
if (telegramOriginBoot) {
|
|
25517
|
+
const { TelegramOriginHoldError, OriginForwardSettledLocallyError } = await import('../messaging/telegram-origin/types.js');
|
|
25518
|
+
const tg = tgConfig();
|
|
25519
|
+
if (hasUsableBotToken && meshSelfId) {
|
|
25520
|
+
// §4.1/§4.2 — settle, forward to the holder, ladder, then hold durably.
|
|
25521
|
+
const runtime = telegramOriginBoot.runtime;
|
|
25522
|
+
const outcome = await originForwardMod.forwardReplyToHolder(forwardDeps(runtime), { topicId, chatId: tg.chatId, text, silent: opts?.silent, formatMode: tg.formatMode, kindMetadata: opts?.kindMetadata });
|
|
25523
|
+
if (outcome.kind === 'local')
|
|
25524
|
+
throw new OriginForwardSettledLocallyError();
|
|
25525
|
+
if (outcome.kind === 'sent')
|
|
25526
|
+
return { messageId: outcome.messageId, topicId, destinationStoreConfirmed: true };
|
|
25527
|
+
if (!outcome.noticeDelivered)
|
|
25528
|
+
void raiseHeldItem({ topicId, holder: outcome.holder, operationId: outcome.operationId, lastAttempt: outcome.lastAttempt, reason: outcome.reason });
|
|
25529
|
+
throw new TelegramOriginHoldError(originForwardMod.HOLD_REASON_LEASE_NOT_HELD, outcome.operationId, 'held');
|
|
25530
|
+
}
|
|
25221
25531
|
const holder = coordinator.getSyncStatus().leaseHolder;
|
|
25222
25532
|
const url = holder ? peerUrl(holder) : null;
|
|
25223
|
-
const { TelegramOriginHoldError } = await import('../messaging/telegram-origin/types.js');
|
|
25224
25533
|
if (!holder || !url || holder === meshSelfId)
|
|
25225
25534
|
throw new TelegramOriginHoldError('origin-credential-owner-unavailable');
|
|
25226
25535
|
const { relayOriginBot } = await import('../messaging/telegram-origin/OriginMeshRelay.js');
|
|
25227
|
-
const tg = originTelegramConfig.config;
|
|
25228
25536
|
return relayOriginBot({ runtime: telegramOriginBoot.runtime, topicId, text, chatId: tg.chatId,
|
|
25229
25537
|
silent: opts?.silent, formatMode: tg.formatMode, kindMetadata: opts?.kindMetadata,
|
|
25230
25538
|
send: command => meshClient.send({ machineId: holder, url }, command, 0, { timeoutMs: relayTimeoutMs }) });
|