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,8 +1,14 @@
1
1
  import path from 'node:path';
2
2
  import { isHelpRequest } from '../../cli/help.js';
3
- import { globalSksRoot, projectRoot, readJson, readStdin } from '../fsx.js';
3
+ import { globalSksRoot, nowIso, projectRoot, readJson, readStdin, writeTextAtomic } from '../fsx.js';
4
4
  import { RemoteCodexSessionBindingStore, loadRemoteMachineRegistry, loadRemoteSessionIndex, remoteCodexSessionBindingsPath, remoteMachineRegistryPath, remoteSessionIndexPath, validateRemoteMachineRegistry, validateRemoteSessionIndex } from '../remote/index.js';
5
- import { loadTelegramConfig, resolveTelegramBotToken, TelegramActionBroker, TelegramAuditLedger, TelegramBotApiClient, TelegramHubRouter, TelegramHubRuntime, TelegramIdempotencyLedger, TelegramOwnerLock, TelegramMessageProjector, TelegramPollingHub, TelegramTopicRegistry, installAndStartTelegramHubService, restartTelegramHubService, setupTelegramLocalCoding, stopTelegramHubService, telegramHubPaths, telegramHubServiceStatus, telegramTokenFingerprint, validateTelegramConfig, validateTelegramPrivatePairing } from '../telegram/index.js';
5
+ import { loadTelegramConfig, probeTelegramBotReadiness, resolveTelegramBotToken, TelegramActionBroker, TelegramAuditLedger, TelegramBotApiClient, TelegramBotApiError, TelegramHubRouter, TelegramHubRuntime, TelegramIdempotencyLedger, TelegramIngressSpool, TelegramOwnerLock, TelegramMessageProjector, TelegramPollingHub, TelegramTopicRegistry, telegramCenterLiveSnapshot, telegramCenterSnapshot, installAndStartTelegramHubService, restartTelegramHubService, setupTelegramLocalCoding, stopTelegramHubService, telegramHubPaths, telegramHubServiceStatus, telegramRuntimeIdentityFingerprint, telegramTokenFingerprint, validateTelegramConfig, validateTelegramPrivatePairing } from '../telegram/index.js';
6
+ export const TELEGRAM_STATUS_NATIVE_DEADLINE_MS = 12_000;
7
+ export const TELEGRAM_STATUS_TOKEN_LOOKUP_TIMEOUT_MS = 3_000;
8
+ export const TELEGRAM_STATUS_BOT_REQUEST_TIMEOUT_MS = 1_500;
9
+ export const TELEGRAM_STATUS_BOT_MAX_RETRIES = 0;
10
+ export const TELEGRAM_STATUS_LIVE_PROBE_BUDGET_MS = TELEGRAM_STATUS_TOKEN_LOOKUP_TIMEOUT_MS
11
+ + TELEGRAM_STATUS_BOT_REQUEST_TIMEOUT_MS * 2;
6
12
  export async function telegramCommand(args = []) {
7
13
  if (isHelpRequest(args)) {
8
14
  printTelegramUsage();
@@ -17,6 +23,23 @@ export async function telegramCommand(args = []) {
17
23
  if (action === 'status') {
18
24
  return print(await telegramStatus(args, root, paths, configPath), json);
19
25
  }
26
+ if (action === 'center-snapshot') {
27
+ const controllingRoot = path.resolve(readOption(args, '--project-root') ?? await projectRoot());
28
+ const sortDirection = telegramCenterSortDirection(readOption(args, '--sort-direction'));
29
+ return print(await telegramCenterSnapshot({
30
+ projectRoot: controllingRoot,
31
+ sessionId: readOption(args, '--session-id'),
32
+ cursor: readOption(args, '--cursor'),
33
+ sortDirection
34
+ }), json);
35
+ }
36
+ if (action === 'center-live') {
37
+ const controllingRoot = path.resolve(readOption(args, '--project-root') ?? await projectRoot());
38
+ return print(await telegramCenterLiveSnapshot({
39
+ projectRoot: controllingRoot,
40
+ sessionId: readOption(args, '--session-id')
41
+ }), json);
42
+ }
20
43
  if (action === 'setup') {
21
44
  if (!args.includes('--bot-token-stdin'))
22
45
  return fail('bot_token_stdin_required', ['setup --bot-token-stdin --project-root <path>'], json);
@@ -36,19 +59,83 @@ export async function telegramCommand(args = []) {
36
59
  const validation = validateTelegramConfig(await readJson(configPath, null));
37
60
  return print({ schema: 'sks.telegram-config-validation.v1', ...validation, config: validation.config ? redactConfig(validation.config) : null }, json);
38
61
  }
62
+ if (action === 'dead-letters') {
63
+ const request = telegramDeadLetterCommandRequest(args);
64
+ const config = await loadTelegramConfig(configPath);
65
+ const token = await resolveTelegramBotToken(config.bot_token_ref);
66
+ const tokenFingerprint = telegramTokenFingerprint(token);
67
+ const ingress = new TelegramIngressSpool(paths.ingress, {
68
+ identityFingerprint: telegramRuntimeIdentityFingerprint(config, tokenFingerprint),
69
+ legacyTokenFingerprint: tokenFingerprint
70
+ });
71
+ if (request.action === 'list' || request.action === 'inspect') {
72
+ const deadLetters = await ingress.deadLetters(request.updateId ?? undefined);
73
+ if (request.action === 'inspect' && deadLetters.length === 0) {
74
+ throw new Error('telegram_ingress_dead_letter_not_found');
75
+ }
76
+ return print({
77
+ schema: 'sks.telegram-ingress-dead-letter-list.v1',
78
+ ok: true,
79
+ unhealthy: deadLetters.length > 0,
80
+ dead_letters: deadLetters,
81
+ next_action: deadLetters.length > 0
82
+ ? 'Stop the Hub, then explicitly requeue only requeue_safe entries or acknowledge entries whose effect may be ambiguous.'
83
+ : 'No dead-lettered Telegram ingress remains.'
84
+ }, json);
85
+ }
86
+ const controllingRoot = path.resolve(readOption(args, '--project-root') ?? await projectRoot());
87
+ const service = await telegramHubServiceStatus({
88
+ projectRoot: controllingRoot,
89
+ globalRoot: root
90
+ });
91
+ if (service.running)
92
+ throw new Error('telegram_hub_must_be_stopped_before_dead_letter_recovery');
93
+ if (!request.confirmed)
94
+ throw new Error('telegram_dead_letter_recovery_confirmation_required');
95
+ if (request.action === 'requeue') {
96
+ const requeued = await ingress.requeueDeadLetter(request.updateId);
97
+ const counts = await ingress.counts();
98
+ await updateTelegramIngressHealth(paths.receive_health, counts);
99
+ return print({
100
+ schema: 'sks.telegram-ingress-recovery.v1',
101
+ ok: true,
102
+ disposition: 'requeued',
103
+ update_id: requeued.update_id,
104
+ replayed: false,
105
+ pending_ingress: counts.pending,
106
+ dead_letter_ingress: counts.dead_letters,
107
+ next_action: 'Restart the Telegram Hub to process the explicitly requeued request.'
108
+ }, json);
109
+ }
110
+ const acknowledged = await ingress.acknowledgeDeadLetters(request.updateId === null ? undefined : [request.updateId]);
111
+ const counts = await ingress.counts();
112
+ await updateTelegramIngressHealth(paths.receive_health, counts);
113
+ return print({
114
+ schema: 'sks.telegram-ingress-recovery.v1',
115
+ ok: true,
116
+ disposition: 'acknowledged',
117
+ acknowledged_dead_letters: acknowledged,
118
+ acknowledged_update_id: request.updateId,
119
+ replayed: false,
120
+ resend_required: acknowledged > 0,
121
+ next_action: acknowledged > 0
122
+ ? 'Restart the Telegram Hub, then resend each failed request from the paired private chat.'
123
+ : 'No dead-lettered Telegram ingress remained.'
124
+ }, json);
125
+ }
39
126
  if (action === 'hub') {
40
127
  const hubAction = args[1] && !String(args[1]).startsWith('-') ? String(args[1]) : 'run';
41
128
  const controllingRoot = path.resolve(readOption(args, '--project-root') ?? await projectRoot());
42
129
  if (hubAction === 'start') {
43
130
  await assertHubSetupReady(controllingRoot, root, configPath);
44
- return print(await installAndStartTelegramHubService({ projectRoot: controllingRoot, globalRoot: root }), json);
131
+ return print(markTelegramHubMutationResult(await installAndStartTelegramHubService({ projectRoot: controllingRoot, globalRoot: root })), json);
45
132
  }
46
133
  if (hubAction === 'stop') {
47
- return print(await stopTelegramHubService({ projectRoot: controllingRoot, globalRoot: root }), json);
134
+ return print(markTelegramHubMutationResult(await stopTelegramHubService({ projectRoot: controllingRoot, globalRoot: root })), json);
48
135
  }
49
136
  if (hubAction === 'restart') {
50
137
  await assertHubSetupReady(controllingRoot, root, configPath);
51
- return print(await restartTelegramHubService({ projectRoot: controllingRoot, globalRoot: root }), json);
138
+ return print(markTelegramHubMutationResult(await restartTelegramHubService({ projectRoot: controllingRoot, globalRoot: root })), json);
52
139
  }
53
140
  if (hubAction === 'status') {
54
141
  return print(await telegramHubServiceStatus({ projectRoot: controllingRoot, globalRoot: root }), json);
@@ -59,7 +146,7 @@ export async function telegramCommand(args = []) {
59
146
  }
60
147
  }
61
148
  catch (err) {
62
- process.exitCode = 1;
149
+ process.exitCode = action === 'hub' && args.includes('--service-managed') ? 0 : 1;
63
150
  return print({
64
151
  schema: 'sks.telegram-command-error.v1',
65
152
  ok: false,
@@ -68,8 +155,13 @@ export async function telegramCommand(args = []) {
68
155
  }
69
156
  return fail('unknown_action', [
70
157
  'status',
158
+ 'center-snapshot [--session-id <id>] [--cursor <opaque>] [--sort-direction asc|desc] --project-root <path>',
159
+ 'center-live [--session-id <id>] --project-root <path>',
71
160
  'setup --bot-token-stdin --project-root <path>',
72
161
  'validate-config',
162
+ 'dead-letters list|inspect --project-root <path>',
163
+ 'dead-letters requeue --update-id <id> --yes --project-root <path>',
164
+ 'dead-letters acknowledge [--update-id <id>] --yes --project-root <path>',
73
165
  'hub run|start|stop|restart|status'
74
166
  ], json);
75
167
  }
@@ -79,6 +171,7 @@ async function telegramStatus(args, root, paths, configPath) {
79
171
  const owner = await readJson(paths.owner, null);
80
172
  const topics = await new TelegramTopicRegistry(paths.topics).list();
81
173
  const controllingRoot = path.resolve(readOption(args, '--project-root') ?? await projectRoot());
174
+ const servicePromise = telegramHubServiceStatus({ projectRoot: controllingRoot, globalRoot: root });
82
175
  const machineRegistryRaw = await readJson(readOption(args, '--machines') ?? remoteMachineRegistryPath(root), null);
83
176
  const machineValidation = validateRemoteMachineRegistry(machineRegistryRaw);
84
177
  const sessionIndexRaw = await readJson(readOption(args, '--session-index') ?? remoteSessionIndexPath(controllingRoot), null);
@@ -90,29 +183,63 @@ async function telegramStatus(args, root, paths, configPath) {
90
183
  const registered = bindings.filter((binding) => targets.some((target) => (binding.machine_id === target.machine_id
91
184
  && binding.project_id === target.project_id
92
185
  && path.resolve(binding.project_root) === path.resolve(target.project_root))));
93
- const service = await telegramHubServiceStatus({ projectRoot: controllingRoot, globalRoot: root });
94
186
  let tokenConfigured = false;
95
- if (validation.config) {
96
- tokenConfigured = await resolveTelegramBotToken(validation.config.bot_token_ref).then(() => true).catch(() => false);
187
+ let liveProbe = telegramLiveProbeStatus(null);
188
+ const probeConfig = validation.config;
189
+ if (probeConfig) {
190
+ const token = await resolveTelegramBotToken(probeConfig.bot_token_ref, {
191
+ timeoutMs: TELEGRAM_STATUS_TOKEN_LOOKUP_TIMEOUT_MS
192
+ }).catch(() => null);
193
+ tokenConfigured = token !== null;
194
+ if (token) {
195
+ try {
196
+ liveProbe = telegramLiveProbeStatus(await probeTelegramBotReadiness(new TelegramBotApiClient(token, {
197
+ timeoutMs: TELEGRAM_STATUS_BOT_REQUEST_TIMEOUT_MS,
198
+ maxRetries: TELEGRAM_STATUS_BOT_MAX_RETRIES
199
+ })));
200
+ }
201
+ catch (error) {
202
+ liveProbe = telegramLiveProbeStatus(null, error);
203
+ }
204
+ }
97
205
  }
206
+ const service = await servicePromise;
98
207
  const pairing = telegramPairingReadiness(rawConfig);
99
208
  const blockers = [
100
- ...validation.issues.map((issue) => `config:${issue}`),
209
+ ...validation.issues.map((issue) => (issue === 'telegram_identity_migration_required'
210
+ ? issue
211
+ : `config:${issue}`)),
101
212
  ...machineValidation.issues.map((issue) => `machine:${issue}`),
102
213
  ...sessionValidation.issues.map((issue) => `target:${issue}`),
103
214
  ...(tokenConfigured ? [] : ['telegram_token_not_available']),
215
+ ...(liveProbe.blocker ? [liveProbe.blocker] : []),
104
216
  ...(pairing.blocker ? [pairing.blocker] : []),
105
217
  ...(registered.length ? [] : ['no_registered_codex_session']),
106
- ...(service.running ? [] : ['telegram_hub_not_running'])
218
+ ...(service.running
219
+ ? service.dead_letter_ingress > 0
220
+ ? ['telegram_ingress_dead_letter_present']
221
+ : service.receive_ready ? [] : ['telegram_receive_not_ready']
222
+ : ['telegram_hub_not_running'])
107
223
  ];
108
224
  return {
109
225
  schema: 'sks.telegram-status.v1',
110
226
  ok: blockers.length === 0,
111
227
  configured: rawConfig !== null,
112
228
  token_configured: tokenConfigured,
229
+ bot_verified: liveProbe.bot_verified,
230
+ bot: liveProbe.bot,
231
+ webhook_configured: liveProbe.webhook_configured,
232
+ pending_update_count: liveProbe.pending_update_count,
233
+ long_poll_ready: liveProbe.long_poll_ready,
234
+ telegram_probe_error: liveProbe.telegram_probe_error,
113
235
  pairing_valid: pairing.pairing_valid,
114
236
  pairing_issues: pairing.pairing_issues,
115
237
  hub_running: service.running,
238
+ hub_ready: service.receive_ready,
239
+ receive_state: service.receive_state,
240
+ last_successful_poll_at: service.last_successful_poll_at,
241
+ pending_ingress: service.pending_ingress,
242
+ dead_letter_ingress: service.dead_letter_ingress,
116
243
  service,
117
244
  config_issues: validation.issues,
118
245
  owner: owner ? {
@@ -135,7 +262,8 @@ async function telegramStatus(args, root, paths, configPath) {
135
262
  updated_at: binding.updated_at
136
263
  })),
137
264
  remote_config_issues: [...machineValidation.issues, ...sessionValidation.issues],
138
- blockers
265
+ blockers,
266
+ next_actions: telegramNextActions(blockers)
139
267
  };
140
268
  }
141
269
  function printTelegramUsage() {
@@ -143,49 +271,94 @@ function printTelegramUsage() {
143
271
 
144
272
  Usage:
145
273
  sks telegram status [--project-root <path>] [--json]
274
+ sks telegram center-snapshot --project-root <path> [--session-id <id>]
275
+ [--cursor <opaque>] [--sort-direction asc|desc] [--json]
276
+ sks telegram center-live --project-root <path> [--session-id <id>] [--json]
146
277
  sks telegram setup --bot-token-stdin --project-root <path> [--paired-chat-id <id>]
147
278
  [--paired-user-id <id>] [--new-session] [--json]
148
279
  sks telegram validate-config [--config <path>] [--json]
280
+ sks telegram dead-letters list --project-root <path> [--json]
281
+ sks telegram dead-letters inspect --update-id <id> --project-root <path> [--json]
282
+ sks telegram dead-letters requeue --update-id <id> --yes --project-root <path> [--json]
283
+ sks telegram dead-letters acknowledge [--update-id <id>] --yes --project-root <path> [--json]
149
284
  sks telegram hub start|stop|restart|status [--project-root <path>] [--json]
150
285
  sks telegram hub run [--project-root <path>] [--json]
151
286
 
152
287
  Pairing (do these first):
153
- 1. Create a private bot with @BotFather in Telegram and copy its token.
288
+ 1. Open @BotFather, send /newbot, choose a name and a unique username ending in "bot",
289
+ then copy the HTTP API token BotFather returns.
154
290
  2. Send /start to that bot from the Telegram account you want to pair.
155
291
  3. printf '%s' "<token>" | sks telegram setup --bot-token-stdin --project-root "$PWD" --json
156
292
  The token is read from stdin only and stored in the macOS Keychain.
157
293
  4. sks telegram hub start --project-root "$PWD" --json
158
294
 
295
+ If status reports telegram_webhook_conflict, remove the webhook in the service
296
+ that configured it, then retry. SKS does not delete external webhook state implicitly.
297
+ If setup or the hub reports telegram_409_conflict, stop the other poller using
298
+ this bot token before retrying.
299
+ Stop the SKS Telegram hub before rerunning setup or rotating its BotFather token.
300
+ If status reports telegram_ingress_dead_letter_present, inspect the bounded list,
301
+ stop the Hub, then explicitly requeue only entries marked requeue_safe or acknowledge
302
+ an ambiguous entry and resend it from the paired private chat. Requeue never executes
303
+ until the Hub is separately restarted, and ambiguous effects are never replayed.
304
+
159
305
  The same flow is available in SKS Center → Remote & Telegram.
306
+ Center uses center-snapshot for stored conversation pages and center-live for the
307
+ bounded local event journal. Center checks the journal files every 2.5 seconds and
308
+ runs center-live only after a change; it never starts Codex App Server for that path.
309
+ The default page is Latest. Use Older, Newer, and Latest in Center, or pass a
310
+ returned cursor with its paired sort direction, to browse every stored turn
311
+ without resuming the Codex thread.
312
+ Reasoning text, raw tool output, and credentials are excluded from that projection.
160
313
  See docs/telegram-and-center.md for the full guide and troubleshooting.
161
314
  `);
162
315
  }
163
316
  export function telegramPairingReadiness(value) {
164
317
  const pairing = validateTelegramPrivatePairing(value);
318
+ const multipleIds = hasMultipleTelegramPairingIds(value);
165
319
  return {
166
320
  pairing_valid: pairing.ok,
167
321
  pairing_issues: pairing.issues,
168
322
  blocker: pairing.ok
169
323
  ? null
170
- : pairing.missing
171
- ? 'telegram_pairing_missing'
172
- : `telegram_pairing_invalid:${pairing.issues.join(',')}`
324
+ : multipleIds
325
+ ? 'telegram_pairing_multiple_ids_requires_setup'
326
+ : pairing.missing
327
+ ? 'telegram_pairing_missing'
328
+ : `telegram_pairing_invalid:${pairing.issues.join(',')}`
329
+ };
330
+ }
331
+ export function telegramLiveProbeStatus(readiness, error = null) {
332
+ const probeError = error === null ? null : telegramProbeFailure(error);
333
+ const webhookConfigured = readiness?.webhook_configured === true;
334
+ return {
335
+ bot_verified: readiness !== null,
336
+ bot: readiness?.bot ?? null,
337
+ webhook_configured: webhookConfigured,
338
+ pending_update_count: readiness?.pending_update_count ?? 0,
339
+ long_poll_ready: readiness !== null && !webhookConfigured,
340
+ telegram_probe_error: probeError,
341
+ blocker: probeError ?? (webhookConfigured ? 'telegram_webhook_conflict' : null)
173
342
  };
174
343
  }
175
344
  async function runHub(args, root, paths, configPath, json) {
345
+ const serviceManaged = args.includes('--service-managed');
346
+ const centerSupervisorPid = telegramCenterSupervisorPid(args);
176
347
  const config = await loadTelegramConfig(configPath);
177
348
  const controllingRoot = path.resolve(readOption(args, '--project-root') ?? await projectRoot());
178
349
  const machineRegistry = await loadRemoteMachineRegistry(readOption(args, '--machines') ?? remoteMachineRegistryPath(root));
179
350
  const sessionIndex = await loadRemoteSessionIndex(readOption(args, '--session-index') ?? remoteSessionIndexPath(controllingRoot), machineRegistry);
180
351
  const token = await resolveTelegramBotToken(config.bot_token_ref);
181
352
  const fingerprint = telegramTokenFingerprint(token);
353
+ const identityFingerprint = telegramRuntimeIdentityFingerprint(config, fingerprint);
182
354
  const owner = new TelegramOwnerLock({
183
355
  lockPath: paths.owner,
184
356
  tokenFingerprint: fingerprint,
357
+ reclaimDeadImmediately: centerSupervisorPid !== null,
185
358
  ...(config.owner_stale_ms === undefined ? {} : { staleMs: config.owner_stale_ms })
186
359
  });
187
360
  await owner.acquire();
188
- const topics = new TelegramTopicRegistry(paths.topics);
361
+ const topics = new TelegramTopicRegistry(paths.topics, { identityFingerprint });
189
362
  const actions = new TelegramActionBroker(paths.actions);
190
363
  const audit = new TelegramAuditLedger(paths.audit, fingerprint);
191
364
  const router = new TelegramHubRouter({
@@ -196,6 +369,10 @@ async function runHub(args, root, paths, configPath, json) {
196
369
  audit
197
370
  });
198
371
  const api = new TelegramBotApiClient(token, { timeoutMs: (config.long_poll_timeout_sec ?? 25) * 1000 + 5_000 });
372
+ const ingress = new TelegramIngressSpool(paths.ingress, {
373
+ identityFingerprint,
374
+ legacyTokenFingerprint: fingerprint
375
+ });
199
376
  const runtime = new TelegramHubRuntime({
200
377
  config,
201
378
  router,
@@ -213,37 +390,102 @@ async function runHub(args, root, paths, configPath, json) {
213
390
  }),
214
391
  machineRegistry,
215
392
  sessionIndex,
216
- projectionStatePath: paths.projection
393
+ projectionStatePath: paths.projection,
394
+ ingress
395
+ });
396
+ const polling = new TelegramPollingHub(api, runtime, owner, config.long_poll_timeout_sec ?? 25, {
397
+ ingress,
398
+ healthPath: paths.receive_health
217
399
  });
218
- const polling = new TelegramPollingHub(api, runtime, owner, config.long_poll_timeout_sec ?? 25);
400
+ const controller = new AbortController();
401
+ const stop = () => controller.abort();
402
+ const supervisorMonitor = centerSupervisorPid === null
403
+ ? null
404
+ : setInterval(() => {
405
+ if (!telegramCenterSupervisorAlive(centerSupervisorPid))
406
+ controller.abort();
407
+ }, 250);
408
+ supervisorMonitor?.unref();
409
+ process.once('SIGINT', stop);
410
+ process.once('SIGTERM', stop);
219
411
  try {
220
412
  await polling.ensureLongPollingAllowed();
221
- const sync = await runtime.initialize();
222
- if (args.includes('--once')) {
223
- const result = await polling.pollOnce();
224
- return print({ schema: 'sks.telegram-hub-run.v1', ...result, sync }, json);
413
+ const receive = await polling.establishReadiness(controller.signal);
414
+ if (receive.stopped_reason === 'aborted') {
415
+ return print({
416
+ schema: 'sks.telegram-hub-run.v1',
417
+ ok: true,
418
+ processed: 0,
419
+ stopped_reason: 'aborted',
420
+ receive,
421
+ sync: null
422
+ }, json);
225
423
  }
226
- const controller = new AbortController();
227
- const stop = () => controller.abort();
228
- process.once('SIGINT', stop);
229
- process.once('SIGTERM', stop);
230
- try {
231
- const result = await polling.run(controller.signal);
232
- return print({ schema: 'sks.telegram-hub-run.v1', ...result, sync }, json);
424
+ if (!receive.ok) {
425
+ return print(markTelegramHubMutationResult({
426
+ schema: 'sks.telegram-hub-run.v1',
427
+ ok: false,
428
+ processed: 0,
429
+ stopped_reason: receive.stopped_reason ?? 'receive_not_ready',
430
+ receive,
431
+ sync: null
432
+ }, { setExitCode: !serviceManaged }), json);
233
433
  }
234
- finally {
235
- process.off('SIGINT', stop);
236
- process.off('SIGTERM', stop);
434
+ const sync = await runtime.initialize();
435
+ if (controller.signal.aborted) {
436
+ return print({
437
+ schema: 'sks.telegram-hub-run.v1',
438
+ ok: true,
439
+ processed: receive.received,
440
+ stopped_reason: 'aborted',
441
+ receive,
442
+ sync
443
+ }, json);
237
444
  }
445
+ await polling.drainPending();
446
+ if (args.includes('--once')) {
447
+ await polling.waitForIdle();
448
+ return print({
449
+ schema: 'sks.telegram-hub-run.v1',
450
+ ok: true,
451
+ processed: receive.received,
452
+ stopped_reason: null,
453
+ receive,
454
+ sync
455
+ }, json);
456
+ }
457
+ const result = await polling.run(controller.signal);
458
+ return print(markTelegramHubMutationResult({
459
+ schema: 'sks.telegram-hub-run.v1',
460
+ ...result,
461
+ receive,
462
+ sync
463
+ }, { setExitCode: !serviceManaged }), json);
238
464
  }
239
465
  finally {
466
+ if (supervisorMonitor)
467
+ clearInterval(supervisorMonitor);
468
+ process.off('SIGINT', stop);
469
+ process.off('SIGTERM', stop);
240
470
  await runtime.close();
241
471
  await owner.release();
242
472
  }
243
473
  }
244
474
  async function assertHubSetupReady(controllingRoot, globalRoot, configPath) {
245
475
  const config = await loadTelegramConfig(configPath);
246
- await resolveTelegramBotToken(config.bot_token_ref);
476
+ const token = await resolveTelegramBotToken(config.bot_token_ref);
477
+ let readiness;
478
+ try {
479
+ readiness = await probeTelegramBotReadiness(new TelegramBotApiClient(token, {
480
+ timeoutMs: 8_000,
481
+ maxRetries: 1
482
+ }));
483
+ }
484
+ catch (error) {
485
+ throw new Error(telegramProbeFailure(error));
486
+ }
487
+ if (readiness.webhook_configured)
488
+ throw new Error('telegram_webhook_conflict');
247
489
  const registry = await loadRemoteMachineRegistry(remoteMachineRegistryPath(globalRoot));
248
490
  const index = await loadRemoteSessionIndex(remoteSessionIndexPath(controllingRoot), registry);
249
491
  const bindings = await new RemoteCodexSessionBindingStore(remoteCodexSessionBindingsPath(controllingRoot)).list();
@@ -253,10 +495,82 @@ async function assertHubSetupReady(controllingRoot, globalRoot, configPath) {
253
495
  throw new Error('no_registered_codex_session');
254
496
  }
255
497
  }
498
+ export function markTelegramHubMutationResult(result, options = {}) {
499
+ if (result.ok !== true && options.setExitCode !== false)
500
+ process.exitCode = 1;
501
+ return result;
502
+ }
503
+ export function telegramCenterSupervisorPid(args, actualParentPid = process.ppid) {
504
+ const supervisedCount = args.filter((value) => value === '--center-supervised').length;
505
+ const pidOptionCount = args.filter((value) => value === '--center-supervisor-pid').length;
506
+ if (supervisedCount === 0 && pidOptionCount === 0)
507
+ return null;
508
+ if (supervisedCount !== 1
509
+ || pidOptionCount !== 1
510
+ || args.includes('--service-managed')) {
511
+ throw new Error('telegram_center_supervisor_invalid');
512
+ }
513
+ const rawPid = readOption(args, '--center-supervisor-pid');
514
+ if (!rawPid || !/^[1-9]\d*$/.test(rawPid))
515
+ throw new Error('telegram_center_supervisor_invalid');
516
+ const pid = Number(rawPid);
517
+ if (!Number.isSafeInteger(pid) || pid !== actualParentPid) {
518
+ throw new Error('telegram_center_supervisor_invalid');
519
+ }
520
+ return pid;
521
+ }
522
+ export function telegramCenterSupervisorAlive(pid, probe = (candidate) => process.kill(candidate, 0)) {
523
+ try {
524
+ probe(pid);
525
+ return true;
526
+ }
527
+ catch (error) {
528
+ return error?.code !== 'ESRCH';
529
+ }
530
+ }
256
531
  function readOption(args, name) {
257
532
  const index = args.indexOf(name);
258
533
  return index >= 0 && args[index + 1] ? String(args[index + 1]) : null;
259
534
  }
535
+ export function telegramDeadLetterCommandRequest(args) {
536
+ const rawAction = args[1] && !String(args[1]).startsWith('-')
537
+ ? String(args[1])
538
+ : 'list';
539
+ const action = rawAction === 'status' ? 'list' : rawAction;
540
+ if (!['list', 'inspect', 'requeue', 'acknowledge'].includes(action)) {
541
+ throw new Error('unknown_dead_letter_action');
542
+ }
543
+ const updateId = readTelegramUpdateId(args);
544
+ if ((action === 'inspect' || action === 'requeue') && updateId === null) {
545
+ throw new Error('telegram_dead_letter_update_id_required');
546
+ }
547
+ return {
548
+ action: action,
549
+ updateId,
550
+ confirmed: args.includes('--yes')
551
+ };
552
+ }
553
+ function readTelegramUpdateId(args) {
554
+ const optionIndexes = args.flatMap((value, index) => value === '--update-id' ? [index] : []);
555
+ if (optionIndexes.length === 0)
556
+ return null;
557
+ if (optionIndexes.length !== 1)
558
+ throw new Error('telegram_dead_letter_update_id_invalid');
559
+ const raw = args[optionIndexes[0] + 1];
560
+ if (!raw || !/^\d+$/.test(raw))
561
+ throw new Error('telegram_dead_letter_update_id_invalid');
562
+ const updateId = Number(raw);
563
+ if (!Number.isSafeInteger(updateId))
564
+ throw new Error('telegram_dead_letter_update_id_invalid');
565
+ return updateId;
566
+ }
567
+ function telegramCenterSortDirection(value) {
568
+ if (value === null)
569
+ return null;
570
+ if (value === 'asc' || value === 'desc')
571
+ return value;
572
+ throw new Error('telegram_center_sort_direction_invalid');
573
+ }
260
574
  function redactConfig(config) {
261
575
  const record = config;
262
576
  return {
@@ -266,6 +580,17 @@ function redactConfig(config) {
266
580
  : null
267
581
  };
268
582
  }
583
+ async function updateTelegramIngressHealth(healthPath, counts) {
584
+ const receiveHealth = await readJson(healthPath, null);
585
+ if (receiveHealth?.schema !== 'sks.telegram-receive-health.v1')
586
+ return;
587
+ await writeTextAtomic(healthPath, `${JSON.stringify({
588
+ ...receiveHealth,
589
+ updated_at: nowIso(),
590
+ pending_ingress: counts.pending,
591
+ dead_letter_ingress: counts.dead_letters
592
+ }, null, 2)}\n`, { mode: 0o600 });
593
+ }
269
594
  function print(value, json) {
270
595
  if (json)
271
596
  console.log(JSON.stringify(value, null, 2));
@@ -284,3 +609,46 @@ function publicError(err) {
284
609
  .replace(/(?:\/Users|\/home)\/[^\s]+/g, '[path-redacted]')
285
610
  .slice(0, 500);
286
611
  }
612
+ function telegramProbeFailure(error) {
613
+ return error instanceof TelegramBotApiError && (error.errorCode === 401 || error.errorCode === 404)
614
+ ? 'telegram_bot_auth_failed'
615
+ : 'telegram_bot_probe_failed';
616
+ }
617
+ export function telegramNextActions(blockers) {
618
+ const actions = [];
619
+ if (blockers.includes('telegram_identity_migration_required')) {
620
+ actions.push('Stop the Hub, then reconnect this project once so SKS writes the v2 bot/pairing identity contract.');
621
+ }
622
+ if (blockers.includes('telegram_token_not_available') || blockers.includes('telegram_bot_auth_failed')) {
623
+ actions.push('Create or regenerate the bot token in @BotFather, then rerun setup with --bot-token-stdin.');
624
+ }
625
+ if (blockers.includes('telegram_webhook_conflict')) {
626
+ actions.push('Remove the existing webhook in the service that configured it, then rerun telegram status.');
627
+ }
628
+ if (blockers.includes('telegram_bot_probe_failed')) {
629
+ actions.push('Check network access to api.telegram.org, then rerun telegram status.');
630
+ }
631
+ if (blockers.includes('telegram_ingress_dead_letter_present')) {
632
+ actions.push('Inspect dead letters, stop the Hub, then explicitly requeue only safe entries or acknowledge an ambiguous entry before resending it.');
633
+ }
634
+ if (blockers.includes('telegram_pairing_missing') || blockers.some((blocker) => blocker.startsWith('telegram_pairing_invalid:'))) {
635
+ actions.push('Send /start to the bot in a private chat, then rerun telegram setup.');
636
+ }
637
+ if (blockers.includes('telegram_pairing_multiple_ids_requires_setup')) {
638
+ actions.push('Stop the hub, then rerun telegram setup to replace the ambiguous multi-ID config with one verified private chat and user.');
639
+ }
640
+ if (blockers.includes('no_registered_codex_session')) {
641
+ actions.push('Rerun telegram setup for this project root.');
642
+ }
643
+ if (blockers.includes('telegram_hub_not_running')) {
644
+ actions.push('Run sks telegram hub start --project-root \"$PWD\" --json after other blockers are cleared.');
645
+ }
646
+ return actions;
647
+ }
648
+ function hasMultipleTelegramPairingIds(value) {
649
+ if (!value || typeof value !== 'object')
650
+ return false;
651
+ const record = value;
652
+ return (Array.isArray(record.paired_chat_ids) && record.paired_chat_ids.length > 1)
653
+ || (Array.isArray(record.paired_user_ids) && record.paired_user_ids.length > 1);
654
+ }
@@ -5,11 +5,21 @@ import { computeTriWikiAffectedGraph } from '../triwiki/triwiki-affected-graph.j
5
5
  import { buildTriWikiGateImpactMap } from '../triwiki/triwiki-gate-impact-map.js';
6
6
  import { DEFAULT_TRIWIKI_MODULE_CARDS } from '../triwiki/triwiki-module-card.js';
7
7
  import { summarizeTriWikiProofBank } from '../triwiki/triwiki-proof-bank.js';
8
+ import { isTriWikiGraphSubcommand, triwikiGraphCommand } from './triwiki-graph-command.js';
8
9
  export async function triwikiCommand(args = []) {
9
10
  const root = await projectRoot();
10
11
  const sub = args[0] && !args[0].startsWith('-') ? args[0] : 'index';
11
12
  const json = flag(args, '--json');
12
13
  let result;
14
+ if (isTriWikiGraphSubcommand(sub)) {
15
+ const answer = await triwikiGraphCommand(root, sub, args.slice(1));
16
+ process.exitCode = answer.ok ? 0 : 2;
17
+ if (json)
18
+ return printJson(answer.result);
19
+ for (const line of answer.lines)
20
+ console.log(line);
21
+ return answer.result;
22
+ }
13
23
  if (sub === 'index') {
14
24
  result = {
15
25
  schema: 'sks.triwiki-index.v1',
@@ -26,7 +36,7 @@ export async function triwikiCommand(args = []) {
26
36
  result = summarizeTriWikiProofBank(root);
27
37
  }
28
38
  else {
29
- console.error('Usage: sks triwiki index|affected|proof-bank [--json]');
39
+ console.error('Usage: sks triwiki index|affected|proof-bank|graph-status|graph-lint|graph-query [--json]');
30
40
  process.exitCode = 1;
31
41
  return null;
32
42
  }