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
@@ -0,0 +1,469 @@
1
+ import Cocoa
2
+ import Darwin
3
+
4
+ struct TelegramHubServiceSnapshot: Equatable {
5
+ let running: Bool
6
+ let receiveReady: Bool
7
+ let mode: String?
8
+ let pid: Int32?
9
+
10
+ var verifiedDirectPID: Int32? {
11
+ guard running, mode == "direct" else { return nil }
12
+ return pid
13
+ }
14
+ }
15
+
16
+ final class TelegramHubSupervisor {
17
+ private static let failureStore = TelegramHubStartupFailureStore()
18
+
19
+ static var latestStartupFailureMessage: String? {
20
+ failureStore.message
21
+ }
22
+
23
+ private let processClient: ProcessClient
24
+ private let actionScript: String
25
+ private let projectRoot: String
26
+ private var process: Process?
27
+ private var generation = 0
28
+ private var intentionalStop = false
29
+ private var startupReady = false
30
+ private var startupRetryCount = 0
31
+ private var startupTerminationReason: String?
32
+ private var unexpectedRestartCount = 0
33
+ private let predecessorWaitDelays: [TimeInterval] = [0.25, 0.5, 1, 1, 2, 2]
34
+ private let startupProbeDelays: [TimeInterval] = [0.25, 0.5, 1, 1, 2, 2, 3, 3]
35
+ private let startupRetryDelays: [TimeInterval] = [0.5, 1.5, 3]
36
+ private let restartDelays: [TimeInterval] = [1, 5, 15]
37
+
38
+ init(processClient: ProcessClient, actionScript: String, projectRoot: String) {
39
+ self.processClient = processClient
40
+ self.actionScript = actionScript
41
+ self.projectRoot = projectRoot
42
+ }
43
+
44
+ func startIfConfigured() {
45
+ let environment = ProcessInfo.processInfo.environment
46
+ let home = FileManager.default.homeDirectoryForCurrentUser
47
+ let globalRoot = environment["SKS_GLOBAL_ROOT"].map { URL(fileURLWithPath: $0, isDirectory: true) }
48
+ ?? home.appendingPathComponent(".sneakoscope-global", isDirectory: true)
49
+ let config = globalRoot
50
+ .appendingPathComponent("telegram", isDirectory: true)
51
+ .appendingPathComponent("config.json")
52
+ guard FileManager.default.fileExists(atPath: config.path) else { return }
53
+ start()
54
+ }
55
+
56
+ func start(completion: ((Bool) -> Void)? = nil) {
57
+ onMain {
58
+ self.unexpectedRestartCount = 0
59
+ self.startupRetryCount = 0
60
+ self.beginStart(completion: completion)
61
+ }
62
+ }
63
+
64
+ func stop(completion: ((Bool) -> Void)? = nil) {
65
+ onMain {
66
+ self.generation += 1
67
+ self.intentionalStop = true
68
+ self.startupReady = false
69
+ self.startupTerminationReason = nil
70
+ Self.failureStore.message = nil
71
+ let requestGeneration = self.generation
72
+ self.terminateOwnedProcess { ownedStopped in
73
+ guard self.generation == requestGeneration else { return }
74
+ self.stopLaunchAgent { result in
75
+ guard self.generation == requestGeneration else { return }
76
+ self.intentionalStop = false
77
+ completion?(ownedStopped && result.stopped)
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ func restart(completion: ((Bool) -> Void)? = nil) {
84
+ stop { [weak self] stopped in
85
+ guard let self = self, stopped else {
86
+ completion?(false)
87
+ return
88
+ }
89
+ self.start(completion: completion)
90
+ }
91
+ }
92
+
93
+ func shutdown() {
94
+ onMain {
95
+ self.generation += 1
96
+ self.intentionalStop = true
97
+ self.startupReady = false
98
+ self.startupTerminationReason = nil
99
+ guard let process = self.process, process.isRunning else { return }
100
+ let pid = process.processIdentifier
101
+ process.terminate()
102
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2) {
103
+ if process.isRunning { Darwin.kill(pid, SIGKILL) }
104
+ }
105
+ }
106
+ }
107
+
108
+ private func beginStart(completion: ((Bool) -> Void)?) {
109
+ if process?.isRunning == true {
110
+ completion?(startupReady)
111
+ return
112
+ }
113
+ generation += 1
114
+ intentionalStop = false
115
+ startupReady = false
116
+ startupTerminationReason = nil
117
+ let requestGeneration = generation
118
+ stopLaunchAgent { [weak self] result in
119
+ guard let self = self, self.generation == requestGeneration else { return }
120
+ if result.stopped {
121
+ self.waitForVerifiedPredecessorRelease(
122
+ generation: requestGeneration,
123
+ attempt: 0,
124
+ completion: completion
125
+ )
126
+ return
127
+ }
128
+ if result.snapshot?.verifiedDirectPID != nil {
129
+ self.waitForVerifiedPredecessorRelease(
130
+ generation: requestGeneration,
131
+ attempt: 0,
132
+ completion: completion
133
+ )
134
+ return
135
+ }
136
+ self.finishStartupFailure("telegram_hub_launch_agent_stop_failed", completion: completion)
137
+ }
138
+ }
139
+
140
+ private func waitForVerifiedPredecessorRelease(
141
+ generation requestGeneration: Int,
142
+ attempt: Int,
143
+ completion: ((Bool) -> Void)?
144
+ ) {
145
+ hubStatus { [weak self] _, snapshot in
146
+ guard let self = self, self.generation == requestGeneration else { return }
147
+ guard snapshot?.verifiedDirectPID != nil else {
148
+ self.launchDirect(generation: requestGeneration, completion: completion)
149
+ return
150
+ }
151
+ guard attempt < self.predecessorWaitDelays.count else {
152
+ self.finishStartupFailure("telegram_hub_predecessor_handoff_timeout", completion: completion)
153
+ return
154
+ }
155
+ let delay = self.predecessorWaitDelays[attempt]
156
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
157
+ guard let self = self,
158
+ self.generation == requestGeneration,
159
+ !self.intentionalStop else { return }
160
+ self.waitForVerifiedPredecessorRelease(
161
+ generation: requestGeneration,
162
+ attempt: attempt + 1,
163
+ completion: completion
164
+ )
165
+ }
166
+ }
167
+ }
168
+
169
+ private func launchDirect(generation requestGeneration: Int, completion: ((Bool) -> Void)?) {
170
+ let process = Process()
171
+ let output = Pipe()
172
+ process.executableURL = URL(fileURLWithPath: actionScript)
173
+ process.arguments = [
174
+ "telegram", "hub", "run",
175
+ "--project-root", projectRoot,
176
+ "--center-supervised",
177
+ "--center-supervisor-pid", String(getpid()),
178
+ "--json"
179
+ ]
180
+ process.currentDirectoryURL = FileManager.default.homeDirectoryForCurrentUser
181
+ process.standardInput = FileHandle.nullDevice
182
+ process.standardOutput = output
183
+ process.standardError = output
184
+ process.terminationHandler = { [weak self, weak process] _ in
185
+ DispatchQueue.global(qos: .utility).async {
186
+ let data = output.fileHandleForReading.readDataToEndOfFile()
187
+ let bounded = Data(data.suffix(32 * 1024))
188
+ let text = String(data: bounded, encoding: .utf8) ?? ""
189
+ DispatchQueue.main.async {
190
+ guard let self = self,
191
+ let process = process,
192
+ self.process === process,
193
+ self.generation == requestGeneration else { return }
194
+ self.handleDirectTermination(
195
+ output: text,
196
+ generation: requestGeneration,
197
+ completion: completion
198
+ )
199
+ }
200
+ }
201
+ }
202
+ do {
203
+ try process.run()
204
+ output.fileHandleForWriting.closeFile()
205
+ self.process = process
206
+ pollStartupReadiness(
207
+ for: process,
208
+ generation: requestGeneration,
209
+ attempt: 0,
210
+ completion: completion
211
+ )
212
+ } catch {
213
+ finishOrRetryStartup(
214
+ reason: "telegram_hub_process_launch_failed",
215
+ generation: requestGeneration,
216
+ completion: completion
217
+ )
218
+ }
219
+ }
220
+
221
+ private func pollStartupReadiness(
222
+ for process: Process,
223
+ generation requestGeneration: Int,
224
+ attempt: Int,
225
+ completion: ((Bool) -> Void)?
226
+ ) {
227
+ guard self.process === process, process.isRunning else { return }
228
+ hubStatus { [weak self, weak process] _, snapshot in
229
+ guard let self = self,
230
+ let process = process,
231
+ self.process === process,
232
+ self.generation == requestGeneration,
233
+ process.isRunning else { return }
234
+ if snapshot?.running == true,
235
+ snapshot?.receiveReady == true,
236
+ snapshot?.mode == "direct",
237
+ snapshot?.pid == process.processIdentifier {
238
+ self.startupReady = true
239
+ self.startupRetryCount = 0
240
+ Self.failureStore.message = nil
241
+ completion?(true)
242
+ DispatchQueue.main.asyncAfter(deadline: .now() + 60) { [weak self, weak process] in
243
+ guard let self = self,
244
+ let process = process,
245
+ self.process === process,
246
+ process.isRunning else { return }
247
+ self.unexpectedRestartCount = 0
248
+ }
249
+ return
250
+ }
251
+ guard attempt < self.startupProbeDelays.count else {
252
+ self.startupTerminationReason = "telegram_hub_startup_timeout"
253
+ self.terminateOwnedStartupProcess(process)
254
+ return
255
+ }
256
+ let delay = self.startupProbeDelays[attempt]
257
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self, weak process] in
258
+ guard let self = self,
259
+ let process = process,
260
+ self.process === process,
261
+ self.generation == requestGeneration,
262
+ process.isRunning else { return }
263
+ self.pollStartupReadiness(
264
+ for: process,
265
+ generation: requestGeneration,
266
+ attempt: attempt + 1,
267
+ completion: completion
268
+ )
269
+ }
270
+ }
271
+ }
272
+
273
+ private func handleDirectTermination(
274
+ output: String,
275
+ generation requestGeneration: Int,
276
+ completion: ((Bool) -> Void)?
277
+ ) {
278
+ process = nil
279
+ let wasReady = startupReady
280
+ startupReady = false
281
+ let forcedReason = startupTerminationReason
282
+ startupTerminationReason = nil
283
+ guard !intentionalStop else { return }
284
+ if wasReady {
285
+ scheduleUnexpectedRestart(after: requestGeneration)
286
+ return
287
+ }
288
+ let reason = forcedReason ?? Self.startupFailureReason(output: output)
289
+ finishOrRetryStartup(reason: reason, generation: requestGeneration, completion: completion)
290
+ }
291
+
292
+ private func finishOrRetryStartup(
293
+ reason: String,
294
+ generation requestGeneration: Int,
295
+ completion: ((Bool) -> Void)?
296
+ ) {
297
+ guard Self.retryableStartupFailure(reason),
298
+ startupRetryCount < startupRetryDelays.count else {
299
+ finishStartupFailure(reason, completion: completion)
300
+ return
301
+ }
302
+ let delay = startupRetryDelays[startupRetryCount]
303
+ startupRetryCount += 1
304
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
305
+ guard let self = self,
306
+ self.generation == requestGeneration,
307
+ !self.intentionalStop,
308
+ self.process?.isRunning != true else { return }
309
+ self.beginStart(completion: completion)
310
+ }
311
+ }
312
+
313
+ private func finishStartupFailure(_ reason: String, completion: ((Bool) -> Void)?) {
314
+ Self.failureStore.message = Self.startupFailureMessage(for: reason)
315
+ completion?(false)
316
+ }
317
+
318
+ private func scheduleUnexpectedRestart(after requestGeneration: Int) {
319
+ guard unexpectedRestartCount < restartDelays.count else {
320
+ finishStartupFailure("telegram_hub_unexpected_exit", completion: nil)
321
+ return
322
+ }
323
+ let delay = restartDelays[unexpectedRestartCount]
324
+ unexpectedRestartCount += 1
325
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
326
+ guard let self = self,
327
+ self.generation == requestGeneration,
328
+ !self.intentionalStop,
329
+ self.process?.isRunning != true else { return }
330
+ self.startupRetryCount = 0
331
+ self.beginStart(completion: nil)
332
+ }
333
+ }
334
+
335
+ private func terminateOwnedStartupProcess(_ process: Process) {
336
+ guard self.process === process, process.isRunning else { return }
337
+ let pid = process.processIdentifier
338
+ process.terminate()
339
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2) {
340
+ if process.isRunning { Darwin.kill(pid, SIGKILL) }
341
+ }
342
+ }
343
+
344
+ private func terminateOwnedProcess(completion: @escaping (Bool) -> Void) {
345
+ guard let process = process, process.isRunning else {
346
+ self.process = nil
347
+ completion(true)
348
+ return
349
+ }
350
+ self.process = nil
351
+ let pid = process.processIdentifier
352
+ process.terminationHandler = { _ in DispatchQueue.main.async { completion(true) } }
353
+ process.terminate()
354
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 2) {
355
+ if process.isRunning { Darwin.kill(pid, SIGKILL) }
356
+ }
357
+ }
358
+
359
+ private func stopLaunchAgent(completion: @escaping (TelegramHubStopResult) -> Void) {
360
+ processClient.run(
361
+ ["telegram", "hub", "stop", "--project-root", projectRoot, "--json"],
362
+ timeout: 12,
363
+ maxOutputBytes: 32 * 1024,
364
+ logOutput: false
365
+ ) { result in
366
+ completion(TelegramHubStopResult(
367
+ stopped: result.code == 0,
368
+ snapshot: Self.serviceSnapshot(from: result.output)
369
+ ))
370
+ }
371
+ }
372
+
373
+ private func hubStatus(completion: @escaping (ProcessResult, TelegramHubServiceSnapshot?) -> Void) {
374
+ processClient.run(
375
+ ["telegram", "hub", "status", "--project-root", projectRoot, "--json"],
376
+ timeout: 5,
377
+ maxOutputBytes: 32 * 1024,
378
+ logOutput: false
379
+ ) { result in
380
+ completion(result, Self.serviceSnapshot(from: result.output))
381
+ }
382
+ }
383
+
384
+ static func serviceSnapshot(from output: String) -> TelegramHubServiceSnapshot? {
385
+ guard let data = output.data(using: .utf8),
386
+ let value = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
387
+ value["schema"] as? String == "sks.telegram-hub-service.v1" else { return nil }
388
+ let pid = (value["pid"] as? NSNumber)?.int32Value
389
+ return TelegramHubServiceSnapshot(
390
+ running: value["running"] as? Bool == true,
391
+ receiveReady: value["receive_ready"] as? Bool == true,
392
+ mode: value["mode"] as? String,
393
+ pid: pid.flatMap { $0 > 0 ? $0 : nil }
394
+ )
395
+ }
396
+
397
+ static func startupFailureReason(output: String) -> String {
398
+ if output.contains("telegram_owner_conflict") { return "telegram_hub_owner_conflict" }
399
+ if output.contains("telegram_409_conflict") { return "telegram_hub_external_poller_conflict" }
400
+ if output.contains("telegram_api_timeout")
401
+ || output.contains("telegram_api_transport_failed")
402
+ || output.contains("telegram_poll_failed") {
403
+ return "telegram_hub_network_unavailable"
404
+ }
405
+ if output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
406
+ return "telegram_hub_empty_startup_output"
407
+ }
408
+ return "telegram_hub_startup_failed"
409
+ }
410
+
411
+ static func retryableStartupFailure(_ reason: String) -> Bool {
412
+ [
413
+ "telegram_hub_owner_conflict",
414
+ "telegram_hub_external_poller_conflict",
415
+ "telegram_hub_network_unavailable",
416
+ "telegram_hub_startup_timeout",
417
+ "telegram_hub_empty_startup_output"
418
+ ].contains(reason)
419
+ }
420
+
421
+ static func startupFailureMessage(for reason: String) -> String {
422
+ switch reason {
423
+ case "telegram_hub_predecessor_handoff_timeout":
424
+ return "The previous Center-supervised Telegram Hub is still shutting down. Wait a few seconds, then retry. Center did not terminate an unverified owner."
425
+ case "telegram_hub_owner_conflict":
426
+ return "Another Telegram Hub still owns this bot. Stop that Hub, then retry. Center did not terminate the other owner."
427
+ case "telegram_hub_external_poller_conflict":
428
+ return "Telegram reports another active poller for this bot. Stop the other poller, then retry."
429
+ case "telegram_hub_network_unavailable":
430
+ return "Telegram Hub could not reach the Telegram API after bounded retries. Check the network, then retry."
431
+ case "telegram_hub_startup_timeout":
432
+ return "Telegram Hub did not become ready before the bounded startup deadline. Check Telegram connectivity, then retry."
433
+ case "telegram_hub_launch_agent_stop_failed":
434
+ return "The legacy Telegram Hub service could not be stopped safely. Open Diagnostics, resolve the service error, then retry."
435
+ case "telegram_hub_process_launch_failed":
436
+ return "Telegram Hub could not launch. Open Diagnostics, confirm the SKS runtime is available, then retry."
437
+ default:
438
+ return "Telegram Hub startup failed. Open Diagnostics, resolve the reported Telegram error, then retry."
439
+ }
440
+ }
441
+
442
+ private func onMain(_ work: @escaping () -> Void) {
443
+ if Thread.isMainThread { work() }
444
+ else { DispatchQueue.main.async(execute: work) }
445
+ }
446
+ }
447
+
448
+ private struct TelegramHubStopResult {
449
+ let stopped: Bool
450
+ let snapshot: TelegramHubServiceSnapshot?
451
+ }
452
+
453
+ private final class TelegramHubStartupFailureStore: @unchecked Sendable {
454
+ private let lock = NSLock()
455
+ private var value: String?
456
+
457
+ var message: String? {
458
+ get {
459
+ lock.lock()
460
+ defer { lock.unlock() }
461
+ return value
462
+ }
463
+ set {
464
+ lock.lock()
465
+ value = newValue
466
+ lock.unlock()
467
+ }
468
+ }
469
+ }
@@ -34,7 +34,7 @@ final class UpdatesViewController: NSViewController, ControlCenterPage {
34
34
  checkButton = NativeView.button("Check Now", target: self, action: #selector(checkNow))
35
35
  codexUpdateButton = NativeView.button("Update Codex CLI", target: self, action: #selector(updateCodexCLI))
36
36
  codexUpdateButton.setAccessibilityHelp("Update the operator Codex CLI using its verified installation method.")
37
- reviewButton = ControlKit.primaryButton("Review and Update", target: self, action: #selector(reviewAndUpdate))
37
+ reviewButton = ControlKit.primaryButton("Review and Update", target: self, action: #selector(reviewAndUpdate), isDefault: true)
38
38
  reviewButton.setAccessibilityHelp("Review the staged update first, then confirm before anything is installed.")
39
39
  codexUpdateStatus.setAccessibilityLabel("Codex CLI update result")
40
40
  let buttons = NSStackView(views: [checkButton, codexUpdateButton, reviewButton, progress])
@@ -46,17 +46,12 @@ final class UpdatesViewController: NSViewController, ControlCenterPage {
46
46
  NativeView.card(title: "Progress", subtitle: "Authoritative stage checklist from the update receipt.", views: [stageStatus]),
47
47
  NativeView.card(title: "Recovery", subtitle: "Rollback guidance and the previous Menu Bar app remain available if final verification fails. The update receipt records the exact recovery command and stage state.", views: [remediation])
48
48
  ])
49
- loadCached()
50
49
  }
51
50
 
52
51
  deinit { receiptTimer?.invalidate() }
53
52
 
54
53
  func refreshOnAppear() { reloadSnapshot() }
55
54
 
56
- private func loadCached() {
57
- reloadSnapshot()
58
- }
59
-
60
55
  private func setBusy(_ value: Bool) {
61
56
  busy = value
62
57
  checkButton?.isEnabled = !value
@@ -0,0 +1,12 @@
1
+ #if SKS_NATIVE_TESTING
2
+ enum AppRuntime {
3
+ static let actionScript = "/tmp/sks-menubar-action.sh"
4
+ static let projectRoot = "/tmp/sks-menubar-project"
5
+ static let buildStampPath = "/tmp/sks-menubar-build-stamp.json"
6
+ static let configPath = "/tmp/sks-menubar-config.json"
7
+ static let lastActionLogPath = "/tmp/sks-menubar-last-action.log"
8
+ static let operationDirectory = "/tmp/sks-menubar-operations"
9
+ static let codexBundleId: String? = nil
10
+ static let packageVersion = "test"
11
+ }
12
+ #endif
@@ -0,0 +1,55 @@
1
+ #if SKS_NATIVE_TEST_RUNNER
2
+ import Cocoa
3
+ import Darwin
4
+
5
+ @main
6
+ struct NativeTelegramRegressionRunner {
7
+ static func main() {
8
+ precondition(
9
+ ProcessClient.alignedTimeout(
10
+ arguments: ["telegram", "status", "--json"],
11
+ requested: 8
12
+ ) == 12
13
+ )
14
+ precondition(
15
+ TelegramHubSupervisor.serviceSnapshot(
16
+ from: #"{"schema":"sks.telegram-hub-service.v1","running":true,"receive_ready":false,"mode":"direct","pid":4242}"#
17
+ )?.verifiedDirectPID == 4242
18
+ )
19
+ precondition(
20
+ TelegramHubSupervisor.serviceSnapshot(
21
+ from: #"{"schema":"sks.telegram-hub-service.v1","running":false,"receive_ready":false,"mode":null,"pid":null}"#
22
+ )?.verifiedDirectPID == nil
23
+ )
24
+ precondition(TelegramHubSupervisor.retryableStartupFailure("telegram_hub_owner_conflict"))
25
+ precondition(TelegramHubSupervisor.retryableStartupFailure("telegram_hub_network_unavailable"))
26
+
27
+ let temp = FileManager.default.temporaryDirectory
28
+ .appendingPathComponent("sks-native-telegram-regression-\(UUID().uuidString)", isDirectory: true)
29
+ try! FileManager.default.createDirectory(at: temp, withIntermediateDirectories: true)
30
+ let client = ProcessClient(
31
+ actionScript: "/bin/sh",
32
+ logPath: temp.appendingPathComponent("process.log").path,
33
+ projectRoot: temp.path
34
+ )
35
+ client.run(["-c", "sleep 30"], timeout: 0.2, logOutput: false) { timeoutResult in
36
+ precondition(timeoutResult.code == -2)
37
+ precondition(timeoutResult.timedOut)
38
+ precondition(!timeoutResult.emptyOutput)
39
+ precondition(timeoutResult.output.contains("native_process_timeout"))
40
+ client.run(["-c", "exit 0"], timeout: 2, logOutput: false) { emptyResult in
41
+ precondition(emptyResult.code == -3)
42
+ precondition(!emptyResult.timedOut)
43
+ precondition(emptyResult.emptyOutput)
44
+ precondition(emptyResult.output.contains("native_process_empty_output"))
45
+ let message = RemoteTelegramViewController.telegramStatusFailureMessage(emptyResult)
46
+ precondition(message.contains("returned no JSON"))
47
+ try? FileManager.default.removeItem(at: temp)
48
+ print("native-telegram-regressions-ok")
49
+ Darwin.exit(0)
50
+ }
51
+ }
52
+ dispatchMain()
53
+ }
54
+ }
55
+ #endif
@@ -0,0 +1,33 @@
1
+ #if canImport(XCTest)
2
+ import XCTest
3
+
4
+ final class ProcessClientTests: XCTestCase {
5
+ func testTelegramStatusTimeoutLeavesRoomForInnerProbeBudget() {
6
+ XCTAssertEqual(
7
+ ProcessClient.alignedTimeout(
8
+ arguments: ["telegram", "status", "--json"],
9
+ requested: 8
10
+ ),
11
+ 12
12
+ )
13
+ XCTAssertEqual(
14
+ ProcessClient.alignedTimeout(
15
+ arguments: ["remote", "readiness", "--json"],
16
+ requested: 8
17
+ ),
18
+ 8
19
+ )
20
+ }
21
+
22
+ func testNativeTimeoutAndEmptyOutputAreStablePublicErrors() {
23
+ XCTAssertEqual(
24
+ ProcessClient.nativeFailureOutput("native_process_timeout"),
25
+ #"{"schema":"sks.native-process-error.v1","ok":false,"error":"native_process_timeout"}"#
26
+ )
27
+ XCTAssertEqual(
28
+ ProcessClient.nativeFailureOutput("native_process_empty_output"),
29
+ #"{"schema":"sks.native-process-error.v1","ok":false,"error":"native_process_empty_output"}"#
30
+ )
31
+ }
32
+ }
33
+ #endif