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
@@ -5,12 +5,13 @@ import { appendJsonlBounded, ensureDir, nowIso, readJson, sha256, writeJsonAtomi
5
5
  import { withFileLock } from '../locks/file-lock.js';
6
6
  export class TelegramTopicRegistry {
7
7
  file;
8
- constructor(file) {
8
+ options;
9
+ constructor(file, options = {}) {
9
10
  this.file = file;
11
+ this.options = options;
10
12
  }
11
13
  async list() {
12
- const registry = await readJson(this.file, { schema: 'sks.telegram-topic-registry.v1', routes: [] });
13
- return registry.schema === 'sks.telegram-topic-registry.v1' && Array.isArray(registry.routes) ? registry.routes : [];
14
+ return (await this.readRegistry()).routes;
14
15
  }
15
16
  async findBySession(machineId, projectId, sessionId) {
16
17
  return (await this.list()).find((route) => route.machine_id === machineId && route.project_id === projectId && route.session_id === sessionId) ?? null;
@@ -20,7 +21,8 @@ export class TelegramTopicRegistry {
20
21
  }
21
22
  async upsert(input) {
22
23
  return withFileLock({ lockPath: `${this.file}.lock`, timeoutMs: 5_000, staleMs: 30_000 }, async () => {
23
- const routes = await this.list();
24
+ const registry = await this.readRegistry();
25
+ const routes = [...registry.routes];
24
26
  const sessionIndex = routes.findIndex((route) => route.machine_id === input.machine_id && route.project_id === input.project_id && route.session_id === input.session_id);
25
27
  const collision = input.message_thread_id > 0
26
28
  ? routes.find((route, index) => index !== sessionIndex && route.chat_id === input.chat_id && route.message_thread_id === input.message_thread_id)
@@ -29,6 +31,10 @@ export class TelegramTopicRegistry {
29
31
  throw new Error('telegram_topic_collision');
30
32
  const current = sessionIndex >= 0 ? routes[sessionIndex] : undefined;
31
33
  const at = nowIso();
34
+ const routeMoved = Boolean(current
35
+ && (current.chat_id !== input.chat_id
36
+ || current.message_thread_id !== input.message_thread_id));
37
+ const generationBase = Math.max(registry.generation_epoch ?? 0, ...routes.map((route) => route.generation));
32
38
  const next = {
33
39
  schema: 'sks.telegram-topic-route.v1',
34
40
  machine_id: input.machine_id,
@@ -39,16 +45,60 @@ export class TelegramTopicRegistry {
39
45
  pinned_message_id: input.pinned_message_id,
40
46
  created_at: current?.created_at ?? at,
41
47
  updated_at: at,
42
- generation: current ? current.generation + (input.recreate === true ? 1 : 0) : 1
48
+ generation: current && !routeMoved && input.recreate !== true
49
+ ? current.generation
50
+ : generationBase + 1
43
51
  };
44
52
  if (sessionIndex >= 0)
45
53
  routes[sessionIndex] = next;
46
54
  else
47
55
  routes.push(next);
48
- await writeJsonAtomic(this.file, { schema: 'sks.telegram-topic-registry.v1', routes });
56
+ await writeJsonAtomic(this.file, {
57
+ schema: 'sks.telegram-topic-registry.v1',
58
+ ...(this.options.identityFingerprint
59
+ ? { identity_fingerprint: this.options.identityFingerprint }
60
+ : {}),
61
+ generation_epoch: Math.max(generationBase, next.generation),
62
+ routes
63
+ });
49
64
  return next;
50
65
  });
51
66
  }
67
+ async invalidateAll() {
68
+ return withFileLock({ lockPath: `${this.file}.lock`, timeoutMs: 5_000, staleMs: 30_000 }, async () => {
69
+ const raw = await this.readRawRegistry();
70
+ const generationEpoch = Math.max(raw.generation_epoch ?? 0, ...raw.routes.map((route) => route.generation));
71
+ await writeJsonAtomic(this.file, {
72
+ schema: 'sks.telegram-topic-registry.v1',
73
+ ...(this.options.identityFingerprint
74
+ ? { identity_fingerprint: this.options.identityFingerprint }
75
+ : {}),
76
+ generation_epoch: generationEpoch,
77
+ routes: []
78
+ });
79
+ return generationEpoch;
80
+ });
81
+ }
82
+ async readRegistry() {
83
+ const registry = await this.readRawRegistry();
84
+ const expectedIdentity = this.options.identityFingerprint;
85
+ if (!expectedIdentity || registry.identity_fingerprint === expectedIdentity)
86
+ return registry;
87
+ return {
88
+ schema: 'sks.telegram-topic-registry.v1',
89
+ identity_fingerprint: expectedIdentity,
90
+ generation_epoch: Math.max(registry.generation_epoch ?? 0, ...registry.routes.map((route) => route.generation)),
91
+ routes: []
92
+ };
93
+ }
94
+ async readRawRegistry() {
95
+ const registry = await readJson(this.file, { schema: 'sks.telegram-topic-registry.v1', routes: [] });
96
+ if (registry.schema !== 'sks.telegram-topic-registry.v1'
97
+ || !Array.isArray(registry.routes)) {
98
+ throw new Error('telegram_topic_registry_schema_invalid');
99
+ }
100
+ return registry;
101
+ }
52
102
  }
53
103
  export function buildTelegramTopicName(input, existingNames = [], maxLength = 128) {
54
104
  const normalized = [input.machine, `${input.repo}/${input.branch}`, input.title]
@@ -80,33 +130,86 @@ export class TelegramIdempotencyLedger {
80
130
  }
81
131
  export class TelegramActionBroker {
82
132
  file;
83
- constructor(file) {
133
+ maxEntries;
134
+ now;
135
+ constructor(file, options = {}) {
84
136
  this.file = file;
137
+ this.maxEntries = Math.max(64, Math.min(4096, options.maxEntries ?? 512));
138
+ this.now = options.now ?? Date.now;
85
139
  }
86
140
  async create(action, route) {
87
- if (action.risk === 'R3')
141
+ return (await this.createMany([{ action, route }]))[0];
142
+ }
143
+ async createMany(entries) {
144
+ if (entries.some((entry) => entry.action.risk === 'R3'))
88
145
  throw new Error('telegram_r3_always_denied');
89
146
  return this.mutate((ledger) => {
90
- if (ledger.actions.some((candidate) => candidate.action_id === action.action_id))
91
- throw new Error('telegram_action_id_exists');
92
- const alias = createAlias();
93
- const callbackData = `cb:${alias}`;
94
- if (Buffer.byteLength(callbackData, 'utf8') < 1 || Buffer.byteLength(callbackData, 'utf8') > 64)
95
- throw new Error('telegram_callback_data_out_of_bounds');
96
- ledger.actions.push(action);
97
- ledger.aliases.push({
98
- alias,
99
- action_id: action.action_id,
100
- session_id: action.session_id,
101
- chat_id: route.chat_id,
102
- message_thread_id: route.message_thread_id,
103
- revision: action.revision,
104
- expires_at: action.expires_at,
105
- used_at: null
147
+ expireOpenActions(ledger, this.now());
148
+ const actionsById = new Map(ledger.actions.map((action) => [action.action_id, action]));
149
+ const aliasesByActionId = new Map(ledger.aliases.map((alias) => [alias.action_id, alias]));
150
+ const protectedActionIds = new Set();
151
+ let additions = 0;
152
+ for (const { action, route } of entries) {
153
+ if (protectedActionIds.has(action.action_id))
154
+ throw new Error('telegram_action_batch_duplicate');
155
+ protectedActionIds.add(action.action_id);
156
+ const existing = actionsById.get(action.action_id);
157
+ if (!existing) {
158
+ additions += 1;
159
+ continue;
160
+ }
161
+ const existingAlias = aliasesByActionId.get(action.action_id);
162
+ if (!existingAlias
163
+ || existing.status !== 'open'
164
+ || existing.route_generation !== action.route_generation
165
+ || existingAlias.chat_id !== route.chat_id
166
+ || existingAlias.message_thread_id !== route.message_thread_id) {
167
+ throw new Error('telegram_action_id_exists');
168
+ }
169
+ }
170
+ reserveActionLedgerCapacity(ledger, this.maxEntries, additions, protectedActionIds);
171
+ return entries.map(({ action, route }) => {
172
+ const existing = actionsById.get(action.action_id);
173
+ if (existing) {
174
+ const existingAlias = aliasesByActionId.get(action.action_id);
175
+ return { action: existing, callback_data: `cb:${existingAlias.alias}` };
176
+ }
177
+ const alias = createAlias();
178
+ const callbackData = `cb:${alias}`;
179
+ if (Buffer.byteLength(callbackData, 'utf8') < 1 || Buffer.byteLength(callbackData, 'utf8') > 64)
180
+ throw new Error('telegram_callback_data_out_of_bounds');
181
+ ledger.actions.push(action);
182
+ const aliasEntry = {
183
+ alias,
184
+ action_id: action.action_id,
185
+ session_id: action.session_id,
186
+ chat_id: route.chat_id,
187
+ message_thread_id: route.message_thread_id,
188
+ route_generation: action.route_generation,
189
+ revision: action.revision,
190
+ expires_at: action.expires_at,
191
+ used_at: null,
192
+ claimed_update_id: null
193
+ };
194
+ ledger.aliases.push(aliasEntry);
195
+ actionsById.set(action.action_id, action);
196
+ aliasesByActionId.set(action.action_id, aliasEntry);
197
+ return { action, callback_data: callbackData };
106
198
  });
107
- return { action, callback_data: callbackData };
108
199
  });
109
200
  }
201
+ async inspect(callbackData) {
202
+ const ledger = await readJson(this.file, { schema: 'sks.telegram-action-ledger.v1', actions: [], aliases: [] });
203
+ if (ledger.schema !== 'sks.telegram-action-ledger.v1') {
204
+ return { alias: null, action: null };
205
+ }
206
+ const aliasValue = callbackData.startsWith('cb:') ? callbackData.slice(3) : '';
207
+ const alias = ledger.aliases.find((entry) => entry.alias === aliasValue) ?? null;
208
+ const action = alias
209
+ ? ledger.actions.find((entry) => entry.action_id === alias.action_id) ?? null
210
+ : null;
211
+ return { alias, action };
212
+ }
110
213
  async resolve(input) {
111
214
  return this.mutate((ledger) => {
112
215
  const aliasValue = input.callback_data.startsWith('cb:') ? input.callback_data.slice(3) : '';
@@ -116,19 +219,30 @@ export class TelegramActionBroker {
116
219
  const action = ledger.actions.find((entry) => entry.action_id === alias.action_id) ?? null;
117
220
  if (!action)
118
221
  return { ok: false, status: 'unknown_action', action: null };
222
+ if (alias.claimed_update_id === input.update_id
223
+ && alias.used_at
224
+ && action.status === 'claimed') {
225
+ return { ok: true, status: 'claimed', action };
226
+ }
119
227
  if (alias.used_at || ['claimed', 'resolved', 'cancelled'].includes(action.status))
120
228
  return { ok: false, status: 'already_resolved', action };
121
- const now = input.now ?? Date.now();
122
- if (Date.parse(alias.expires_at) <= now || Date.parse(action.expires_at) <= now) {
229
+ const now = input.now ?? this.now();
230
+ if (hasExpired(alias.expires_at, now) || hasExpired(action.expires_at, now)) {
123
231
  action.status = 'expired';
124
232
  return { ok: false, status: 'expired', action };
125
233
  }
126
234
  if (alias.chat_id !== input.chat_id || alias.message_thread_id !== input.message_thread_id)
127
235
  return { ok: false, status: 'wrong_topic', action };
236
+ if (input.route_generation === null
237
+ || alias.route_generation !== input.route_generation
238
+ || action.route_generation !== input.route_generation) {
239
+ return { ok: false, status: 'stale_generation', action };
240
+ }
128
241
  if (alias.revision !== input.revision || action.revision !== input.revision)
129
242
  return { ok: false, status: 'revision_mismatch', action };
130
243
  const usedAt = new Date(now).toISOString();
131
244
  alias.used_at = usedAt;
245
+ alias.claimed_update_id = input.update_id;
132
246
  action.status = 'claimed';
133
247
  return { ok: true, status: 'claimed', action };
134
248
  });
@@ -148,11 +262,52 @@ export class TelegramActionBroker {
148
262
  if (ledger.schema !== 'sks.telegram-action-ledger.v1')
149
263
  throw new Error('telegram_action_ledger_schema_invalid');
150
264
  const result = fn(ledger);
265
+ capActionLedger(ledger, this.maxEntries);
151
266
  await writeJsonAtomic(this.file, ledger);
152
267
  return result;
153
268
  });
154
269
  }
155
270
  }
271
+ function capActionLedger(ledger, maxEntries) {
272
+ if (ledger.actions.length <= maxEntries)
273
+ return;
274
+ pruneTerminalActionHistory(ledger, ledger.actions.length - maxEntries);
275
+ }
276
+ function reserveActionLedgerCapacity(ledger, maxEntries, additions, protectedActionIds) {
277
+ const removeCount = Math.max(0, ledger.actions.length + additions - maxEntries);
278
+ if (removeCount === 0)
279
+ return;
280
+ pruneTerminalActionHistory(ledger, removeCount, protectedActionIds);
281
+ }
282
+ function pruneTerminalActionHistory(ledger, removeCount, protectedActionIds = new Set()) {
283
+ const removable = ledger.actions
284
+ .filter((action) => isTerminalActionStatus(action.status) && !protectedActionIds.has(action.action_id))
285
+ .slice(0, removeCount);
286
+ if (removable.length < removeCount)
287
+ throw new Error('telegram_action_ledger_capacity_exceeded');
288
+ const removedIds = new Set(removable.map((action) => action.action_id));
289
+ ledger.actions = ledger.actions.filter((action) => !removedIds.has(action.action_id));
290
+ ledger.aliases = ledger.aliases.filter((alias) => !removedIds.has(alias.action_id));
291
+ }
292
+ function isTerminalActionStatus(status) {
293
+ return status === 'resolved' || status === 'cancelled' || status === 'expired';
294
+ }
295
+ function expireOpenActions(ledger, now) {
296
+ const aliasesByActionId = new Map(ledger.aliases.map((alias) => [alias.action_id, alias]));
297
+ for (const action of ledger.actions) {
298
+ if (action.status !== 'open')
299
+ continue;
300
+ const alias = aliasesByActionId.get(action.action_id);
301
+ if (hasExpired(action.expires_at, now)
302
+ || (alias && hasExpired(alias.expires_at, now))) {
303
+ action.status = 'expired';
304
+ }
305
+ }
306
+ }
307
+ function hasExpired(expiresAt, now) {
308
+ const expiresAtMs = Date.parse(expiresAt);
309
+ return !Number.isFinite(expiresAtMs) || expiresAtMs <= now;
310
+ }
156
311
  export class TelegramAuditLedger {
157
312
  file;
158
313
  salt;
@@ -167,11 +322,15 @@ export class TelegramAuditLedger {
167
322
  update_id: input.update_id,
168
323
  chat_id_hash: `sha256:${sha256(`${this.salt}:${input.chat_id}`)}`,
169
324
  topic_id: input.topic_id,
170
- action_alias: input.action_alias,
325
+ action_alias: input.action_alias ? `sha256:${sha256(`${this.salt}:${input.action_alias}`)}` : null,
171
326
  command_kind: sanitizeAuditField(input.command_kind),
172
327
  decision: input.decision,
173
328
  policy_reason: sanitizeAuditField(input.policy_reason),
174
- effect_receipt: input.effect_receipt ? sanitizeAuditField(input.effect_receipt) : null
329
+ effect_receipt: input.effect_receipt ? sanitizeAuditField(input.effect_receipt) : null,
330
+ telegram_delivery: input.telegram_delivery,
331
+ telegram_delivery_receipt: input.telegram_delivery_receipt
332
+ ? sanitizeAuditField(input.telegram_delivery_receipt)
333
+ : null
175
334
  };
176
335
  await appendJsonlBounded(this.file, event, 5 * 1024 * 1024);
177
336
  return event;
@@ -178,14 +178,34 @@ export class TelegramMessageProjector {
178
178
  text,
179
179
  reply_markup: replyMarkup
180
180
  }));
181
- return { method: 'editMessageText', message_id: input.route.pinned_message_id, pinned: true };
181
+ return {
182
+ method: 'editMessageText',
183
+ message_id: input.route.pinned_message_id,
184
+ pinned: true,
185
+ pin_delivery: 'not_attempted'
186
+ };
182
187
  }
183
188
  const sent = await this.sendFinal({ route: input.route, text, replyMarkup });
184
- await this.api.call('pinChatMessage', this.basePayload(input.route, {
185
- message_id: sent.message_id,
186
- disable_notification: true
187
- }));
188
- return { method: sent.method, message_id: sent.message_id, pinned: true };
189
+ try {
190
+ await this.api.call('pinChatMessage', this.basePayload(input.route, {
191
+ message_id: sent.message_id,
192
+ disable_notification: true
193
+ }));
194
+ return {
195
+ method: sent.method,
196
+ message_id: sent.message_id,
197
+ pinned: true,
198
+ pin_delivery: 'delivered'
199
+ };
200
+ }
201
+ catch (error) {
202
+ return {
203
+ method: sent.method,
204
+ message_id: sent.message_id,
205
+ pinned: false,
206
+ pin_delivery: error instanceof TelegramBotApiError ? error.delivery : 'unknown'
207
+ };
208
+ }
189
209
  }
190
210
  async requestInput(input) {
191
211
  const sent = await this.sendFinal({
@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
2
2
  import fsp from 'node:fs/promises';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import { errorCodeOf } from '../errors/message.js';
5
6
  import { ensureDir, nowIso } from '../fsx.js';
6
7
  export class TelegramOwnerConflictError extends Error {
7
8
  owner;
@@ -19,6 +20,7 @@ export class TelegramOwnerLock {
19
20
  processStartTime;
20
21
  now;
21
22
  isPidAlive;
23
+ reclaimDeadImmediately;
22
24
  owner = null;
23
25
  constructor(options) {
24
26
  this.options = options;
@@ -28,6 +30,7 @@ export class TelegramOwnerLock {
28
30
  this.processStartTime = options.processStartTime ?? new Date(Date.now() - process.uptime() * 1000).toISOString();
29
31
  this.now = options.now ?? Date.now;
30
32
  this.isPidAlive = options.isPidAlive ?? localPidAlive;
33
+ this.reclaimDeadImmediately = options.reclaimDeadImmediately === true;
31
34
  }
32
35
  async acquire() {
33
36
  await ensureDir(path.dirname(this.options.lockPath));
@@ -46,7 +49,7 @@ export class TelegramOwnerLock {
46
49
  return owner;
47
50
  }
48
51
  catch (error) {
49
- if (errorCode(error) !== 'EEXIST')
52
+ if (errorCodeOf(error) !== 'EEXIST')
50
53
  throw error;
51
54
  const existing = await this.readOwner();
52
55
  if (!this.isStale(existing))
@@ -57,7 +60,7 @@ export class TelegramOwnerLock {
57
60
  await fsp.unlink(quarantine).catch(() => undefined);
58
61
  }
59
62
  catch (renameError) {
60
- if (!['ENOENT', 'EEXIST'].includes(errorCode(renameError)))
63
+ if (!['ENOENT', 'EEXIST'].includes(errorCodeOf(renameError)))
61
64
  throw renameError;
62
65
  }
63
66
  }
@@ -108,7 +111,10 @@ export class TelegramOwnerLock {
108
111
  if (!owner)
109
112
  return true;
110
113
  const age = this.now() - Date.parse(owner.heartbeat_at);
111
- return Number.isFinite(age) && age > this.staleMs && !this.isPidAlive(owner.pid, owner.host);
114
+ const alive = this.isPidAlive(owner.pid, owner.host);
115
+ if (!alive && this.reclaimDeadImmediately)
116
+ return true;
117
+ return Number.isFinite(age) && age > this.staleMs && !alive;
112
118
  }
113
119
  }
114
120
  function sameOwner(left, right) {
@@ -126,9 +132,6 @@ function localPidAlive(pid, host) {
126
132
  return true;
127
133
  }
128
134
  catch (error) {
129
- return errorCode(error) !== 'ESRCH';
135
+ return errorCodeOf(error) !== 'ESRCH';
130
136
  }
131
137
  }
132
- function errorCode(error) {
133
- return error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
134
- }
@@ -3,13 +3,20 @@ import { buildTelegramTopicName } from './ledgers.js';
3
3
  import { publicSafeText } from './messages.js';
4
4
  import { asRecordOrNull as asRecord } from '../json/records.js';
5
5
  export { asRecord };
6
- export function pickerAction(route, now) {
6
+ export function pickerAction(route, now, updateId) {
7
7
  return {
8
8
  schema: 'sks.remote-action.v1',
9
- action_id: crypto.randomUUID(),
9
+ action_id: `tg-pick-${crypto.createHash('sha256').update([
10
+ updateId,
11
+ route.machine_id,
12
+ route.project_id,
13
+ route.session_id,
14
+ route.generation
15
+ ].join(':')).digest('hex').slice(0, 36)}`,
10
16
  machine_id: route.machine_id,
11
17
  project_id: route.project_id,
12
18
  session_id: route.session_id,
19
+ route_generation: route.generation,
13
20
  kind: 'read',
14
21
  risk: 'R0',
15
22
  prompt: 'pick',
@@ -63,6 +70,28 @@ export function successSummary(action, data) {
63
70
  return `Verification snapshot: ${snapshotSummary(data ?? {})}`;
64
71
  return snapshotSummary(data ?? {});
65
72
  }
73
+ export function failureSummary(codeInput, deliveryInput) {
74
+ const code = publicSafeText(String(codeInput || 'remote_command_failed')).slice(0, 160);
75
+ const delivery = String(deliveryInput || '');
76
+ if (code === 'codex_turn_completion_unknown') {
77
+ return `Codex accepted this turn, but SKS lost the completion signal. It will not replay the request automatically. Check SKS Center → Telegram → Codex activity or send /status before trying again. Code: ${code}`;
78
+ }
79
+ if (code === 'codex_turn_start_delivery_unknown' || delivery === 'unknown') {
80
+ return `Codex may already have received this request, so SKS will not replay it automatically. Check SKS Center → Telegram → Codex activity or send /status before trying again. Code: ${code}`;
81
+ }
82
+ if (code.includes('_contract_invalid')) {
83
+ return `The linked Codex App Server rejected an incompatible request contract before delivery. Restart the updated SKS Hub, then retry once. Code: ${code}`;
84
+ }
85
+ if (code === 'codex_app_server_initialize_failed'
86
+ || code === 'codex_app_server_unavailable'
87
+ || code === 'codex_app_server_provider_credentials_unavailable') {
88
+ return `The request was not sent because the linked Codex App Server could not initialize. Recheck provider authentication and Hub status in SKS Center. Code: ${code}`;
89
+ }
90
+ if (delivery === 'acknowledged') {
91
+ return `Codex acknowledged the request, but SKS could not finish projecting the result. Check the linked Codex thread in SKS Center before sending another request. Code: ${code}`;
92
+ }
93
+ return `Remote action blocked before completion. Use /status or SKS Center → Telegram → Diagnostics for the next action. Code: ${code}`;
94
+ }
66
95
  export function topicName(target, sessionId, snapshot) {
67
96
  const project = asRecord(snapshot?.project);
68
97
  return buildTelegramTopicName({
@@ -80,7 +109,13 @@ export function routeKey(route) {
80
109
  return `${route.machine_id}:${route.project_id}:${route.session_id}:${route.generation}`;
81
110
  }
82
111
  export function emptyProjection() {
83
- return { after_seq: 0, snapshot_digest: null, draft_message_id: null, final_generation: null };
112
+ return {
113
+ after_seq: 0,
114
+ snapshot_digest: null,
115
+ draft_message_id: null,
116
+ final_generation: null,
117
+ final_generation_unknown: null
118
+ };
84
119
  }
85
120
  export function requestId(prefix) {
86
121
  return `${prefix}:${crypto.randomUUID()}`;