sneakoscope 7.3.0 → 7.6.0

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 (283) hide show
  1. package/README.md +129 -10
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/codex-app-command.js +1 -1
  5. package/dist/cli/command-manifest-lite.js +4 -1
  6. package/dist/cli/command-registry.js +2 -0
  7. package/dist/cli/install-helpers-codex-lb-chain.js +42 -0
  8. package/dist/cli/install-helpers-codex-lb-config.js +114 -9
  9. package/dist/cli/install-helpers-install-support.js +6 -16
  10. package/dist/cli/install-helpers.js +788 -600
  11. package/dist/cli/router.js +30 -2
  12. package/dist/commands/codex-app.js +37 -1
  13. package/dist/commands/codex-lb.js +491 -300
  14. package/dist/config/skills-manifest.json +57 -71
  15. package/dist/core/align/align-route.js +127 -0
  16. package/dist/core/codex-app/codex-app-fast-ui-repair.js +8 -4
  17. package/dist/core/codex-app/codex-app-restart.js +83 -0
  18. package/dist/core/codex-app/codex-app-ui-state-snapshot.js +8 -4
  19. package/dist/core/codex-app/desktop-routing-snapshot.js +1 -1
  20. package/dist/core/codex-app/menubar/constants.js +5 -0
  21. package/dist/core/codex-app/menubar/generation-transaction-journal.js +1 -3
  22. package/dist/core/codex-app/menubar/installer.js +18 -0
  23. package/dist/core/codex-app/multi-provider-router.js +22 -10
  24. package/dist/core/codex-app/openrouter-activate.js +26 -1
  25. package/dist/core/codex-app.js +118 -26
  26. package/dist/core/codex-control/codex-app-server-runtime-env.js +86 -0
  27. package/dist/core/codex-control/codex-app-server-v2-client.js +78 -8
  28. package/dist/core/codex-control/codex-task-runner.js +2 -2
  29. package/dist/core/codex-lb/capability-runner.js +204 -0
  30. package/dist/core/codex-lb/capability-types.js +1 -0
  31. package/dist/core/codex-lb/codex-lb-env.js +9 -13
  32. package/dist/core/codex-lb/codex-lb-setup.js +45 -8
  33. package/dist/core/codex-lb/codex-lb-tool-catalog.js +52 -74
  34. package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +1 -1
  35. package/dist/core/codex-lb/desktop-auth-invariant.js +185 -0
  36. package/dist/core/codex-lb/desktop-bridge/client.js +34 -0
  37. package/dist/core/codex-lb/desktop-bridge/header-policy.js +106 -0
  38. package/dist/core/codex-lb/desktop-bridge/http-forward.js +101 -0
  39. package/dist/core/codex-lb/desktop-bridge/index.js +10 -0
  40. package/dist/core/codex-lb/desktop-bridge/launchd.js +81 -0
  41. package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +27 -0
  42. package/dist/core/codex-lb/desktop-bridge/security.js +274 -0
  43. package/dist/core/codex-lb/desktop-bridge/server.js +179 -0
  44. package/dist/core/codex-lb/desktop-bridge/state.js +119 -0
  45. package/dist/core/codex-lb/desktop-bridge/types.js +16 -0
  46. package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +151 -0
  47. package/dist/core/codex-lb/desktop-center-credentials.js +189 -0
  48. package/dist/core/codex-lb/desktop-controller.js +1410 -0
  49. package/dist/core/codex-lb/desktop-mode.js +32 -0
  50. package/dist/core/codex-lb/desktop-real-evidence.js +168 -0
  51. package/dist/core/codex-lb/desktop-service.js +613 -0
  52. package/dist/core/codex-lb/legacy-migration.js +575 -0
  53. package/dist/core/codex-lb/migration-receipt.js +246 -0
  54. package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +60 -0
  55. package/dist/core/codex-lb/probes/bridge-probe.js +75 -0
  56. package/dist/core/codex-lb/probes/catalog-probe.js +128 -0
  57. package/dist/core/codex-lb/probes/computer-use-probe.js +50 -0
  58. package/dist/core/codex-lb/probes/image-generation-probe.js +47 -0
  59. package/dist/core/codex-lb/probes/probe-evidence.js +39 -0
  60. package/dist/core/codex-lb/probes/voice-realtime-probe.js +44 -0
  61. package/dist/core/codex-lb/trusted-deep-evidence.js +403 -0
  62. package/dist/core/codex-native/core-skill-manifest.js +22 -6
  63. package/dist/core/codex-runtime/codex-desktop-config-policy.js +2 -1
  64. package/dist/core/commands/align-command.js +220 -0
  65. package/dist/core/commands/image-ux-review-command.js +280 -68
  66. package/dist/core/commands/naruto-command.js +257 -8
  67. package/dist/core/commands/ppt-command.js +2 -2
  68. package/dist/core/commands/telegram-command.js +402 -34
  69. package/dist/core/commands/triwiki-command.js +11 -1
  70. package/dist/core/commands/triwiki-graph-command.js +160 -0
  71. package/dist/core/commands/wiki-command.js +100 -20
  72. package/dist/core/doctor/skill-legacy-surface.js +1 -3
  73. package/dist/core/errors/message.js +3 -0
  74. package/dist/core/evidence/evidence-freshness.js +14 -0
  75. package/dist/core/feature-fixtures.js +9 -1
  76. package/dist/core/feature-registry.js +5 -3
  77. package/dist/core/fsx.js +7 -3
  78. package/dist/core/hooks-runtime/codex-commit-hooks-selftest.js +82 -0
  79. package/dist/core/hooks-runtime/context-graph-freshness-preflight.js +113 -0
  80. package/dist/core/hooks-runtime/hook-context.js +15 -2
  81. package/dist/core/hooks-runtime/managed-skill-generation-heal.js +46 -0
  82. package/dist/core/hooks-runtime/naruto-terminal-finalization.js +1 -0
  83. package/dist/core/hooks-runtime/official-subagent-lifecycle.js +1 -1
  84. package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
  85. package/dist/core/hooks-runtime.js +33 -89
  86. package/dist/core/image-ux-review/callout-extraction.js +6 -0
  87. package/dist/core/image-ux-review/imagegen-adapter.js +64 -23
  88. package/dist/core/image-ux-review/real-callout-extractor.js +1 -0
  89. package/dist/core/image-ux-review.js +230 -12
  90. package/dist/core/imagegen/codex-lb-imagegen-target.js +21 -7
  91. package/dist/core/imagegen/imagegen-capability.js +10 -4
  92. package/dist/core/imagegen/require-imagegen.js +29 -5
  93. package/dist/core/init/skills.js +25 -13
  94. package/dist/core/init.js +1 -1
  95. package/dist/core/install/installed-package-smoke.js +157 -4
  96. package/dist/core/managed-assets/managed-assets-manifest.js +2 -1
  97. package/dist/core/managed-path-safety.js +8 -10
  98. package/dist/core/mcp-config/backup.js +2 -4
  99. package/dist/core/mcp-config/config-reader.js +2 -4
  100. package/dist/core/mcp-config/scope.js +3 -5
  101. package/dist/core/naruto/context-graph-advisor-scope.js +242 -0
  102. package/dist/core/naruto/context-graph-advisor.js +0 -0
  103. package/dist/core/perf/release-latency-slo.js +12 -6
  104. package/dist/core/pipeline-internals/runtime-core.js +78 -0
  105. package/dist/core/pipeline-internals/runtime-gates.js +5 -2
  106. package/dist/core/policy/latest-version-guidance.js +113 -0
  107. package/dist/core/ppt.js +8 -8
  108. package/dist/core/preflight/parallel-preflight-engine.js +1 -1
  109. package/dist/core/proof/auto-finalize.js +98 -6
  110. package/dist/core/proof/route-proof-gate.js +12 -1
  111. package/dist/core/proof/route-proof-policy.js +8 -8
  112. package/dist/core/release/gate-manifest.js +32 -1
  113. package/dist/core/release/npm-stage-contract.js +21 -0
  114. package/dist/core/release/package-size-budget.js +2 -2
  115. package/dist/core/release/release-gate-contract.js +5 -0
  116. package/dist/core/release/release-proof-io.js +1 -3
  117. package/dist/core/release/release-real-contract.js +5 -4
  118. package/dist/core/release/release-stamp-proof.js +1 -3
  119. package/dist/core/release/stage-publish.js +27 -0
  120. package/dist/core/remote/audit-idempotency.js +50 -18
  121. package/dist/core/remote/codex-public-activity.js +283 -0
  122. package/dist/core/remote/index.js +1 -0
  123. package/dist/core/remote/ssh-worker-client.js +4 -1
  124. package/dist/core/remote/worker.js +206 -27
  125. package/dist/core/research/research-adversarial-review.js +1 -3
  126. package/dist/core/research/research-claim-synthesizer.js +1 -3
  127. package/dist/core/research/research-falsification-runner.js +1 -3
  128. package/dist/core/research/research-source-evidence.js +1 -3
  129. package/dist/core/responses-stream.js +65 -19
  130. package/dist/core/routes/constants.js +1 -1
  131. package/dist/core/routes/design-policy.js +7 -2
  132. package/dist/core/routes/dollar-manifest-lite.js +2 -4
  133. package/dist/core/routes/evidence.js +1 -1
  134. package/dist/core/routes.js +30 -10
  135. package/dist/core/safety/command-contract/contracts.js +2 -0
  136. package/dist/core/safety/command-contract/types.js +1 -1
  137. package/dist/core/search/context-graph-seeds.js +205 -0
  138. package/dist/core/search/context.js +0 -0
  139. package/dist/core/search/provider.js +2 -2
  140. package/dist/core/secret-redaction.js +6 -0
  141. package/dist/core/subagents/naruto-help-contract.js +2 -1
  142. package/dist/core/subagents/naruto-host-credentials.js +161 -0
  143. package/dist/core/subagents/official-subagent-preparation.js +4 -1
  144. package/dist/core/subagents/official-subagent-prompt.js +44 -2
  145. package/dist/core/subagents/official-subagent-runner.js +26 -8
  146. package/dist/core/subagents/triwiki-attention.js +135 -123
  147. package/dist/core/telegram/bot-api.js +85 -10
  148. package/dist/core/telegram/center-snapshot.js +718 -0
  149. package/dist/core/telegram/config.js +158 -31
  150. package/dist/core/telegram/hub.js +288 -33
  151. package/dist/core/telegram/index.js +2 -0
  152. package/dist/core/telegram/ingress-spool.js +419 -0
  153. package/dist/core/telegram/ledgers.js +188 -29
  154. package/dist/core/telegram/messages.js +26 -6
  155. package/dist/core/telegram/owner-lock.js +10 -7
  156. package/dist/core/telegram/runtime-projection.js +38 -3
  157. package/dist/core/telegram/runtime.js +298 -63
  158. package/dist/core/telegram/service.js +137 -8
  159. package/dist/core/telegram/setup.js +291 -73
  160. package/dist/core/text/strings.js +3 -0
  161. package/dist/core/triwiki/code-pack.js +12 -120
  162. package/dist/core/triwiki/context-graph/compiler/cache-key.js +277 -0
  163. package/dist/core/triwiki/context-graph/compiler/extract.js +104 -0
  164. package/dist/core/triwiki/context-graph/compiler/freshness.js +108 -0
  165. package/dist/core/triwiki/context-graph/compiler/index.js +237 -0
  166. package/dist/core/triwiki/context-graph/compiler/merge.js +0 -0
  167. package/dist/core/triwiki/context-graph/compiler/serialize.js +115 -0
  168. package/dist/core/triwiki/context-graph/contracts.js +236 -0
  169. package/dist/core/triwiki/context-graph/extractors/code/builders.js +150 -0
  170. package/dist/core/triwiki/context-graph/extractors/code/declarations.js +161 -0
  171. package/dist/core/triwiki/context-graph/extractors/code/index.js +265 -0
  172. package/dist/core/triwiki/context-graph/extractors/code/inventory.js +237 -0
  173. package/dist/core/triwiki/context-graph/extractors/code/module-graph.js +252 -0
  174. package/dist/core/triwiki/context-graph/extractors/code/modules.js +79 -0
  175. package/dist/core/triwiki/context-graph/extractors/code/references.js +184 -0
  176. package/dist/core/triwiki/context-graph/extractors/code/selection.js +100 -0
  177. package/dist/core/triwiki/context-graph/extractors/code/ts-config.js +161 -0
  178. package/dist/core/triwiki/context-graph/extractors/code/types.js +18 -0
  179. package/dist/core/triwiki/context-graph/extractors/evidence/claims.js +289 -0
  180. package/dist/core/triwiki/context-graph/extractors/evidence/index.js +54 -0
  181. package/dist/core/triwiki/context-graph/extractors/evidence/proof-index.js +213 -0
  182. package/dist/core/triwiki/context-graph/extractors/evidence/proofs.js +266 -0
  183. package/dist/core/triwiki/context-graph/extractors/evidence/redaction.js +177 -0
  184. package/dist/core/triwiki/context-graph/extractors/evidence/shared.js +0 -0
  185. package/dist/core/triwiki/context-graph/extractors/evidence/sources.js +154 -0
  186. package/dist/core/triwiki/context-graph/extractors/index.js +7 -0
  187. package/dist/core/triwiki/context-graph/extractors/topology/commands.js +207 -0
  188. package/dist/core/triwiki/context-graph/extractors/topology/gate-edges.js +201 -0
  189. package/dist/core/triwiki/context-graph/extractors/topology/gates.js +182 -0
  190. package/dist/core/triwiki/context-graph/extractors/topology/globs.js +124 -0
  191. package/dist/core/triwiki/context-graph/extractors/topology/index.js +92 -0
  192. package/dist/core/triwiki/context-graph/extractors/topology/routes.js +135 -0
  193. package/dist/core/triwiki/context-graph/extractors/topology/shared.js +215 -0
  194. package/dist/core/triwiki/context-graph/graph-index.js +143 -0
  195. package/dist/core/triwiki/context-graph/ids.js +0 -0
  196. package/dist/core/triwiki/context-graph/lint/index.js +0 -0
  197. package/dist/core/triwiki/context-graph/lint/rules.js +232 -0
  198. package/dist/core/triwiki/context-graph/lint/warnings.js +93 -0
  199. package/dist/core/triwiki/context-graph/paths.js +116 -0
  200. package/dist/core/triwiki/context-graph/profiles.js +78 -0
  201. package/dist/core/triwiki/context-graph/projections/anchors.js +91 -0
  202. package/dist/core/triwiki/context-graph/projections/attention.js +58 -0
  203. package/dist/core/triwiki/context-graph/projections/code-pack-workspace.js +62 -0
  204. package/dist/core/triwiki/context-graph/projections/code-pack.js +144 -0
  205. package/dist/core/triwiki/context-graph/projections/index.js +7 -0
  206. package/dist/core/triwiki/context-graph/projections/module-view.js +140 -0
  207. package/dist/core/triwiki/context-graph/projections/node-summary.js +118 -0
  208. package/dist/core/triwiki/context-graph/projections/pack-contract.js +6 -0
  209. package/dist/core/triwiki/context-graph/query/explain.js +0 -0
  210. package/dist/core/triwiki/context-graph/query/index.js +178 -0
  211. package/dist/core/triwiki/context-graph/query/load.js +116 -0
  212. package/dist/core/triwiki/context-graph/query/pack.js +117 -0
  213. package/dist/core/triwiki/context-graph/query/rank.js +142 -0
  214. package/dist/core/triwiki/context-graph/query/ranking-config.js +64 -0
  215. package/dist/core/triwiki/context-graph/query/seeds.js +241 -0
  216. package/dist/core/triwiki/context-graph/query/snapshot-cache.js +72 -0
  217. package/dist/core/triwiki/context-graph/query/traverse.js +154 -0
  218. package/dist/core/triwiki/context-graph/query-types.js +29 -0
  219. package/dist/core/triwiki/context-graph/store/compile-lock.js +21 -0
  220. package/dist/core/triwiki/context-graph/store/event-log.js +48 -0
  221. package/dist/core/triwiki/context-graph/store/fragment-cache.js +85 -0
  222. package/dist/core/triwiki/context-graph/store/graph-status.js +85 -0
  223. package/dist/core/triwiki/context-graph/store/snapshot-store.js +155 -0
  224. package/dist/core/triwiki/triwiki-proof-bank-index-repair.js +115 -0
  225. package/dist/core/triwiki/triwiki-proof-bank-index-store.js +290 -0
  226. package/dist/core/triwiki/triwiki-proof-bank-index.js +126 -0
  227. package/dist/core/triwiki/triwiki-proof-bank.js +7 -1
  228. package/dist/core/verification/context-graph-affected.js +271 -0
  229. package/dist/core/version.js +1 -1
  230. package/dist/native/sks-menubar/Sources/AlertFactory.swift +11 -3
  231. package/dist/native/sks-menubar/Sources/AppDelegate.swift +9 -0
  232. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +25 -3
  233. package/dist/native/sks-menubar/Sources/ControlKit.swift +19 -8
  234. package/dist/native/sks-menubar/Sources/DiagnosticsViewController.swift +1 -2
  235. package/dist/native/sks-menubar/Sources/MCPServersViewController.swift +45 -17
  236. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +162 -0
  237. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +136 -190
  238. package/dist/native/sks-menubar/Sources/ProcessClient.swift +107 -11
  239. package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +5 -1
  240. package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +16 -4
  241. package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +4 -1
  242. package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +131 -0
  243. package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +288 -341
  244. package/dist/native/sks-menubar/Sources/RemoteTelegramCenterState.swift +447 -0
  245. package/dist/native/sks-menubar/Sources/RemoteTelegramRendering.swift +484 -0
  246. package/dist/native/sks-menubar/Sources/RemoteTelegramViewController.swift +364 -115
  247. package/dist/native/sks-menubar/Sources/SettingsViewController.swift +73 -12
  248. package/dist/native/sks-menubar/Sources/SidebarItem.swift +3 -3
  249. package/dist/native/sks-menubar/Sources/StatusItemController.swift +13 -13
  250. package/dist/native/sks-menubar/Sources/TelegramHubSupervisor.swift +469 -0
  251. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +1 -6
  252. package/dist/native/sks-menubar/Tests/AppRuntimeTestSupport.swift +12 -0
  253. package/dist/native/sks-menubar/Tests/NativeTelegramRegressionRunner.swift +55 -0
  254. package/dist/native/sks-menubar/Tests/ProcessClientTests.swift +33 -0
  255. package/dist/native/sks-menubar/Tests/ProvidersViewControllerTests.swift +142 -0
  256. package/dist/native/sks-menubar/Tests/RemoteTelegramCenterStateTests.swift +56 -0
  257. package/dist/native/sks-menubar/Tests/TelegramHubSupervisorTests.swift +55 -0
  258. package/dist/scripts/codex-lb-catalog-passthrough-check.js +120 -0
  259. package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +169 -0
  260. package/dist/scripts/codex-lb-desktop-bridge-check.js +394 -0
  261. package/dist/scripts/codex-lb-desktop-bridge-latency-check.js +196 -0
  262. package/dist/scripts/codex-lb-desktop-capabilities-check.js +287 -0
  263. package/dist/scripts/codex-lb-desktop-real-evidence-check.js +123 -0
  264. package/dist/scripts/codex-lb-fast-mode-truth-check.js +10 -3
  265. package/dist/scripts/codex-lb-fast-ui-preservation-check.js +81 -40
  266. package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +99 -37
  267. package/dist/scripts/context-graph-check.js +191 -0
  268. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +1 -1
  269. package/dist/scripts/gate-policy-audit-check.js +1 -1
  270. package/dist/scripts/helper-dedup-check.js +0 -2
  271. package/dist/scripts/installed-package-smoke-check.js +4 -1
  272. package/dist/scripts/latest-version-guidance-check.js +74 -0
  273. package/dist/scripts/release-metadata-check.js +21 -4
  274. package/dist/scripts/release-real-check.js +7 -0
  275. package/dist/scripts/ux-review-imagegen-blackbox-check.js +25 -1
  276. package/dist/scripts/ux-review-run-wires-imagegen-check.js +9 -1
  277. package/package.json +8 -3
  278. package/release-gates.v2.json +176 -1
  279. package/runtime-required-scripts.json +30 -0
  280. package/schemas/triwiki/context-graph.schema.json +119 -0
  281. package/dist/cli/install-helpers-codex-lb-selftest-chain.js +0 -112
  282. package/dist/cli/install-helpers-codex-lb-selftest.js +0 -481
  283. package/dist/core/triwiki/code-index-scanner.js +0 -309
