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.
Files changed (131) hide show
  1. package/dist/commands/init.d.ts.map +1 -1
  2. package/dist/commands/init.js +8 -0
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/commands/server.d.ts.map +1 -1
  5. package/dist/commands/server.js +323 -15
  6. package/dist/commands/server.js.map +1 -1
  7. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  8. package/dist/config/ConfigDefaults.js +15 -0
  9. package/dist/config/ConfigDefaults.js.map +1 -1
  10. package/dist/core/BackupManager.d.ts.map +1 -1
  11. package/dist/core/BackupManager.js +8 -0
  12. package/dist/core/BackupManager.js.map +1 -1
  13. package/dist/core/FileClassifier.d.ts.map +1 -1
  14. package/dist/core/FileClassifier.js +6 -0
  15. package/dist/core/FileClassifier.js.map +1 -1
  16. package/dist/core/MachineIdentity.d.ts.map +1 -1
  17. package/dist/core/MachineIdentity.js +3 -0
  18. package/dist/core/MachineIdentity.js.map +1 -1
  19. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  20. package/dist/core/PostUpdateMigrator.js +58 -2
  21. package/dist/core/PostUpdateMigrator.js.map +1 -1
  22. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  23. package/dist/core/devGatedFeatures.js +6 -0
  24. package/dist/core/devGatedFeatures.js.map +1 -1
  25. package/dist/core/keyMaterialPaths.d.ts +73 -0
  26. package/dist/core/keyMaterialPaths.d.ts.map +1 -0
  27. package/dist/core/keyMaterialPaths.js +139 -0
  28. package/dist/core/keyMaterialPaths.js.map +1 -0
  29. package/dist/core/types.d.ts +20 -0
  30. package/dist/core/types.d.ts.map +1 -1
  31. package/dist/core/types.js.map +1 -1
  32. package/dist/data/standards-guard-index.json +1 -1
  33. package/dist/data/standards-guard-index.meta.json +2 -2
  34. package/dist/data/standards-registry.meta.json +1 -1
  35. package/dist/messaging/TelegramAdapter.d.ts +19 -0
  36. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  37. package/dist/messaging/TelegramAdapter.js +47 -9
  38. package/dist/messaging/TelegramAdapter.js.map +1 -1
  39. package/dist/messaging/telegram-origin/OriginAwareness.d.ts +2 -0
  40. package/dist/messaging/telegram-origin/OriginAwareness.d.ts.map +1 -1
  41. package/dist/messaging/telegram-origin/OriginAwareness.js +4 -0
  42. package/dist/messaging/telegram-origin/OriginAwareness.js.map +1 -1
  43. package/dist/messaging/telegram-origin/OriginConfig.d.ts +4 -0
  44. package/dist/messaging/telegram-origin/OriginConfig.d.ts.map +1 -1
  45. package/dist/messaging/telegram-origin/OriginConfig.js +26 -0
  46. package/dist/messaging/telegram-origin/OriginConfig.js.map +1 -1
  47. package/dist/messaging/telegram-origin/OriginForwardToHolder.d.ts +236 -0
  48. package/dist/messaging/telegram-origin/OriginForwardToHolder.d.ts.map +1 -0
  49. package/dist/messaging/telegram-origin/OriginForwardToHolder.js +337 -0
  50. package/dist/messaging/telegram-origin/OriginForwardToHolder.js.map +1 -0
  51. package/dist/messaging/telegram-origin/OriginMesh.d.ts.map +1 -1
  52. package/dist/messaging/telegram-origin/OriginMesh.js +17 -2
  53. package/dist/messaging/telegram-origin/OriginMesh.js.map +1 -1
  54. package/dist/messaging/telegram-origin/OriginMeshRelay.d.ts +7 -0
  55. package/dist/messaging/telegram-origin/OriginMeshRelay.d.ts.map +1 -1
  56. package/dist/messaging/telegram-origin/OriginMeshRelay.js +15 -15
  57. package/dist/messaging/telegram-origin/OriginMeshRelay.js.map +1 -1
  58. package/dist/messaging/telegram-origin/OriginStore.d.ts +38 -1
  59. package/dist/messaging/telegram-origin/OriginStore.d.ts.map +1 -1
  60. package/dist/messaging/telegram-origin/OriginStore.js +8 -1
  61. package/dist/messaging/telegram-origin/OriginStore.js.map +1 -1
  62. package/dist/messaging/telegram-origin/OriginStore.worker.js +1 -1
  63. package/dist/messaging/telegram-origin/OriginStore.worker.js.map +1 -1
  64. package/dist/messaging/telegram-origin/OriginStoreBackend.d.ts +58 -1
  65. package/dist/messaging/telegram-origin/OriginStoreBackend.d.ts.map +1 -1
  66. package/dist/messaging/telegram-origin/OriginStoreBackend.js +209 -23
  67. package/dist/messaging/telegram-origin/OriginStoreBackend.js.map +1 -1
  68. package/dist/messaging/telegram-origin/StoreTypes.d.ts +24 -0
  69. package/dist/messaging/telegram-origin/StoreTypes.d.ts.map +1 -1
  70. package/dist/messaging/telegram-origin/TelegramOriginBoot.d.ts.map +1 -1
  71. package/dist/messaging/telegram-origin/TelegramOriginBoot.js +7 -0
  72. package/dist/messaging/telegram-origin/TelegramOriginBoot.js.map +1 -1
  73. package/dist/messaging/telegram-origin/TelegramOriginRuntime.d.ts +49 -7
  74. package/dist/messaging/telegram-origin/TelegramOriginRuntime.d.ts.map +1 -1
  75. package/dist/messaging/telegram-origin/TelegramOriginRuntime.js +178 -2
  76. package/dist/messaging/telegram-origin/TelegramOriginRuntime.js.map +1 -1
  77. package/dist/messaging/telegram-origin/TelegramOriginService.d.ts +8 -0
  78. package/dist/messaging/telegram-origin/TelegramOriginService.d.ts.map +1 -1
  79. package/dist/messaging/telegram-origin/TelegramOriginService.js +19 -3
  80. package/dist/messaging/telegram-origin/TelegramOriginService.js.map +1 -1
  81. package/dist/messaging/telegram-origin/types.d.ts +12 -0
  82. package/dist/messaging/telegram-origin/types.d.ts.map +1 -1
  83. package/dist/messaging/telegram-origin/types.js +6 -0
  84. package/dist/messaging/telegram-origin/types.js.map +1 -1
  85. package/dist/monitoring/A2ARedeliverySentinel.d.ts +145 -6
  86. package/dist/monitoring/A2ARedeliverySentinel.d.ts.map +1 -1
  87. package/dist/monitoring/A2ARedeliverySentinel.js +451 -11
  88. package/dist/monitoring/A2ARedeliverySentinel.js.map +1 -1
  89. package/dist/scaffold/templates.d.ts.map +1 -1
  90. package/dist/scaffold/templates.js +7 -1
  91. package/dist/scaffold/templates.js.map +1 -1
  92. package/dist/server/fileRoutes.d.ts +26 -0
  93. package/dist/server/fileRoutes.d.ts.map +1 -1
  94. package/dist/server/fileRoutes.js +204 -56
  95. package/dist/server/fileRoutes.js.map +1 -1
  96. package/dist/server/routes.d.ts.map +1 -1
  97. package/dist/server/routes.js +103 -9
  98. package/dist/server/routes.js.map +1 -1
  99. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  100. package/dist/testing/selfActionRegistry.js +128 -0
  101. package/dist/testing/selfActionRegistry.js.map +1 -1
  102. package/dist/threadline/A2ADeliveryTracker.d.ts +55 -1
  103. package/dist/threadline/A2ADeliveryTracker.d.ts.map +1 -1
  104. package/dist/threadline/A2ADeliveryTracker.js +97 -5
  105. package/dist/threadline/A2ADeliveryTracker.js.map +1 -1
  106. package/dist/threadline/ThreadlineMCPServer.d.ts +15 -0
  107. package/dist/threadline/ThreadlineMCPServer.d.ts.map +1 -1
  108. package/dist/threadline/ThreadlineMCPServer.js +3 -0
  109. package/dist/threadline/ThreadlineMCPServer.js.map +1 -1
  110. package/dist/threadline/client/ThreadlineClient.d.ts +47 -0
  111. package/dist/threadline/client/ThreadlineClient.d.ts.map +1 -1
  112. package/dist/threadline/client/ThreadlineClient.js +93 -0
  113. package/dist/threadline/client/ThreadlineClient.js.map +1 -1
  114. package/dist/threadline/mcp-http-client.d.ts.map +1 -1
  115. package/dist/threadline/mcp-http-client.js +2 -0
  116. package/dist/threadline/mcp-http-client.js.map +1 -1
  117. package/dist/threadline/peerDark.d.ts +165 -0
  118. package/dist/threadline/peerDark.d.ts.map +1 -0
  119. package/dist/threadline/peerDark.js +204 -0
  120. package/dist/threadline/peerDark.js.map +1 -0
  121. package/package.json +1 -1
  122. package/src/data/builtin-manifest.json +64 -64
  123. package/src/data/standards-guard-index.json +1 -1
  124. package/src/data/standards-guard-index.meta.json +2 -2
  125. package/src/data/standards-registry.meta.json +1 -1
  126. package/src/scaffold/templates.ts +7 -1
  127. package/upgrades/1.3.1340.md +32 -0
  128. package/upgrades/1.3.1341.md +94 -0
  129. package/upgrades/side-effects/a2a-peer-dark-reporting.md +149 -0
  130. package/upgrades/side-effects/files-never-serve-key-material.md +399 -0
  131. package/upgrades/side-effects/telegram-origin-forward-to-holder.md +151 -0
@@ -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
- if (a2aDelivCfg.enabled && a2aDeliveryTracker) {
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
- const priorityMap = {
18085
- high: 'HIGH', medium: 'NORMAL', low: 'LOW',
18086
- };
18087
- return telegram.createAttentionItem({
18088
- id: `a2a-redelivery-${Date.now()}`,
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-redelivery',
18142
+ category: 'a2a-peer-dark',
18093
18143
  priority: priorityMap[item.priority ?? 'medium'] ?? 'NORMAL',
18094
- sourceContext: item.source ?? 'a2a-redelivery',
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
- }, { ...mergeDefaults(DEFAULT_A2A_REDELIVERY_CONFIG, a2aDelivCfg), enabled: true });
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(' A2ARedeliverySentinel: armed (A2A redelivery + dark-peer escalation)'));
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 }) });