@@ -1,40 +1,71 @@
1
1
  import fsp from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { runProcess, sha256 } from '../fsx.js';
3
+ import { runProcess, sha256, which } from '../fsx.js';
4
4
  import { isRecord } from '../json/records.js';
5
+ const KEYCHAIN_READER_SWIFT = `import Foundation
6
+ import Security
7
+ let a=CommandLine.arguments[1],s=CommandLine.arguments[2]
8
+ let q:[String:Any]=[kSecClass as String:kSecClassGenericPassword,kSecAttrAccount as String:a,kSecAttrService as String:s,kSecReturnData as String:true,kSecMatchLimit as String:kSecMatchLimitOne]
9
+ var item:CFTypeRef?
10
+ let r=SecItemCopyMatching(q as CFDictionary,&item)
11
+ if r==errSecItemNotFound{exit(44)}
12
+ guard r==errSecSuccess,let d=item as? Data,let v=String(data:d,encoding:.utf8) else{FileHandle.standardError.write(Data("keychain_status=\\(r)\\n".utf8));exit(1)}
13
+ print(v)`;
5
14
  export function validateTelegramConfig(value) {
6
- const issues = [];
15
+ const issues = telegramBaseConfigIssues(value);
7
16
  if (!isRecord(value))
8
17
  return { ok: false, issues: ['config_not_object'], config: null };
9
- if (value.schema !== 'sks.telegram-config.v1')
10
- issues.push('schema');
11
- if ('bot_token' in value || 'token' in value)
12
- issues.push('raw_bot_token_forbidden');
13
- const secret = value.bot_token_ref;
14
- if (!isRecord(secret) || !['keychain', 'external_file'].includes(String(secret.type || ''))) {
15
- issues.push('bot_token_ref');
18
+ if (value.schema !== 'sks.telegram-config.v2') {
19
+ issues.push(value.schema === 'sks.telegram-config.v1'
20
+ ? 'telegram_identity_migration_required'
21
+ : 'telegram_config_schema_unsupported');
22
+ return { ok: false, issues, config: null };
16
23
  }
17
- else if (secret.type === 'keychain') {
18
- if (!nonEmpty(secret.service) || !nonEmpty(secret.account))
19
- issues.push('keychain_reference_incomplete');
24
+ if (!isPositiveTelegramId(value.bot_user_id))
25
+ issues.push('bot_user_id');
26
+ if (!telegramFingerprint(value.bot_token_fingerprint))
27
+ issues.push('bot_token_fingerprint');
28
+ const pairedChatId = Array.isArray(value.paired_chat_ids) ? value.paired_chat_ids[0] : undefined;
29
+ const pairedUserId = Array.isArray(value.paired_user_ids) ? value.paired_user_ids[0] : undefined;
30
+ if (!isPositiveTelegramId(value.paired_chat_id) || value.paired_chat_id !== pairedChatId) {
31
+ issues.push('paired_chat_id');
20
32
  }
21
- else if (secret.type === 'external_file') {
22
- if (!nonEmpty(secret.path) || !path.isAbsolute(String(secret.path)))
23
- issues.push('external_secret_path_must_be_absolute');
33
+ if (!isPositiveTelegramId(value.paired_user_id) || value.paired_user_id !== pairedUserId) {
34
+ issues.push('paired_user_id');
35
+ }
36
+ if (!Number.isSafeInteger(value.config_generation) || Number(value.config_generation) < 1) {
37
+ issues.push('config_generation');
38
+ }
39
+ if (!telegramFingerprint(value.identity_fingerprint)) {
40
+ issues.push('identity_fingerprint');
41
+ }
42
+ else if (issues.length === 0
43
+ && value.identity_fingerprint !== telegramIdentityFingerprint({
44
+ botUserId: String(value.bot_user_id),
45
+ botTokenFingerprint: String(value.bot_token_fingerprint),
46
+ pairedChatId: String(value.paired_chat_id),
47
+ pairedUserId: String(value.paired_user_id),
48
+ configGeneration: Number(value.config_generation)
49
+ })) {
50
+ issues.push('identity_fingerprint_mismatch');
24
51
  }
25
- issues.push(...validateTelegramPrivatePairing(value).issues);
26
- if (typeof value.long_poll_timeout_sec === 'number' && (value.long_poll_timeout_sec < 1 || value.long_poll_timeout_sec > 50))
27
- issues.push('long_poll_timeout_sec');
28
- if (typeof value.owner_stale_ms === 'number' && value.owner_stale_ms < 5_000)
29
- issues.push('owner_stale_ms');
30
- if ('mini_app' in value)
31
- issues.push('mini_app_excluded_from_6_3_package');
32
52
  return {
33
53
  ok: issues.length === 0,
34
54
  issues,
35
55
  config: issues.length === 0 ? value : null
36
56
  };
37
57
  }
58
+ export function readTelegramPreviousConfig(value) {
59
+ if (!isRecord(value))
60
+ return null;
61
+ if (value.schema === 'sks.telegram-config.v2') {
62
+ return validateTelegramConfig(value).config;
63
+ }
64
+ if (value.schema !== 'sks.telegram-config.v1')
65
+ return null;
66
+ const issues = telegramBaseConfigIssues(value);
67
+ return issues.length === 0 ? value : null;
68
+ }
38
69
  export async function loadTelegramConfig(file) {
39
70
  const parsed = JSON.parse(await fsp.readFile(file, 'utf8'));
40
71
  const validation = validateTelegramConfig(parsed);
@@ -46,15 +77,10 @@ export async function resolveTelegramBotToken(ref, options = {}) {
46
77
  if (ref.type === 'keychain') {
47
78
  if (!ref.service || !ref.account)
48
79
  throw new Error('telegram_keychain_reference_incomplete');
49
- if (process.platform !== 'darwin')
50
- throw new Error('telegram_keychain_requires_macos');
51
- const run = options.run ?? runProcess;
52
- const result = await run('/usr/bin/security', [
53
- 'find-generic-password', '-w', '-s', ref.service, '-a', ref.account
54
- ], { timeoutMs: 5_000, maxOutputBytes: 8 * 1024 });
55
- if (result.code !== 0 || result.timedOut)
80
+ const token = await readTelegramKeychainToken(ref.service, ref.account, options);
81
+ if (token === null)
56
82
  throw new Error('telegram_keychain_lookup_failed');
57
- return validateTelegramBotToken(result.stdout.trim());
83
+ return token;
58
84
  }
59
85
  if (!ref.path || !path.isAbsolute(ref.path))
60
86
  throw new Error('telegram_external_secret_path_invalid');
@@ -67,9 +93,76 @@ export async function resolveTelegramBotToken(ref, options = {}) {
67
93
  throw new Error('telegram_external_secret_permissions_must_be_0600');
68
94
  return validateTelegramBotToken((await fsp.readFile(ref.path, 'utf8')).trim());
69
95
  }
96
+ export async function readTelegramKeychainToken(service, account, options = {}) {
97
+ if (!service.trim() || !account.trim())
98
+ throw new Error('telegram_keychain_reference_incomplete');
99
+ if (process.platform !== 'darwin')
100
+ throw new Error('telegram_keychain_requires_macos');
101
+ const run = options.run ?? runProcess;
102
+ const swift = options.swiftBin
103
+ ?? await which('swift').catch(() => null)
104
+ ?? '/usr/bin/swift';
105
+ const timeoutMs = Math.max(500, Math.min(30_000, options.timeoutMs ?? 5_000));
106
+ const result = await run(swift, ['-e', KEYCHAIN_READER_SWIFT, account, service], {
107
+ timeoutMs,
108
+ maxOutputBytes: 8 * 1024
109
+ });
110
+ if (result.code === 44 && !result.timedOut)
111
+ return null;
112
+ if (result.code !== 0 || result.timedOut)
113
+ throw new Error('telegram_keychain_lookup_failed');
114
+ return validateTelegramBotToken(result.stdout.trim());
115
+ }
70
116
  export function telegramTokenFingerprint(token) {
71
117
  return `sha256:${sha256(token)}`;
72
118
  }
119
+ export function telegramIdentityFingerprint(input) {
120
+ return `sha256:${sha256(JSON.stringify([
121
+ 'sks.telegram-identity.v1',
122
+ input.botUserId,
123
+ input.botTokenFingerprint,
124
+ input.pairedChatId,
125
+ input.pairedUserId,
126
+ input.configGeneration
127
+ ]))}`;
128
+ }
129
+ export function telegramRuntimeIdentityFingerprint(config, botTokenFingerprint) {
130
+ if (config.bot_token_fingerprint !== botTokenFingerprint) {
131
+ throw new Error('telegram_config_token_identity_mismatch:run_setup_to_rotate_token');
132
+ }
133
+ return config.identity_fingerprint;
134
+ }
135
+ export function telegramConfigPairing(config) {
136
+ return {
137
+ chatId: config.paired_chat_id,
138
+ userId: config.paired_user_id
139
+ };
140
+ }
141
+ export function buildTelegramConfigV2(input) {
142
+ const identityFingerprint = telegramIdentityFingerprint({
143
+ botUserId: input.botUserId,
144
+ botTokenFingerprint: input.botTokenFingerprint,
145
+ pairedChatId: input.pairedChatId,
146
+ pairedUserId: input.pairedUserId,
147
+ configGeneration: input.configGeneration
148
+ });
149
+ return {
150
+ schema: 'sks.telegram-config.v2',
151
+ bot_token_ref: input.botTokenRef,
152
+ paired_chat_ids: [input.pairedChatId],
153
+ paired_user_ids: [input.pairedUserId],
154
+ bot_user_id: input.botUserId,
155
+ bot_token_fingerprint: input.botTokenFingerprint,
156
+ paired_chat_id: input.pairedChatId,
157
+ paired_user_id: input.pairedUserId,
158
+ config_generation: input.configGeneration,
159
+ identity_fingerprint: identityFingerprint,
160
+ ...(input.longPollTimeoutSec === undefined ? {} : { long_poll_timeout_sec: input.longPollTimeoutSec }),
161
+ ...(input.ownerStaleMs === undefined ? {} : { owner_stale_ms: input.ownerStaleMs }),
162
+ ...(input.protectContent === undefined ? {} : { protect_content: input.protectContent }),
163
+ ...(input.silentNotifications === undefined ? {} : { silent_notifications: input.silentNotifications })
164
+ };
165
+ }
73
166
  export function validateTelegramBotToken(token) {
74
167
  if (!/^\d{5,}:[A-Za-z0-9_-]{20,}$/.test(token))
75
168
  throw new Error('telegram_bot_token_format_invalid');
@@ -98,7 +191,41 @@ function nonEmpty(value) {
98
191
  return typeof value === 'string' && value.trim().length > 0;
99
192
  }
100
193
  function positiveStringIdArray(value) {
101
- return Array.isArray(value) && value.length > 0 && value.every(isPositiveTelegramId);
194
+ return Array.isArray(value) && value.length === 1 && value.every(isPositiveTelegramId);
195
+ }
196
+ function telegramFingerprint(value) {
197
+ return typeof value === 'string' && /^sha256:[a-f0-9]{64}$/.test(value);
198
+ }
199
+ function telegramBaseConfigIssues(value) {
200
+ if (!isRecord(value))
201
+ return ['config_not_object'];
202
+ const issues = [];
203
+ if ('bot_token' in value || 'token' in value)
204
+ issues.push('raw_bot_token_forbidden');
205
+ const secret = value.bot_token_ref;
206
+ if (!isRecord(secret) || !['keychain', 'external_file'].includes(String(secret.type || ''))) {
207
+ issues.push('bot_token_ref');
208
+ }
209
+ else if (secret.type === 'keychain') {
210
+ if (!nonEmpty(secret.service) || !nonEmpty(secret.account))
211
+ issues.push('keychain_reference_incomplete');
212
+ }
213
+ else if (secret.type === 'external_file') {
214
+ if (!nonEmpty(secret.path) || !path.isAbsolute(String(secret.path))) {
215
+ issues.push('external_secret_path_must_be_absolute');
216
+ }
217
+ }
218
+ issues.push(...validateTelegramPrivatePairing(value).issues);
219
+ if (typeof value.long_poll_timeout_sec === 'number'
220
+ && (value.long_poll_timeout_sec < 1 || value.long_poll_timeout_sec > 50)) {
221
+ issues.push('long_poll_timeout_sec');
222
+ }
223
+ if (typeof value.owner_stale_ms === 'number' && value.owner_stale_ms < 5_000) {
224
+ issues.push('owner_stale_ms');
225
+ }
226
+ if ('mini_app' in value)
227
+ issues.push('mini_app_not_supported');
228
+ return issues;
102
229
  }
103
230
  function pairingFieldMissing(value, field) {
104
231
  return !(field in value) || (Array.isArray(value[field]) && value[field].length === 0);
@@ -1,7 +1,8 @@
1
1
  import crypto from 'node:crypto';
2
2
  import path from 'node:path';
3
- import { globalSksRoot, nowIso } from '../fsx.js';
3
+ import { globalSksRoot, nowIso, sha256, writeTextAtomic } from '../fsx.js';
4
4
  import { TelegramBotApiClient, TelegramBotApiError } from './bot-api.js';
5
+ import { TelegramIngressSpool } from './ingress-spool.js';
5
6
  import { TelegramActionBroker, TelegramAuditLedger, TelegramIdempotencyLedger, TelegramTopicRegistry } from './ledgers.js';
6
7
  import { TelegramOwnerLock } from './owner-lock.js';
7
8
  const COMMAND_POLICY = {
@@ -19,6 +20,7 @@ const COMMAND_POLICY = {
19
20
  '/open': { kind: 'read', risk: 'R0' }
20
21
  };
21
22
  const UI_POLICY = {
23
+ status: { command: '/status', kind: 'read', risk: 'R0' },
22
24
  open: { command: '/open', kind: 'read', risk: 'R0' },
23
25
  diff: { command: '/diff', kind: 'read', risk: 'R0' },
24
26
  proof: { command: '/proof', kind: 'read', risk: 'R0' },
@@ -37,23 +39,29 @@ export class TelegramHubRouter {
37
39
  this.commandRevision = options.commandRevision ?? 1;
38
40
  this.now = options.now ?? Date.now;
39
41
  }
40
- async handleUpdate(update) {
42
+ async handleUpdate(update, options = {}) {
41
43
  const actor = actorFromUpdate(update);
42
44
  if (!actor || !this.isPaired(actor.chatId, actor.userId, actor.chatType)) {
43
45
  await this.audit(update, actor?.chatId ?? 'unknown', actor?.topicId ?? null, null, 'unpaired', 'rejected', 'not_paired');
44
46
  return result(false, 'not_paired');
45
47
  }
46
- if (!await this.options.idempotency.claim(update.update_id)) {
48
+ if (!options.skipIdempotency && !await this.options.idempotency.claim(update.update_id)) {
47
49
  await this.audit(update, actor.chatId, actor.topicId, null, 'duplicate', 'rejected', 'duplicate_update_id');
48
50
  return result(false, 'duplicate_update_id');
49
51
  }
50
52
  const callback = update.callback_query;
51
53
  if (callback?.data?.startsWith('cb:')) {
54
+ const inspected = await this.options.actions.inspect(callback.data);
55
+ const currentRoute = inspected.action
56
+ ? await this.options.topics.findBySession(inspected.action.machine_id, inspected.action.project_id, inspected.action.session_id)
57
+ : null;
52
58
  const resolution = await this.options.actions.resolve({
53
59
  callback_data: callback.data,
54
60
  chat_id: actor.chatId,
55
61
  message_thread_id: actor.topicId ?? 0,
62
+ route_generation: currentRoute?.generation ?? null,
56
63
  revision: this.commandRevision,
64
+ update_id: update.update_id,
57
65
  now: this.now()
58
66
  });
59
67
  await this.audit(update, actor.chatId, actor.topicId, callback.data.slice(3), 'callback', resolution.ok ? 'accepted' : 'rejected', resolution.status);
@@ -83,7 +91,7 @@ export class TelegramHubRouter {
83
91
  const prompt = policy.kind === 'input'
84
92
  ? inputText(text, command, Boolean(staticUiAction))
85
93
  : command || staticUiAction?.command || '';
86
- const action = makeAction(route, policy.kind, policy.risk, prompt, this.commandRevision, this.now());
94
+ const action = makeAction(route, policy.kind, policy.risk, prompt, this.commandRevision, this.now(), update.update_id);
87
95
  if (action.risk === 'R2') {
88
96
  const created = await this.options.actions.create(action, { chat_id: route.chat_id, message_thread_id: route.message_thread_id });
89
97
  await this.audit(update, actor.chatId, actor.topicId, created.callback_data.slice(3), command, 'accepted', 'approval_required');
@@ -98,7 +106,7 @@ export class TelegramHubRouter {
98
106
  if (!text)
99
107
  return this.reject(update, actor, 'empty', 'unsupported_update');
100
108
  const forceReply = message?.reply_to_message?.message_id;
101
- const action = makeAction(route, 'input', 'R1', text, this.commandRevision, this.now());
109
+ const action = makeAction(route, 'input', 'R1', text, this.commandRevision, this.now(), update.update_id);
102
110
  const authorization = authorizeTelegramAction(action.risk, { exactTopic: true, explicitUserMessage: true });
103
111
  if (!authorization.ok)
104
112
  return this.reject(update, actor, 'input', authorization.reason);
@@ -127,7 +135,9 @@ export class TelegramHubRouter {
127
135
  command_kind: commandKind,
128
136
  decision,
129
137
  policy_reason: policyReason,
130
- effect_receipt: null
138
+ effect_receipt: null,
139
+ telegram_delivery: 'not_attempted',
140
+ telegram_delivery_receipt: null
131
141
  });
132
142
  }
133
143
  }
@@ -151,46 +161,269 @@ export class TelegramPollingHub {
151
161
  processor;
152
162
  ownerLock;
153
163
  longPollTimeoutSeconds;
164
+ options;
154
165
  offset = 0;
155
- constructor(client, processor, ownerLock, longPollTimeoutSeconds = 25) {
166
+ offsetInitialized = false;
167
+ generation = crypto.randomUUID();
168
+ startedAt;
169
+ activeUpdateIds = new Set();
170
+ laneTails = new Map();
171
+ inFlight = new Set();
172
+ maxConflictRetries;
173
+ conflictBackoffMs;
174
+ maxPollRetries;
175
+ pollBackoffMs;
176
+ startupRequestTimeoutMs;
177
+ sleep;
178
+ now;
179
+ pid;
180
+ consecutiveConflicts = 0;
181
+ readyEstablished = false;
182
+ lastSuccessfulPollAt = null;
183
+ constructor(client, processor, ownerLock, longPollTimeoutSeconds = 25, options = {}) {
156
184
  this.client = client;
157
185
  this.processor = processor;
158
186
  this.ownerLock = ownerLock;
159
187
  this.longPollTimeoutSeconds = longPollTimeoutSeconds;
188
+ this.options = options;
189
+ this.maxConflictRetries = Math.max(0, options.maxConflictRetries ?? 3);
190
+ this.conflictBackoffMs = options.conflictBackoffMs?.length
191
+ ? options.conflictBackoffMs
192
+ : [1_000, 2_000, 5_000];
193
+ this.maxPollRetries = Math.max(0, options.maxPollRetries ?? 5);
194
+ this.pollBackoffMs = options.pollBackoffMs?.length
195
+ ? options.pollBackoffMs
196
+ : [1_000, 2_000, 5_000, 10_000, 30_000];
197
+ this.startupRequestTimeoutMs = Math.max(1_000, options.startupRequestTimeoutMs ?? 8_000);
198
+ this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
199
+ this.now = options.now ?? Date.now;
200
+ this.pid = options.pid ?? process.pid;
201
+ this.startedAt = new Date(this.now()).toISOString();
160
202
  }
161
203
  async ensureLongPollingAllowed() {
162
204
  const info = await this.client.call('getWebhookInfo', {});
163
- if (info?.url)
164
- throw new Error('telegram_webhook_conflict');
205
+ if (typeof info?.url === 'string' && info.url.trim()) {
206
+ throw new Error('telegram_webhook_conflict:remove_webhook_before_long_polling');
207
+ }
165
208
  }
166
- async pollOnce() {
209
+ async establishReadiness(signal) {
210
+ await this.ensureOffset();
211
+ await this.writeHealth('starting', null);
212
+ const result = await this.receiveBatch(0, false, this.startupRequestTimeoutMs, signal);
213
+ this.readyEstablished = result.ok && result.stopped_reason === null;
214
+ return { ok: result.ok, received: result.processed, stopped_reason: result.stopped_reason };
215
+ }
216
+ async pollOnce(options = {}) {
217
+ await this.ensureOffset();
218
+ const result = await this.receiveBatch(this.longPollTimeoutSeconds, true, undefined, options.signal);
219
+ if (!result.ok || result.stopped_reason === 'aborted')
220
+ return result;
221
+ if (options.awaitProcessing !== false) {
222
+ await this.waitForIdle();
223
+ await this.processor.tick?.();
224
+ }
225
+ else if (this.inFlight.size === 0) {
226
+ await this.processor.tick?.();
227
+ }
228
+ await this.ownerLock.heartbeat();
229
+ return result;
230
+ }
231
+ async run(signal) {
167
232
  try {
168
- const updates = await this.client.getUpdates({ offset: this.offset, timeout: this.longPollTimeoutSeconds });
169
- for (const update of updates) {
170
- await this.processor.processUpdate(update);
171
- this.offset = Math.max(this.offset, update.update_id + 1);
233
+ if (!this.readyEstablished) {
234
+ await this.ensureLongPollingAllowed();
235
+ const readiness = await this.establishReadiness(signal);
236
+ if (readiness.stopped_reason === 'aborted') {
237
+ return { ok: true, stopped_reason: 'aborted' };
238
+ }
239
+ if (!readiness.ok) {
240
+ return { ok: false, stopped_reason: readiness.stopped_reason ?? 'receive_not_ready' };
241
+ }
172
242
  }
243
+ await this.drainPending();
244
+ while (!signal.aborted) {
245
+ const result = await this.pollOnce({ awaitProcessing: false, signal });
246
+ if (result.stopped_reason === 'aborted')
247
+ return { ok: true, stopped_reason: 'aborted' };
248
+ if (!result.ok)
249
+ return { ok: false, stopped_reason: result.stopped_reason ?? 'poll_failed' };
250
+ }
251
+ return { ok: true, stopped_reason: 'aborted' };
252
+ }
253
+ finally {
254
+ await this.waitForIdle();
173
255
  await this.processor.tick?.();
174
- await this.ownerLock.heartbeat();
175
- return { ok: true, processed: updates.length, stopped_reason: null };
176
256
  }
177
- catch (error) {
178
- if (error instanceof TelegramBotApiError && error.errorCode === 409) {
179
- await this.ownerLock.release();
180
- return { ok: false, processed: 0, stopped_reason: 'telegram_409_conflict' };
257
+ }
258
+ async drainPending() {
259
+ const ingress = this.options.ingress;
260
+ if (!ingress)
261
+ return 0;
262
+ const entries = await ingress.pending();
263
+ let scheduled = 0;
264
+ for (const entry of entries) {
265
+ if (this.activeUpdateIds.has(entry.update_id))
266
+ continue;
267
+ let routed = entry.routed;
268
+ try {
269
+ if (!routed && this.processor.prepareUpdate) {
270
+ routed = await this.processor.prepareUpdate(entry.update, { durablyAdmitted: true });
271
+ await ingress.markRouted(entry.update_id, routed);
272
+ }
273
+ }
274
+ catch (error) {
275
+ await ingress.markFailed(entry.update_id, error);
276
+ continue;
181
277
  }
182
- throw error;
278
+ this.scheduleEntry(entry.update, routed);
279
+ scheduled += 1;
183
280
  }
281
+ await this.writeHealth('ready', null);
282
+ return scheduled;
184
283
  }
185
- async run(signal) {
186
- await this.ensureLongPollingAllowed();
187
- while (!signal.aborted) {
188
- const result = await this.pollOnce();
189
- if (!result.ok)
190
- return { ok: false, stopped_reason: result.stopped_reason ?? 'poll_failed' };
284
+ async waitForIdle() {
285
+ while (this.inFlight.size > 0) {
286
+ await Promise.allSettled([...this.inFlight]);
191
287
  }
192
- await this.ownerLock.release();
193
- return { ok: true, stopped_reason: 'aborted' };
288
+ }
289
+ async receiveBatch(timeout, dispatch, requestTimeoutMs, signal) {
290
+ let conflictAttempt = 0;
291
+ let pollAttempt = 0;
292
+ while (true) {
293
+ if (signal?.aborted)
294
+ return { ok: true, processed: 0, stopped_reason: 'aborted' };
295
+ try {
296
+ const updates = await this.client.getUpdates({ offset: this.offset, timeout }, {
297
+ ...(requestTimeoutMs === undefined ? {} : { requestTimeoutMs }),
298
+ ...(signal === undefined ? {} : { signal })
299
+ });
300
+ for (const update of updates) {
301
+ if (this.options.ingress) {
302
+ await this.options.ingress.admit(update);
303
+ }
304
+ else if (dispatch) {
305
+ await this.processor.processUpdate(update);
306
+ }
307
+ this.offset = Math.max(this.offset, update.update_id + 1);
308
+ }
309
+ this.consecutiveConflicts = 0;
310
+ this.lastSuccessfulPollAt = new Date(this.now()).toISOString();
311
+ await this.writeHealth('ready', null);
312
+ if (dispatch && this.options.ingress)
313
+ await this.drainPending();
314
+ return { ok: true, processed: updates.length, stopped_reason: null };
315
+ }
316
+ catch (error) {
317
+ if (signal?.aborted)
318
+ return { ok: true, processed: 0, stopped_reason: 'aborted' };
319
+ if (!(error instanceof TelegramBotApiError) || error.errorCode !== 409) {
320
+ this.consecutiveConflicts = 0;
321
+ await this.ownerLock.heartbeat().catch(() => undefined);
322
+ if (pollAttempt >= this.maxPollRetries) {
323
+ await this.writeHealth('terminal', 'telegram_poll_failed');
324
+ return { ok: false, processed: 0, stopped_reason: 'telegram_poll_failed' };
325
+ }
326
+ await this.writeHealth('degraded', 'telegram_poll_retrying');
327
+ const delay = this.pollBackoffMs[Math.min(pollAttempt, this.pollBackoffMs.length - 1)] ?? 30_000;
328
+ pollAttempt += 1;
329
+ await this.sleepWithSignal(Math.max(0, delay), signal);
330
+ continue;
331
+ }
332
+ this.consecutiveConflicts += 1;
333
+ await this.ownerLock.heartbeat().catch(() => undefined);
334
+ if (conflictAttempt >= this.maxConflictRetries) {
335
+ await this.writeHealth('terminal', 'telegram_409_conflict');
336
+ return { ok: false, processed: 0, stopped_reason: 'telegram_409_conflict' };
337
+ }
338
+ await this.writeHealth('degraded', 'telegram_409_conflict_retrying');
339
+ const delay = this.conflictBackoffMs[Math.min(conflictAttempt, this.conflictBackoffMs.length - 1)] ?? 5_000;
340
+ conflictAttempt += 1;
341
+ await this.sleepWithSignal(Math.max(0, delay), signal);
342
+ }
343
+ }
344
+ }
345
+ async sleepWithSignal(ms, signal) {
346
+ if (!signal) {
347
+ await this.sleep(ms);
348
+ return;
349
+ }
350
+ if (signal.aborted)
351
+ return;
352
+ let onAbort = null;
353
+ const aborted = new Promise((resolve) => {
354
+ onAbort = () => resolve();
355
+ signal.addEventListener('abort', onAbort, { once: true });
356
+ });
357
+ try {
358
+ await Promise.race([this.sleep(ms), aborted]);
359
+ }
360
+ finally {
361
+ if (onAbort)
362
+ signal.removeEventListener('abort', onAbort);
363
+ }
364
+ }
365
+ scheduleEntry(update, routed) {
366
+ const ingress = this.options.ingress;
367
+ if (!ingress)
368
+ return;
369
+ const updateId = update.update_id;
370
+ const lane = updateLane(update);
371
+ this.activeUpdateIds.add(updateId);
372
+ const previous = this.laneTails.get(lane) ?? Promise.resolve();
373
+ let task;
374
+ task = previous
375
+ .catch(() => undefined)
376
+ .then(async () => {
377
+ if (routed && this.processor.executePreparedUpdate) {
378
+ await this.processor.executePreparedUpdate(update, routed);
379
+ }
380
+ else {
381
+ await this.processor.processUpdate(update);
382
+ }
383
+ await ingress.markCompleted(updateId);
384
+ })
385
+ .catch(async (error) => {
386
+ await ingress.markFailed(updateId, error);
387
+ })
388
+ .finally(() => {
389
+ this.activeUpdateIds.delete(updateId);
390
+ this.inFlight.delete(task);
391
+ if (this.laneTails.get(lane) === task)
392
+ this.laneTails.delete(lane);
393
+ });
394
+ this.laneTails.set(lane, task);
395
+ this.inFlight.add(task);
396
+ }
397
+ async ensureOffset() {
398
+ if (this.offsetInitialized)
399
+ return;
400
+ this.offset = this.options.ingress ? await this.options.ingress.nextOffset() : 0;
401
+ this.offsetInitialized = true;
402
+ }
403
+ async writeHealth(state, terminalReason) {
404
+ if (!this.options.healthPath)
405
+ return;
406
+ const counts = this.options.ingress
407
+ ? await this.options.ingress.counts()
408
+ : { pending: this.activeUpdateIds.size, dead_letters: 0 };
409
+ const at = new Date(this.now()).toISOString();
410
+ const effectiveState = state === 'ready' && counts.dead_letters > 0
411
+ ? 'degraded'
412
+ : state;
413
+ const health = {
414
+ schema: 'sks.telegram-receive-health.v1',
415
+ generation: this.generation,
416
+ pid: this.pid,
417
+ state: effectiveState,
418
+ started_at: this.startedAt,
419
+ updated_at: at,
420
+ last_successful_poll_at: this.lastSuccessfulPollAt,
421
+ consecutive_conflicts: this.consecutiveConflicts,
422
+ pending_ingress: counts.pending,
423
+ dead_letter_ingress: counts.dead_letters,
424
+ terminal_reason: effectiveState === 'terminal' ? terminalReason : null
425
+ };
426
+ await writeTextAtomic(this.options.healthPath, `${JSON.stringify(health, null, 2)}\n`, { mode: 0o600 });
194
427
  }
195
428
  }
196
429
  export function telegramHubPaths(root = globalSksRoot()) {
@@ -202,7 +435,9 @@ export function telegramHubPaths(root = globalSksRoot()) {
202
435
  topics: path.join(base, 'topic-registry.json'),
203
436
  actions: path.join(base, 'actions.json'),
204
437
  audit: path.join(base, 'audit.jsonl'),
205
- projection: path.join(base, 'projection-state.json')
438
+ projection: path.join(base, 'projection-state.json'),
439
+ ingress: path.join(base, 'ingress-spool.json'),
440
+ receive_health: path.join(base, 'receive-health.json')
206
441
  };
207
442
  }
208
443
  function actorFromUpdate(update) {
@@ -214,17 +449,28 @@ function actorFromUpdate(update) {
214
449
  chatId: String(message.chat.id),
215
450
  userId: String(from.id),
216
451
  chatType: message.chat.type,
217
- topicId: Number.isInteger(message.message_thread_id) ? message.message_thread_id ?? null : null,
452
+ topicId: message.is_topic_message === true && Number.isInteger(message.message_thread_id)
453
+ ? message.message_thread_id ?? null
454
+ : null,
218
455
  message: update.message ?? null
219
456
  };
220
457
  }
221
- function makeAction(route, kind, risk, prompt, revision, now) {
458
+ function makeAction(route, kind, risk, prompt, revision, now, updateId) {
222
459
  return {
223
460
  schema: 'sks.remote-action.v1',
224
- action_id: crypto.randomUUID(),
461
+ action_id: `tg-${sha256([
462
+ updateId,
463
+ route.machine_id,
464
+ route.project_id,
465
+ route.session_id,
466
+ route.generation,
467
+ kind,
468
+ revision
469
+ ].join(':')).slice(0, 40)}`,
225
470
  machine_id: route.machine_id,
226
471
  project_id: route.project_id,
227
472
  session_id: route.session_id,
473
+ route_generation: route.generation,
228
474
  kind,
229
475
  risk,
230
476
  prompt: prompt.slice(0, kind === 'input' ? 16 * 1024 : 120),
@@ -245,3 +491,12 @@ function inputText(text, command, staticAction) {
245
491
  function result(ok, status) {
246
492
  return { schema: 'sks.telegram-route-result.v1', ok, status, action: null, session_picker: false };
247
493
  }
494
+ function updateLane(update) {
495
+ const message = update.message ?? update.callback_query?.message;
496
+ if (!message)
497
+ return `update:${update.update_id}`;
498
+ const topic = message.is_topic_message === true && Number.isInteger(message.message_thread_id)
499
+ ? message.message_thread_id
500
+ : 0;
501
+ return `${String(message.chat.id)}:${topic}`;
502
+ }
@@ -3,8 +3,10 @@ export * from './config.js';
3
3
  export * from './owner-lock.js';
4
4
  export * from './ledgers.js';
5
5
  export * from './bot-api.js';
6
+ export * from './ingress-spool.js';
6
7
  export * from './messages.js';
7
8
  export * from './hub.js';
8
9
  export * from './runtime.js';
9
10
  export * from './setup.js';
10
11
  export * from './service.js';
12
+ export * from './center-snapshot.js';