apsimo 1.3.0__py3-none-any.whl

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 (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. colony_sidecar/__init__.py +4 -0
@@ -0,0 +1,1446 @@
1
+ """ProjectStore -- SQLite persistence for projects + steps (survives restarts)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+ import threading
7
+ import time
8
+ import hashlib
9
+ import json
10
+ import os
11
+ from datetime import datetime, timezone
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ from apsimo.projects.models import Project, Step
15
+
16
+
17
+ class ProjectStore:
18
+ _AUTHORITY_BOUND_PROJECT_SOURCES = frozenset({
19
+ "cognition_spine", "governed_action",
20
+ })
21
+ _AUTHORITY_BOUND_PROJECT_IMMUTABLE_FIELDS = (
22
+ "title", "objective", "source", "concern_id",
23
+ "source_event_refs", "thought_job_id", "thought_result_ref",
24
+ "goal_proposal_id", "evidence_refs", "policy_decision_refs",
25
+ "subject_person_id", "viewer_scope", "shareability",
26
+ "capability_allowlist", "goal_fingerprint", "entity_ids",
27
+ )
28
+ _WORK_ORDER_PROJECT_AUTHORITY_FIELDS = (
29
+ _AUTHORITY_BOUND_PROJECT_IMMUTABLE_FIELDS
30
+ )
31
+
32
+ def __init__(self, db_path: Optional[str] = None) -> None:
33
+ self._lock = threading.RLock()
34
+ self._conn = sqlite3.connect(str(db_path) if db_path else ":memory:",
35
+ check_same_thread=False)
36
+ self._conn.row_factory = sqlite3.Row
37
+ with self._lock:
38
+ self._conn.execute(
39
+ """CREATE TABLE IF NOT EXISTS projects (
40
+ id TEXT PRIMARY KEY, title TEXT NOT NULL, objective TEXT,
41
+ source TEXT, status TEXT, outcome TEXT DEFAULT 'pending',
42
+ entity_ids TEXT, reason TEXT,
43
+ replans INTEGER DEFAULT 0, next_review_at REAL,
44
+ concern_id TEXT, source_event_refs TEXT,
45
+ thought_job_id TEXT, thought_result_ref TEXT,
46
+ goal_proposal_id TEXT, evidence_refs TEXT,
47
+ policy_decision_refs TEXT, subject_person_id TEXT,
48
+ viewer_scope TEXT DEFAULT 'owner',
49
+ shareability TEXT DEFAULT 'owner_private',
50
+ capability_allowlist TEXT, goal_fingerprint TEXT,
51
+ created_at REAL, updated_at REAL
52
+ )""")
53
+ self._conn.execute(
54
+ """CREATE TABLE IF NOT EXISTS steps (
55
+ id TEXT PRIMARY KEY, project_id TEXT NOT NULL,
56
+ ordinal INTEGER, description TEXT, action_kind TEXT,
57
+ depends_on TEXT, status TEXT, attempts INTEGER DEFAULT 0,
58
+ result TEXT, work_order_ref TEXT, work_order_digest TEXT,
59
+ work_order_issued_at REAL DEFAULT 0, result_ref TEXT,
60
+ boundary_subject TEXT,
61
+ confidence REAL DEFAULT 0.6,
62
+ created_at REAL, updated_at REAL
63
+ )""")
64
+ # Additive migrations only: old databases remain directly
65
+ # readable and rollback can ignore the new columns/tables.
66
+ project_cols = {r[1] for r in self._conn.execute(
67
+ "PRAGMA table_info(projects)").fetchall()}
68
+ for name, definition in (
69
+ ("outcome", "TEXT DEFAULT 'pending'"),
70
+ ("concern_id", "TEXT"),
71
+ ("source_event_refs", "TEXT"),
72
+ ("thought_job_id", "TEXT"),
73
+ ("thought_result_ref", "TEXT"),
74
+ ("goal_proposal_id", "TEXT"),
75
+ ("evidence_refs", "TEXT"),
76
+ ("policy_decision_refs", "TEXT"),
77
+ ("subject_person_id", "TEXT"),
78
+ ("viewer_scope", "TEXT DEFAULT 'owner'"),
79
+ ("shareability", "TEXT DEFAULT 'owner_private'"),
80
+ ("capability_allowlist", "TEXT"),
81
+ ("goal_fingerprint", "TEXT"),
82
+ ):
83
+ if name not in project_cols:
84
+ self._conn.execute(
85
+ f"ALTER TABLE projects ADD COLUMN {name} {definition}")
86
+ cols = {r[1] for r in self._conn.execute(
87
+ "PRAGMA table_info(steps)").fetchall()}
88
+ for name, definition in (
89
+ ("confidence", "REAL DEFAULT 0.6"),
90
+ ("work_order_ref", "TEXT"),
91
+ ("work_order_digest", "TEXT"),
92
+ ("work_order_issued_at", "REAL DEFAULT 0"),
93
+ ("result_ref", "TEXT"),
94
+ ):
95
+ if name not in cols:
96
+ self._conn.execute(
97
+ f"ALTER TABLE steps ADD COLUMN {name} {definition}")
98
+ self._conn.execute(
99
+ """CREATE TABLE IF NOT EXISTS project_work_orders (
100
+ work_order_ref TEXT PRIMARY KEY,
101
+ work_order_id TEXT NOT NULL UNIQUE,
102
+ work_order_digest TEXT NOT NULL UNIQUE,
103
+ project_id TEXT NOT NULL,
104
+ step_id TEXT NOT NULL,
105
+ schema_version INTEGER NOT NULL,
106
+ payload_json TEXT NOT NULL,
107
+ created_at REAL NOT NULL
108
+ )""")
109
+ self._conn.execute(
110
+ """CREATE TABLE IF NOT EXISTS project_execution_results (
111
+ result_ref TEXT PRIMARY KEY,
112
+ work_order_ref TEXT NOT NULL UNIQUE,
113
+ work_order_id TEXT NOT NULL UNIQUE,
114
+ work_order_digest TEXT NOT NULL,
115
+ run_id TEXT NOT NULL,
116
+ attempt_number INTEGER NOT NULL,
117
+ terminal_outcome TEXT NOT NULL,
118
+ verification_result TEXT NOT NULL,
119
+ effect_class TEXT NOT NULL,
120
+ payload_json TEXT NOT NULL,
121
+ created_at REAL NOT NULL,
122
+ updated_at REAL NOT NULL
123
+ )""")
124
+ self._conn.execute(
125
+ """CREATE TABLE IF NOT EXISTS project_execution_attempts (
126
+ attempt_ref TEXT PRIMARY KEY,
127
+ work_order_ref TEXT NOT NULL,
128
+ work_order_id TEXT NOT NULL,
129
+ run_id TEXT NOT NULL,
130
+ attempt_number INTEGER NOT NULL,
131
+ transport_status TEXT NOT NULL,
132
+ result_json TEXT NOT NULL,
133
+ created_at REAL NOT NULL,
134
+ UNIQUE(work_order_id, run_id),
135
+ UNIQUE(work_order_id, attempt_number)
136
+ )""")
137
+ self._conn.execute(
138
+ """CREATE TABLE IF NOT EXISTS project_event_outbox (
139
+ event_key TEXT PRIMARY KEY,
140
+ event_type TEXT NOT NULL,
141
+ event_digest TEXT NOT NULL,
142
+ payload_json TEXT NOT NULL,
143
+ occurred_at TEXT NOT NULL,
144
+ state TEXT NOT NULL DEFAULT 'pending',
145
+ journal_seq INTEGER,
146
+ journal_event_id TEXT,
147
+ journal_recorded_at TEXT,
148
+ projection_receipt_digest TEXT,
149
+ journal_acknowledged INTEGER NOT NULL DEFAULT 0,
150
+ projection_attempts INTEGER NOT NULL DEFAULT 0,
151
+ last_error TEXT,
152
+ created_at REAL NOT NULL,
153
+ updated_at REAL NOT NULL,
154
+ CHECK (state IN ('pending','projected'))
155
+ )""")
156
+ outbox_cols = {r[1] for r in self._conn.execute(
157
+ "PRAGMA table_info(project_event_outbox)"
158
+ ).fetchall()}
159
+ if "journal_acknowledged" not in outbox_cols:
160
+ self._conn.execute(
161
+ "ALTER TABLE project_event_outbox ADD COLUMN "
162
+ "journal_acknowledged INTEGER NOT NULL DEFAULT 0"
163
+ )
164
+ if "projection_receipt_digest" not in outbox_cols:
165
+ self._conn.execute(
166
+ "ALTER TABLE project_event_outbox ADD COLUMN "
167
+ "projection_receipt_digest TEXT"
168
+ )
169
+ self._conn.execute(
170
+ "CREATE INDEX IF NOT EXISTS idx_project_event_outbox_state "
171
+ "ON project_event_outbox(state, created_at, event_key)")
172
+ self._conn.execute(
173
+ "CREATE INDEX IF NOT EXISTS idx_steps_project ON steps(project_id)")
174
+ self._conn.execute(
175
+ "CREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status)")
176
+ self._conn.execute(
177
+ "CREATE INDEX IF NOT EXISTS idx_projects_goal_fingerprint "
178
+ "ON projects(goal_fingerprint)")
179
+ self._conn.commit()
180
+
181
+ def close(self) -> None:
182
+ with self._lock:
183
+ self._conn.close()
184
+
185
+ @staticmethod
186
+ def _json(payload: Dict[str, Any]) -> str:
187
+ return json.dumps(payload, sort_keys=True, separators=(",", ":"),
188
+ ensure_ascii=False)
189
+
190
+ @staticmethod
191
+ def _digest(payload: Dict[str, Any]) -> str:
192
+ return hashlib.sha256(
193
+ ProjectStore._json(payload).encode("utf-8")
194
+ ).hexdigest()
195
+
196
+ @staticmethod
197
+ def project_event_envelope_digest(
198
+ *, event_key: str, event_type: str, occurred_at: str,
199
+ payload: Dict[str, Any],
200
+ ) -> str:
201
+ """Bind payload and all journal-routing envelope fields together."""
202
+
203
+ return ProjectStore._digest({
204
+ "schema": "ProjectEventEnvelopeV1",
205
+ "version": 1,
206
+ "event_key": str(event_key),
207
+ "event_type": str(event_type),
208
+ "occurred_at": str(occurred_at),
209
+ "payload": payload,
210
+ })
211
+
212
+ @staticmethod
213
+ def project_event_projection_receipt_digest(
214
+ *,
215
+ event_key: str,
216
+ event_type: str,
217
+ event_digest: str,
218
+ occurred_at: str,
219
+ journal_seq: int,
220
+ journal_event_id: str,
221
+ journal_recorded_at: str,
222
+ ) -> str:
223
+ """Bind a staged envelope to the exact journal projection receipt."""
224
+
225
+ key = str(event_key or "").strip()
226
+ kind = str(event_type or "").strip()
227
+ envelope_digest = str(event_digest or "").strip()
228
+ occurred = str(occurred_at or "").strip()
229
+ event_id = str(journal_event_id or "").strip()
230
+ recorded = str(journal_recorded_at or "").strip()
231
+ if not key or len(key) > 256 or not kind or len(kind) > 128:
232
+ raise ValueError("journal projection envelope identity is invalid")
233
+ if len(envelope_digest) != 64 or any(
234
+ character not in "0123456789abcdef"
235
+ for character in envelope_digest
236
+ ):
237
+ raise ValueError("journal projection envelope digest is invalid")
238
+ if type(journal_seq) is not int or journal_seq < 1:
239
+ raise ValueError("journal projection sequence is invalid")
240
+ if (
241
+ not event_id
242
+ or len(event_id) > 256
243
+ or any(character.isspace() for character in event_id)
244
+ ):
245
+ raise ValueError("journal projection event ID is invalid")
246
+ try:
247
+ occurred_time = datetime.fromisoformat(
248
+ occurred.replace("Z", "+00:00")
249
+ )
250
+ recorded_time = datetime.fromisoformat(
251
+ recorded.replace("Z", "+00:00")
252
+ )
253
+ except (TypeError, ValueError) as exc:
254
+ raise ValueError("journal projection timestamps are invalid") from exc
255
+ if occurred_time.tzinfo is None or recorded_time.tzinfo is None:
256
+ raise ValueError("journal projection timestamps require a timezone")
257
+ return ProjectStore._digest({
258
+ "schema": "ProjectEventProjectionReceiptV1",
259
+ "version": 1,
260
+ "event_key": key,
261
+ "event_type": kind,
262
+ "event_digest": envelope_digest,
263
+ "occurred_at": occurred,
264
+ "journal_seq": journal_seq,
265
+ "journal_event_id": event_id,
266
+ "journal_recorded_at": recorded,
267
+ })
268
+
269
+ @staticmethod
270
+ def _cognition_mode_at_stage() -> str:
271
+ """Capture the server-controlled evidence mode in the staged record.
272
+
273
+ The reducer may consume the journal much later under a different
274
+ runtime mode. Binding the mode into the immutable outbox payload
275
+ prevents outcomes produced while the legacy writer was authoritative
276
+ from becoming new receipt-derived learning after a live cutover.
277
+ """
278
+
279
+ value = os.environ.get(
280
+ "COLONY_COGNITION_EVIDENCE", "off",
281
+ ).strip().lower()
282
+ return value if value in {"off", "shadow", "live"} else "off"
283
+
284
+ def _cognition_mode_for_event_locked(
285
+ self, event_key: str,
286
+ ) -> Optional[str]:
287
+ """Return the first-stage mode, never the mode of a later replay."""
288
+
289
+ row = self._conn.execute(
290
+ "SELECT payload_json FROM project_event_outbox WHERE event_key=?",
291
+ (str(event_key),),
292
+ ).fetchone()
293
+ if row is None:
294
+ return self._cognition_mode_at_stage()
295
+ try:
296
+ payload = json.loads(str(row["payload_json"]))
297
+ except (TypeError, ValueError, json.JSONDecodeError):
298
+ return None
299
+ if not isinstance(payload, dict):
300
+ return None
301
+ mode = str(payload.get("cognition_mode_at_stage") or "").lower()
302
+ return mode if mode in {"off", "shadow", "live"} else None
303
+
304
+ def _stage_event_locked(
305
+ self,
306
+ *,
307
+ event_key: str,
308
+ event_type: str,
309
+ payload: Dict[str, Any],
310
+ occurred_at: str,
311
+ ) -> None:
312
+ """Stage one immutable journal projection in the owning transaction."""
313
+
314
+ key = str(event_key or "").strip()
315
+ kind = str(event_type or "").strip().lower()
316
+ when = str(occurred_at or "").strip()
317
+ if not key or len(key) > 256:
318
+ raise ValueError("project event key is invalid")
319
+ if not kind or len(kind) > 128:
320
+ raise ValueError("project event type is invalid")
321
+ try:
322
+ parsed = datetime.fromisoformat(when.replace("Z", "+00:00"))
323
+ except (TypeError, ValueError) as exc:
324
+ raise ValueError("project event occurred_at is invalid") from exc
325
+ if parsed.tzinfo is None:
326
+ raise ValueError("project event occurred_at requires a timezone")
327
+ encoded = self._json(payload)
328
+ existing = self._conn.execute(
329
+ "SELECT * FROM project_event_outbox WHERE event_key=?", (key,),
330
+ ).fetchone()
331
+ if existing is not None:
332
+ digest = self.project_event_envelope_digest(
333
+ event_key=key,
334
+ event_type=kind,
335
+ occurred_at=str(existing["occurred_at"]),
336
+ payload=payload,
337
+ )
338
+ if (
339
+ existing["event_type"] != kind
340
+ or existing["event_digest"] != digest
341
+ or existing["payload_json"] != encoded
342
+ ):
343
+ raise ValueError("immutable project event replay mismatch")
344
+ return
345
+ digest = self.project_event_envelope_digest(
346
+ event_key=key,
347
+ event_type=kind,
348
+ occurred_at=when,
349
+ payload=payload,
350
+ )
351
+ stamp = time.time()
352
+ self._conn.execute(
353
+ """INSERT INTO project_event_outbox (
354
+ event_key,event_type,event_digest,payload_json,occurred_at,
355
+ state,created_at,updated_at
356
+ ) VALUES (?,?,?,?,?,'pending',?,?)""",
357
+ (key, kind, digest, encoded, when, stamp, stamp),
358
+ )
359
+
360
+ def _stage_project_terminal_locked(self, project: Project) -> None:
361
+ if project.status not in {"completed", "abandoned"}:
362
+ return
363
+ identity = self._digest({
364
+ "project_id": project.id,
365
+ "lifecycle_status": project.status,
366
+ "outcome": project.outcome,
367
+ })
368
+ event_key = f"project-terminal:{identity}"
369
+ mode_at_stage = self._cognition_mode_for_event_locked(event_key)
370
+ rows = self._conn.execute(
371
+ "SELECT id,status,result_ref FROM steps WHERE project_id=? "
372
+ "ORDER BY ordinal ASC,id ASC",
373
+ (project.id,),
374
+ ).fetchall()
375
+ results: list[Dict[str, Any]] = []
376
+ verified_success = bool(rows) and project.outcome == "succeeded"
377
+ for row in rows:
378
+ result_ref = str(row["result_ref"] or "")
379
+ if row["status"] != "done" or not result_ref:
380
+ verified_success = False
381
+ continue
382
+ result_row = self._conn.execute(
383
+ "SELECT * FROM project_execution_results WHERE result_ref=?",
384
+ (result_ref,),
385
+ ).fetchone()
386
+ if result_row is None:
387
+ verified_success = False
388
+ continue
389
+ result_payload = json.loads(result_row["payload_json"])
390
+ work_order_row = self._conn.execute(
391
+ "SELECT * FROM project_work_orders WHERE work_order_id=?",
392
+ (result_row["work_order_id"],),
393
+ ).fetchone()
394
+ receipt_refs = result_payload.get("receipt_refs") or []
395
+ if (
396
+ work_order_row is None
397
+ or work_order_row["project_id"] != project.id
398
+ or work_order_row["step_id"] != row["id"]
399
+ or work_order_row["work_order_digest"]
400
+ != result_row["work_order_digest"]
401
+ or result_payload.get("work_order_id")
402
+ != result_row["work_order_id"]
403
+ or result_payload.get("work_order_digest")
404
+ != result_row["work_order_digest"]
405
+ or
406
+ result_row["terminal_outcome"] != "succeeded"
407
+ or result_row["verification_result"] != "verified"
408
+ or not isinstance(receipt_refs, list)
409
+ or not receipt_refs
410
+ ):
411
+ verified_success = False
412
+ results.append({
413
+ "step_id": str(row["id"]),
414
+ "result_ref": result_ref,
415
+ "result_digest": self._digest(result_payload),
416
+ })
417
+ material = {
418
+ "schema": "ProjectTerminalEvidenceV2",
419
+ "version": 2,
420
+ "project_id": project.id,
421
+ "lifecycle_status": project.status,
422
+ "outcome": project.outcome,
423
+ "status": (
424
+ "verified"
425
+ if verified_success else
426
+ "failed"
427
+ if project.status == "abandoned" else
428
+ "unverified"
429
+ ),
430
+ "reason_code": str(project.reason or "")[:128],
431
+ "source": str(project.source or "")[:64],
432
+ "subject_person_id": project.subject_person_id or "owner",
433
+ "viewer_scope": project.viewer_scope or "owner",
434
+ "shareability": project.shareability or "owner_private",
435
+ "concern_id": str(project.concern_id or "")[:128],
436
+ "goal_proposal_id": str(project.goal_proposal_id or "")[:256],
437
+ "result_refs": results,
438
+ "evidence_status": (
439
+ "verified" if verified_success else "unverified"
440
+ ),
441
+ }
442
+ if mode_at_stage is not None:
443
+ material["cognition_mode_at_stage"] = mode_at_stage
444
+ material["project_digest"] = self._digest(material)
445
+ self._stage_event_locked(
446
+ event_key=event_key,
447
+ event_type=f"project.{project.status}",
448
+ payload=material,
449
+ occurred_at=datetime.now(timezone.utc).isoformat(),
450
+ )
451
+
452
+ # -- projects ---------------------------------------------------------
453
+ def insert_authority_bound_project(
454
+ self, p: Project,
455
+ ) -> tuple[Project, bool]:
456
+ """Insert one immutable authority-bound Project, or replay it exactly.
457
+
458
+ Unlike :meth:`save_project`, this operation never upserts. A replay
459
+ returns the persisted lifecycle row byte-for-byte as reconstructed by
460
+ ``Project.from_row``; it cannot rewind status, rewrite timestamps, or
461
+ widen any WorkOrder authority field.
462
+ """
463
+
464
+ if p.source not in self._AUTHORITY_BOUND_PROJECT_SOURCES:
465
+ raise ValueError("exact project insert requires an authority-bound source")
466
+ row = p.to_row()
467
+ with self._lock:
468
+ self._conn.execute("BEGIN IMMEDIATE")
469
+ try:
470
+ existing_row = self._conn.execute(
471
+ "SELECT * FROM projects WHERE id=?", (p.id,),
472
+ ).fetchone()
473
+ if existing_row is not None:
474
+ existing = Project.from_row(dict(existing_row))
475
+ if any(
476
+ getattr(existing, field) != getattr(p, field)
477
+ for field in self._WORK_ORDER_PROJECT_AUTHORITY_FIELDS
478
+ ):
479
+ raise ValueError(
480
+ "immutable authority-bound project replay mismatch"
481
+ )
482
+ self._conn.commit()
483
+ return existing, False
484
+ if (
485
+ p.status != "planning"
486
+ or p.outcome != "pending"
487
+ or p.reason
488
+ or p.replans != 0
489
+ or p.next_review_at != 0.0
490
+ ):
491
+ raise ValueError(
492
+ "exact project insert requires an initial planning lifecycle"
493
+ )
494
+ cols = ", ".join(row)
495
+ placeholders = ", ".join(["?"] * len(row))
496
+ self._conn.execute(
497
+ f"INSERT INTO projects ({cols}) VALUES ({placeholders})",
498
+ list(row.values()),
499
+ )
500
+ self._conn.commit()
501
+ return Project.from_row(row), True
502
+ except Exception:
503
+ self._conn.rollback()
504
+ raise
505
+
506
+ def save_project(self, p: Project) -> Project:
507
+ p.updated_at = time.time()
508
+ row = p.to_row()
509
+ with self._lock:
510
+ self._conn.execute("BEGIN IMMEDIATE")
511
+ try:
512
+ existing_row = self._conn.execute(
513
+ "SELECT * FROM projects WHERE id=?", (p.id,),
514
+ ).fetchone()
515
+ if existing_row is not None:
516
+ existing = Project.from_row(dict(existing_row))
517
+ bound_sources = self._AUTHORITY_BOUND_PROJECT_SOURCES
518
+ if existing.source in bound_sources or p.source in bound_sources:
519
+ if any(getattr(existing, field) != getattr(p, field)
520
+ for field in self._AUTHORITY_BOUND_PROJECT_IMMUTABLE_FIELDS):
521
+ if (
522
+ existing.source == "cognition_spine"
523
+ and p.source == "cognition_spine"
524
+ ):
525
+ raise ValueError(
526
+ "immutable cognition project provenance mismatch"
527
+ )
528
+ raise ValueError(
529
+ "immutable authority-bound project provenance mismatch"
530
+ )
531
+ cols = ", ".join(row); ph = ", ".join(["?"] * len(row))
532
+ updates = ", ".join(
533
+ f"{k}=excluded.{k}" for k in row if k != "id"
534
+ )
535
+ self._conn.execute(
536
+ f"INSERT INTO projects ({cols}) VALUES ({ph}) "
537
+ f"ON CONFLICT(id) DO UPDATE SET {updates}", list(row.values()))
538
+ self._stage_project_terminal_locked(p)
539
+ self._conn.commit()
540
+ except Exception:
541
+ self._conn.rollback()
542
+ raise
543
+ return p
544
+
545
+ def get_project(self, project_id: str) -> Optional[Project]:
546
+ with self._lock:
547
+ r = self._conn.execute(
548
+ "SELECT * FROM projects WHERE id=?", (project_id,)).fetchone()
549
+ return Project.from_row(dict(r)) if r else None
550
+
551
+ def list_projects(self, status: Optional[str] = None,
552
+ limit: int = 50, offset: int = 0) -> List[Project]:
553
+ q = "SELECT * FROM projects"
554
+ params: List[Any] = []
555
+ if status:
556
+ q += " WHERE status=?"; params.append(status)
557
+ q += " ORDER BY created_at DESC LIMIT ? OFFSET ?"
558
+ params.extend((limit, max(0, int(offset))))
559
+ with self._lock:
560
+ rows = self._conn.execute(q, params).fetchall()
561
+ return [Project.from_row(dict(r)) for r in rows]
562
+
563
+ def count(self, status: Optional[str] = None) -> int:
564
+ q = "SELECT COUNT(*) AS n FROM projects"
565
+ params: List[Any] = []
566
+ if status:
567
+ q += " WHERE status=?"; params.append(status)
568
+ with self._lock:
569
+ r = self._conn.execute(q, params).fetchone()
570
+ return int(r["n"])
571
+
572
+ def find_by_goal_fingerprint(self, fingerprint: str) -> Optional[Project]:
573
+ """Find a non-terminal project for one normalized autonomous goal."""
574
+
575
+ if not str(fingerprint or "").strip():
576
+ return None
577
+ with self._lock:
578
+ row = self._conn.execute(
579
+ """SELECT * FROM projects WHERE goal_fingerprint=?
580
+ AND status NOT IN ('completed','abandoned')
581
+ ORDER BY created_at ASC LIMIT 1""",
582
+ (str(fingerprint),),
583
+ ).fetchone()
584
+ return Project.from_row(dict(row)) if row else None
585
+
586
+ def find_by_source_event_refs(
587
+ self, references: List[str],
588
+ ) -> Optional[Project]:
589
+ """Find the first Project already bound to any exact source event.
590
+
591
+ A transport-attested owner goal may create its Project before the
592
+ ordinary cognition reducer reaches the same journal event. Matching
593
+ the immutable reference prevents that later model pass from creating a
594
+ second Project with different wording and therefore a different goal
595
+ fingerprint.
596
+ """
597
+
598
+ wanted = {
599
+ str(reference) for reference in references
600
+ if str(reference or "").strip()
601
+ }
602
+ if not wanted:
603
+ return None
604
+ with self._lock:
605
+ rows = self._conn.execute(
606
+ "SELECT * FROM projects ORDER BY created_at ASC,id ASC"
607
+ ).fetchall()
608
+ for row in rows:
609
+ project = Project.from_row(dict(row))
610
+ if wanted.intersection(project.source_event_refs):
611
+ return project
612
+ return None
613
+
614
+ def due_for_review(self, now: Optional[float] = None,
615
+ limit: int = 20, offset: int = 0) -> List[Project]:
616
+ """Active projects whose next_review_at has passed."""
617
+ now = now or time.time()
618
+ with self._lock:
619
+ rows = self._conn.execute(
620
+ "SELECT * FROM projects WHERE status='active' AND "
621
+ "coalesce(next_review_at, 0) <= ? "
622
+ "ORDER BY next_review_at ASC LIMIT ? OFFSET ?",
623
+ (now, limit, max(0, int(offset)))).fetchall()
624
+ return [Project.from_row(dict(r)) for r in rows]
625
+
626
+ # -- steps --------------------------------------------------------------
627
+ def save_step(self, s: Step) -> Step:
628
+ s.updated_at = time.time()
629
+ row = s.to_row()
630
+ with self._lock:
631
+ cols = ", ".join(row); ph = ", ".join(["?"] * len(row))
632
+ updates = ", ".join(f"{k}=excluded.{k}" for k in row if k != "id")
633
+ self._conn.execute(
634
+ f"INSERT INTO steps ({cols}) VALUES ({ph}) "
635
+ f"ON CONFLICT(id) DO UPDATE SET {updates}", list(row.values()))
636
+ self._conn.commit()
637
+ return s
638
+
639
+ def steps_for(self, project_id: str) -> List[Step]:
640
+ with self._lock:
641
+ rows = self._conn.execute(
642
+ "SELECT * FROM steps WHERE project_id=? "
643
+ "ORDER BY ordinal ASC,id ASC",
644
+ (project_id,)).fetchall()
645
+ return [Step.from_row(dict(r)) for r in rows]
646
+
647
+ def delete_steps(self, project_id: str,
648
+ statuses: Optional[List[str]] = None) -> int:
649
+ """Delete steps of a project (optionally only certain statuses)."""
650
+ q = "DELETE FROM steps WHERE project_id=?"
651
+ params: List[Any] = [project_id]
652
+ if statuses:
653
+ q += f" AND status IN ({','.join(['?'] * len(statuses))})"
654
+ params.extend(statuses)
655
+ with self._lock:
656
+ cur = self._conn.execute(q, params)
657
+ self._conn.commit()
658
+ return cur.rowcount
659
+
660
+ # -- immutable WorkOrder / execution-result ledger --------------------
661
+ def prepare_work_order(self, project: Project, step: Step, order: Any) -> str:
662
+ """Insert missing parents and bind a WorkOrder without stale upserts.
663
+
664
+ Queue polling may hold an older in-memory Project/Step. This method
665
+ never overwrites an existing lifecycle row: it validates authority
666
+ fields and terminal state, creates the immutable WorkOrder, and binds
667
+ its reference to the step in one SQLite transaction.
668
+ """
669
+
670
+ order.validate()
671
+ if step.project_id != project.id or (
672
+ order.project_id != project.id or order.step_id != step.id
673
+ ):
674
+ raise ValueError("WorkOrder parent identity mismatch")
675
+ if project.status in {"completed", "abandoned"} or step.status in {
676
+ "done", "failed", "skipped",
677
+ }:
678
+ raise ValueError("cannot prepare work for a terminal project step")
679
+ ref = f"work-order:{order.work_order_id}"
680
+ payload_json = self._json(order.payload())
681
+ project_row = project.to_row()
682
+ step_row = step.to_row()
683
+ issued_at = datetime.fromisoformat(
684
+ str(order.issued_at).replace("Z", "+00:00")
685
+ ).timestamp()
686
+
687
+ with self._lock:
688
+ self._conn.execute("BEGIN IMMEDIATE")
689
+ try:
690
+ existing_project_row = self._conn.execute(
691
+ "SELECT * FROM projects WHERE id=?", (project.id,),
692
+ ).fetchone()
693
+ if existing_project_row is None:
694
+ cols = ", ".join(project_row)
695
+ ph = ", ".join(["?"] * len(project_row))
696
+ self._conn.execute(
697
+ f"INSERT INTO projects ({cols}) VALUES ({ph})",
698
+ list(project_row.values()),
699
+ )
700
+ else:
701
+ existing_project = Project.from_row(
702
+ dict(existing_project_row)
703
+ )
704
+ if existing_project.status in {"completed", "abandoned"}:
705
+ raise ValueError("persisted WorkOrder project is terminal")
706
+ if any(
707
+ getattr(existing_project, field)
708
+ != getattr(project, field)
709
+ for field in self._WORK_ORDER_PROJECT_AUTHORITY_FIELDS
710
+ ) or (
711
+ existing_project.status != project.status
712
+ or existing_project.outcome != project.outcome
713
+ ):
714
+ raise ValueError(
715
+ "stale WorkOrder project differs from persisted parent"
716
+ )
717
+
718
+ existing_step_row = self._conn.execute(
719
+ "SELECT * FROM steps WHERE id=?", (step.id,),
720
+ ).fetchone()
721
+ if existing_step_row is None:
722
+ cols = ", ".join(step_row)
723
+ ph = ", ".join(["?"] * len(step_row))
724
+ self._conn.execute(
725
+ f"INSERT INTO steps ({cols}) VALUES ({ph})",
726
+ list(step_row.values()),
727
+ )
728
+ existing_step = step
729
+ else:
730
+ existing_step = Step.from_row(dict(existing_step_row))
731
+ if existing_step.status in {"done", "failed", "skipped"}:
732
+ raise ValueError("persisted WorkOrder step is terminal")
733
+ step_authority = (
734
+ "project_id", "ordinal", "description", "action_kind",
735
+ "depends_on", "boundary_subject", "confidence",
736
+ )
737
+ if any(
738
+ getattr(existing_step, field) != getattr(step, field)
739
+ for field in step_authority
740
+ ) or existing_step.status != step.status:
741
+ raise ValueError(
742
+ "stale WorkOrder step differs from persisted parent"
743
+ )
744
+ stored_issued = float(
745
+ existing_step.work_order_issued_at or 0.0
746
+ )
747
+ if stored_issued and abs(stored_issued - issued_at) > 0.001:
748
+ raise ValueError("persisted WorkOrder issue time mismatch")
749
+
750
+ existing_order = self._conn.execute(
751
+ "SELECT * FROM project_work_orders WHERE work_order_id=?",
752
+ (order.work_order_id,),
753
+ ).fetchone()
754
+ if existing_order is not None:
755
+ row = dict(existing_order)
756
+ if (
757
+ row["work_order_digest"] != order.work_order_digest
758
+ or row["payload_json"] != payload_json
759
+ or row["work_order_ref"] != ref
760
+ ):
761
+ raise ValueError("immutable WorkOrder replay mismatch")
762
+ else:
763
+ self._conn.execute(
764
+ """INSERT INTO project_work_orders (
765
+ work_order_ref, work_order_id, work_order_digest,
766
+ project_id, step_id, schema_version, payload_json,
767
+ created_at
768
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
769
+ (
770
+ ref, order.work_order_id, order.work_order_digest,
771
+ order.project_id, order.step_id, int(order.version),
772
+ payload_json, time.time(),
773
+ ),
774
+ )
775
+
776
+ if (
777
+ existing_step.work_order_ref
778
+ and existing_step.work_order_ref != ref
779
+ ) or (
780
+ existing_step.work_order_digest
781
+ and existing_step.work_order_digest
782
+ != order.work_order_digest
783
+ ):
784
+ raise ValueError("step WorkOrder authority replay mismatch")
785
+ changed = self._conn.execute(
786
+ """UPDATE steps SET work_order_ref=?,work_order_digest=?,
787
+ work_order_issued_at=?,updated_at=?
788
+ WHERE id=? AND status NOT IN ('done','failed','skipped')""",
789
+ (
790
+ ref, order.work_order_digest, issued_at, time.time(),
791
+ step.id,
792
+ ),
793
+ ).rowcount
794
+ if changed != 1:
795
+ raise ValueError("persisted WorkOrder step became terminal")
796
+ self._conn.commit()
797
+ except Exception:
798
+ self._conn.rollback()
799
+ raise
800
+ return ref
801
+
802
+ def save_work_order(self, order: Any) -> str:
803
+ """Persist one immutable authority envelope and return its ledger ref."""
804
+
805
+ order.validate()
806
+ ref = f"work-order:{order.work_order_id}"
807
+ payload_json = self._json(order.payload())
808
+ with self._lock:
809
+ existing = self._conn.execute(
810
+ "SELECT * FROM project_work_orders WHERE work_order_id=?",
811
+ (order.work_order_id,),
812
+ ).fetchone()
813
+ if existing:
814
+ row = dict(existing)
815
+ if (
816
+ row["work_order_digest"] != order.work_order_digest
817
+ or row["payload_json"] != payload_json
818
+ or row["work_order_ref"] != ref
819
+ ):
820
+ raise ValueError("immutable WorkOrder replay mismatch")
821
+ return ref
822
+ self._conn.execute(
823
+ """INSERT INTO project_work_orders (
824
+ work_order_ref, work_order_id, work_order_digest,
825
+ project_id, step_id, schema_version, payload_json, created_at
826
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
827
+ (
828
+ ref, order.work_order_id, order.work_order_digest,
829
+ order.project_id, order.step_id, int(order.version),
830
+ payload_json, time.time(),
831
+ ),
832
+ )
833
+ self._conn.commit()
834
+ return ref
835
+
836
+ def get_work_order(self, ref_or_id: str) -> Optional[Dict[str, Any]]:
837
+ with self._lock:
838
+ row = self._conn.execute(
839
+ """SELECT * FROM project_work_orders
840
+ WHERE work_order_ref=? OR work_order_id=?""",
841
+ (ref_or_id, ref_or_id),
842
+ ).fetchone()
843
+ if not row:
844
+ return None
845
+ result = dict(row)
846
+ result["payload"] = json.loads(result.pop("payload_json"))
847
+ return result
848
+
849
+ def work_orders_awaiting_reconciliation(
850
+ self, *, limit: int = 25,
851
+ ) -> List[Dict[str, Any]]:
852
+ """Return bounded, non-terminal WorkOrders that may need polling.
853
+
854
+ Queue execution and Project pursuit are separate durable lifecycles.
855
+ A Project can become blocked (or ordinary pursuit can be cancelled)
856
+ after its WorkOrder has already reached a terminal queue state. Such
857
+ a result is still an observed fact and must be projected into the
858
+ execution ledger independently of whether the Project is currently
859
+ eligible to advance.
860
+
861
+ This query deliberately includes blocked Projects, but excludes
862
+ terminal Project/Step parents because ``save_execution_result`` must
863
+ never reopen those lifecycles. Rows without a result are ordered
864
+ first so idempotent re-polls cannot starve fresh terminal outcomes.
865
+ """
866
+
867
+ bound = max(1, min(100, int(limit)))
868
+ with self._lock:
869
+ rows = self._conn.execute(
870
+ """SELECT wo.*
871
+ FROM project_work_orders wo
872
+ JOIN projects p ON p.id=wo.project_id
873
+ JOIN steps s ON s.id=wo.step_id
874
+ AND s.project_id=wo.project_id
875
+ WHERE p.status NOT IN ('completed','abandoned')
876
+ AND s.status NOT IN ('done','failed','skipped')
877
+ ORDER BY CASE WHEN coalesce(s.result_ref, '') = ''
878
+ THEN 0 ELSE 1 END,
879
+ wo.created_at ASC,wo.work_order_id ASC
880
+ LIMIT ?""",
881
+ (bound,),
882
+ ).fetchall()
883
+ results: List[Dict[str, Any]] = []
884
+ for row in rows:
885
+ item = dict(row)
886
+ raw_payload = item.pop("payload_json")
887
+ try:
888
+ item["payload"] = json.loads(raw_payload)
889
+ except (TypeError, ValueError, json.JSONDecodeError):
890
+ # Keep malformed authority addressable so one corrupt row is
891
+ # reported without starving valid rows later in the batch.
892
+ item["payload"] = None
893
+ results.append(item)
894
+ return results
895
+
896
+ def save_execution_result(
897
+ self,
898
+ order: Any,
899
+ result: Any,
900
+ *,
901
+ transport_status: str,
902
+ ) -> str:
903
+ """Append a run attempt and update one stable logical result row.
904
+
905
+ Duplicate delivery of the same run is idempotent. Reusing a run ID
906
+ or attempt number for different content is rejected. Older attempts
907
+ can never overwrite the latest logical result.
908
+ """
909
+
910
+ order.validate()
911
+ from apsimo.execution_results import (
912
+ EFFECT_CLASSES,
913
+ EXECUTION_RESULT_VERSION,
914
+ TERMINAL_OUTCOMES,
915
+ VERIFICATION_RESULTS,
916
+ bounded_refs,
917
+ )
918
+
919
+ if (
920
+ result.schema != "ExecutionResultV1"
921
+ or type(result.version) is not int
922
+ or result.version != EXECUTION_RESULT_VERSION
923
+ ):
924
+ raise ValueError("execution result schema/version is invalid")
925
+ if result.work_order_id != order.work_order_id:
926
+ raise ValueError("execution result WorkOrder ID mismatch")
927
+ if result.work_order_digest != order.work_order_digest:
928
+ raise ValueError("execution result WorkOrder digest mismatch")
929
+ if int(result.work_order_version) != int(order.version):
930
+ raise ValueError("execution result WorkOrder version mismatch")
931
+ if result.result_ref != f"execution-result:{order.work_order_id}":
932
+ raise ValueError("execution result reference mismatch")
933
+ if (
934
+ type(result.attempt_number) is not int
935
+ or result.attempt_number < 1
936
+ or result.attempt_number > int(order.max_attempts)
937
+ ):
938
+ raise ValueError("execution attempt exceeds WorkOrder budget")
939
+ if result.terminal_outcome not in TERMINAL_OUTCOMES:
940
+ raise ValueError("execution result terminal outcome is invalid")
941
+ if (
942
+ result.effect_class not in EFFECT_CLASSES
943
+ or result.effect_class != order.effect_class
944
+ ):
945
+ raise ValueError("execution result effect class mismatch")
946
+ if result.verification_result not in VERIFICATION_RESULTS:
947
+ raise ValueError("execution result verification state is invalid")
948
+ receipts = bounded_refs(result.receipt_refs)
949
+ if receipts != tuple(result.receipt_refs):
950
+ raise ValueError("execution result receipt refs are not canonical")
951
+ if result.terminal_outcome == "succeeded":
952
+ if result.verification_result == "not_applicable":
953
+ raise ValueError("succeeded execution requires verification state")
954
+ if result.verification_result == "verified" and (
955
+ not receipts
956
+ or not str(result.verifier_identity or "").strip()
957
+ or result.verifier_identity == result.executor_identity
958
+ ):
959
+ raise ValueError(
960
+ "verified execution requires independent receipt evidence"
961
+ )
962
+ elif result.verification_result != "not_applicable":
963
+ raise ValueError(
964
+ "non-success execution cannot claim success verification"
965
+ )
966
+ for value, field, maximum in (
967
+ (result.run_id, "run_id", 256),
968
+ (result.executor_identity, "executor_identity", 256),
969
+ (result.verifier_identity, "verifier_identity", 256),
970
+ (result.summary, "summary", 4000),
971
+ (result.error, "error", 2000),
972
+ ):
973
+ text = str(value or "").strip()
974
+ if len(text) > maximum or (
975
+ field in {"run_id", "executor_identity"} and not text
976
+ ):
977
+ raise ValueError(f"execution result {field} is invalid")
978
+ try:
979
+ started_at = datetime.fromisoformat(
980
+ str(result.started_at).replace("Z", "+00:00")
981
+ )
982
+ ended_at = datetime.fromisoformat(
983
+ str(result.ended_at).replace("Z", "+00:00")
984
+ )
985
+ except (TypeError, ValueError) as exc:
986
+ raise ValueError("execution result timestamps are invalid") from exc
987
+ if (
988
+ started_at.tzinfo is None
989
+ or ended_at.tzinfo is None
990
+ or ended_at < started_at
991
+ ):
992
+ raise ValueError("execution result timestamps are invalid")
993
+ transport = str(transport_status or "").strip().lower()
994
+ if transport not in {"completed", "neutral", "failed", "cancelled"}:
995
+ raise ValueError("execution result transport status is invalid")
996
+ if transport == "failed" and result.terminal_outcome != "failed":
997
+ raise ValueError("execution result conflicts with failed transport")
998
+ if transport == "cancelled" and result.terminal_outcome != "cancelled":
999
+ raise ValueError("execution result conflicts with cancelled transport")
1000
+
1001
+ work_order_ref = f"work-order:{order.work_order_id}"
1002
+ payload_json = self._json(result.payload())
1003
+ attempt_ref = "execution-attempt:%s" % hashlib.sha256(
1004
+ f"{order.work_order_id}\0{result.run_id}".encode("utf-8")
1005
+ ).hexdigest()[:24]
1006
+ now = time.time()
1007
+
1008
+ with self._lock:
1009
+ self._conn.execute("BEGIN IMMEDIATE")
1010
+ try:
1011
+ work_order = self._conn.execute(
1012
+ """SELECT wo.work_order_digest,wo.project_id,wo.step_id,
1013
+ p.status AS project_status,
1014
+ s.status AS step_status
1015
+ FROM project_work_orders wo
1016
+ JOIN projects p ON p.id=wo.project_id
1017
+ JOIN steps s ON s.id=wo.step_id
1018
+ AND s.project_id=wo.project_id
1019
+ WHERE wo.work_order_ref=?""",
1020
+ (work_order_ref,),
1021
+ ).fetchone()
1022
+ if (
1023
+ not work_order
1024
+ or work_order["work_order_digest"] != order.work_order_digest
1025
+ or work_order["project_id"] != order.project_id
1026
+ or work_order["step_id"] != order.step_id
1027
+ ):
1028
+ raise ValueError("execution result has no matching WorkOrder ledger row")
1029
+
1030
+ existing_attempt = self._conn.execute(
1031
+ """SELECT * FROM project_execution_attempts
1032
+ WHERE work_order_id=? AND (run_id=? OR attempt_number=?)""",
1033
+ (order.work_order_id, result.run_id, int(result.attempt_number)),
1034
+ ).fetchone()
1035
+ if existing_attempt:
1036
+ row = dict(existing_attempt)
1037
+ if (
1038
+ row["run_id"] != result.run_id
1039
+ or int(row["attempt_number"]) != int(result.attempt_number)
1040
+ or row["transport_status"] != transport
1041
+ or row["result_json"] != payload_json
1042
+ ):
1043
+ raise ValueError("execution attempt replay mismatch")
1044
+ else:
1045
+ if (
1046
+ work_order["project_status"] in {
1047
+ "completed", "abandoned",
1048
+ }
1049
+ or work_order["step_status"] in {
1050
+ "done", "failed", "skipped",
1051
+ }
1052
+ ):
1053
+ raise ValueError(
1054
+ "new execution attempt cannot mutate a terminal "
1055
+ "project step"
1056
+ )
1057
+ self._conn.execute(
1058
+ """INSERT INTO project_execution_attempts (
1059
+ attempt_ref, work_order_ref, work_order_id, run_id,
1060
+ attempt_number, transport_status, result_json, created_at
1061
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
1062
+ (
1063
+ attempt_ref, work_order_ref, order.work_order_id,
1064
+ result.run_id, int(result.attempt_number),
1065
+ transport, payload_json, now,
1066
+ ),
1067
+ )
1068
+
1069
+ logical = self._conn.execute(
1070
+ "SELECT * FROM project_execution_results WHERE result_ref=?",
1071
+ (result.result_ref,),
1072
+ ).fetchone()
1073
+ if logical is None:
1074
+ self._conn.execute(
1075
+ """INSERT INTO project_execution_results (
1076
+ result_ref, work_order_ref, work_order_id,
1077
+ work_order_digest, run_id, attempt_number,
1078
+ terminal_outcome, verification_result, effect_class,
1079
+ payload_json, created_at, updated_at
1080
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
1081
+ (
1082
+ result.result_ref, work_order_ref, order.work_order_id,
1083
+ order.work_order_digest, result.run_id,
1084
+ int(result.attempt_number), result.terminal_outcome,
1085
+ result.verification_result, result.effect_class,
1086
+ payload_json, now, now,
1087
+ ),
1088
+ )
1089
+ else:
1090
+ current = dict(logical)
1091
+ current_attempt = int(current["attempt_number"])
1092
+ if int(result.attempt_number) == current_attempt:
1093
+ if current["run_id"] != result.run_id or current["payload_json"] != payload_json:
1094
+ raise ValueError("logical execution result replay mismatch")
1095
+ elif int(result.attempt_number) > current_attempt:
1096
+ self._conn.execute(
1097
+ """UPDATE project_execution_results SET
1098
+ run_id=?, attempt_number=?, terminal_outcome=?,
1099
+ verification_result=?, effect_class=?,
1100
+ payload_json=?, updated_at=? WHERE result_ref=?""",
1101
+ (
1102
+ result.run_id, int(result.attempt_number),
1103
+ result.terminal_outcome, result.verification_result,
1104
+ result.effect_class, payload_json, now,
1105
+ result.result_ref,
1106
+ ),
1107
+ )
1108
+ self._conn.execute(
1109
+ """UPDATE steps SET result_ref=?, work_order_ref=?,
1110
+ work_order_digest=?, updated_at=? WHERE id=?""",
1111
+ (
1112
+ result.result_ref, work_order_ref,
1113
+ order.work_order_digest, now, order.step_id,
1114
+ ),
1115
+ )
1116
+ project_row = self._conn.execute(
1117
+ "SELECT subject_person_id,viewer_scope,shareability "
1118
+ "FROM projects WHERE id=?",
1119
+ (order.project_id,),
1120
+ ).fetchone()
1121
+ result_payload = result.payload()
1122
+ status = (
1123
+ "verified"
1124
+ if result.terminal_outcome == "succeeded"
1125
+ and result.verification_result == "verified"
1126
+ and bool(result.receipt_refs)
1127
+ else result.terminal_outcome
1128
+ if result.terminal_outcome in {
1129
+ "failed", "cancelled", "skipped",
1130
+ }
1131
+ else "unverified"
1132
+ )
1133
+ identity = self._digest({
1134
+ "work_order_id": order.work_order_id,
1135
+ "run_id": result.run_id,
1136
+ })
1137
+ event_key = f"project-execution:{identity}"
1138
+ mode_at_stage = self._cognition_mode_for_event_locked(event_key)
1139
+ evidence = {
1140
+ "schema": "ProjectExecutionEvidenceV2",
1141
+ "version": 2,
1142
+ "project_id": order.project_id,
1143
+ "step_id": order.step_id,
1144
+ "work_order_id": order.work_order_id,
1145
+ "work_order_digest": order.work_order_digest,
1146
+ "result_ref": result.result_ref,
1147
+ "run_id": result.run_id,
1148
+ "attempt_number": int(result.attempt_number),
1149
+ "terminal_outcome": result.terminal_outcome,
1150
+ "verification_result": result.verification_result,
1151
+ "effect_class": result.effect_class,
1152
+ "transport_status": transport,
1153
+ "status": status,
1154
+ "result_digest": self._digest(result_payload),
1155
+ "receipt_refs": list(result.receipt_refs),
1156
+ "verifier_identity": str(result.verifier_identity or "")[:256],
1157
+ "subject_person_id": (
1158
+ str(project_row["subject_person_id"] or "owner")
1159
+ if project_row is not None else "owner"
1160
+ ),
1161
+ "viewer_scope": (
1162
+ str(project_row["viewer_scope"] or "owner")
1163
+ if project_row is not None else "owner"
1164
+ ),
1165
+ "shareability": (
1166
+ str(project_row["shareability"] or "owner_private")
1167
+ if project_row is not None else "owner_private"
1168
+ ),
1169
+ }
1170
+ if mode_at_stage is not None:
1171
+ evidence["cognition_mode_at_stage"] = mode_at_stage
1172
+ evidence["evidence_digest"] = self._digest(evidence)
1173
+ self._stage_event_locked(
1174
+ event_key=event_key,
1175
+ event_type="work_order.result",
1176
+ payload=evidence,
1177
+ occurred_at=result.ended_at,
1178
+ )
1179
+ self._conn.commit()
1180
+ except Exception:
1181
+ self._conn.rollback()
1182
+ raise
1183
+ return result.result_ref
1184
+
1185
+ def get_execution_result(self, ref_or_work_order_id: str) -> Optional[Dict[str, Any]]:
1186
+ with self._lock:
1187
+ row = self._conn.execute(
1188
+ """SELECT * FROM project_execution_results
1189
+ WHERE result_ref=? OR work_order_id=?""",
1190
+ (ref_or_work_order_id, ref_or_work_order_id),
1191
+ ).fetchone()
1192
+ if not row:
1193
+ return None
1194
+ result = dict(row)
1195
+ result["payload"] = json.loads(result.pop("payload_json"))
1196
+ return result
1197
+
1198
+ def execution_attempts_for(self, work_order_id: str) -> List[Dict[str, Any]]:
1199
+ with self._lock:
1200
+ rows = self._conn.execute(
1201
+ """SELECT * FROM project_execution_attempts
1202
+ WHERE work_order_id=? ORDER BY attempt_number ASC""",
1203
+ (work_order_id,),
1204
+ ).fetchall()
1205
+ results = []
1206
+ for row in rows:
1207
+ item = dict(row)
1208
+ item["result"] = json.loads(item.pop("result_json"))
1209
+ results.append(item)
1210
+ return results
1211
+
1212
+ # -- crash-recoverable host-journal outbox ---------------------------
1213
+ def pending_project_events(self, *, limit: int = 100) -> List[Dict[str, Any]]:
1214
+ bound = max(1, min(500, int(limit)))
1215
+ with self._lock:
1216
+ rows = self._conn.execute(
1217
+ "SELECT * FROM project_event_outbox WHERE state='pending' "
1218
+ "ORDER BY created_at ASC,event_key ASC LIMIT ?",
1219
+ (bound,),
1220
+ ).fetchall()
1221
+ events: List[Dict[str, Any]] = []
1222
+ for row in rows:
1223
+ item = dict(row)
1224
+ raw_payload = item.pop("payload_json")
1225
+ try:
1226
+ item["payload"] = json.loads(raw_payload)
1227
+ item["payload_error"] = ""
1228
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
1229
+ # Keep the row addressable so the projector can persist a
1230
+ # bounded last_error instead of crashing the entire drain.
1231
+ item["payload"] = None
1232
+ item["payload_error"] = f"invalid project outbox JSON:{exc}"
1233
+ events.append(item)
1234
+ return events
1235
+
1236
+ def unacknowledged_project_events(
1237
+ self, *, limit: int = 100,
1238
+ ) -> List[Dict[str, Any]]:
1239
+ bound = max(1, min(500, int(limit)))
1240
+ with self._lock:
1241
+ rows = self._conn.execute(
1242
+ "SELECT * FROM project_event_outbox WHERE state='projected' "
1243
+ "AND journal_acknowledged=0 "
1244
+ "ORDER BY updated_at ASC,event_key ASC LIMIT ?",
1245
+ (bound,),
1246
+ ).fetchall()
1247
+ events: List[Dict[str, Any]] = []
1248
+ for row in rows:
1249
+ item = dict(row)
1250
+ raw_payload = item.pop("payload_json")
1251
+ try:
1252
+ item["payload"] = json.loads(raw_payload)
1253
+ item["payload_error"] = ""
1254
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
1255
+ item["payload"] = None
1256
+ item["payload_error"] = f"invalid project outbox JSON:{exc}"
1257
+ events.append(item)
1258
+ return events
1259
+
1260
+ def complete_project_event(
1261
+ self,
1262
+ event_key: str,
1263
+ journal_record: Dict[str, Any],
1264
+ ) -> Dict[str, Any]:
1265
+ key = str(event_key or "").strip()
1266
+ raw_seq = journal_record.get("seq")
1267
+ raw_event_id = journal_record.get("ulid")
1268
+ raw_recorded_at = journal_record.get("recordedAt")
1269
+ if (
1270
+ not key
1271
+ or type(raw_seq) is not int
1272
+ or type(raw_event_id) is not str
1273
+ or type(raw_recorded_at) is not str
1274
+ ):
1275
+ raise ValueError("journal projection receipt is invalid")
1276
+ seq = raw_seq
1277
+ event_id = raw_event_id.strip()
1278
+ recorded_at = raw_recorded_at.strip()
1279
+ if event_id != raw_event_id or recorded_at != raw_recorded_at:
1280
+ raise ValueError("journal projection receipt is not canonical")
1281
+ with self._lock:
1282
+ self._conn.execute("BEGIN IMMEDIATE")
1283
+ try:
1284
+ row = self._conn.execute(
1285
+ "SELECT * FROM project_event_outbox WHERE event_key=?",
1286
+ (key,),
1287
+ ).fetchone()
1288
+ if row is None:
1289
+ raise ValueError("project event outbox row is unavailable")
1290
+ try:
1291
+ payload = json.loads(str(row["payload_json"]))
1292
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
1293
+ raise ValueError("project event outbox payload is invalid") from exc
1294
+ if not isinstance(payload, dict):
1295
+ raise ValueError("project event outbox payload is invalid")
1296
+ expected_envelope = self.project_event_envelope_digest(
1297
+ event_key=str(row["event_key"]),
1298
+ event_type=str(row["event_type"]),
1299
+ occurred_at=str(row["occurred_at"]),
1300
+ payload=payload,
1301
+ )
1302
+ if str(row["event_digest"] or "") != expected_envelope:
1303
+ raise ValueError("project event outbox envelope digest mismatch")
1304
+ receipt_digest = self.project_event_projection_receipt_digest(
1305
+ event_key=str(row["event_key"]),
1306
+ event_type=str(row["event_type"]),
1307
+ event_digest=expected_envelope,
1308
+ occurred_at=str(row["occurred_at"]),
1309
+ journal_seq=seq,
1310
+ journal_event_id=event_id,
1311
+ journal_recorded_at=recorded_at,
1312
+ )
1313
+ if row["state"] == "projected":
1314
+ if (
1315
+ int(row["journal_seq"] or 0) != seq
1316
+ or str(row["journal_event_id"] or "") != event_id
1317
+ or str(row["journal_recorded_at"] or "") != recorded_at
1318
+ or str(row["projection_receipt_digest"] or "")
1319
+ != receipt_digest
1320
+ ):
1321
+ raise ValueError("project event projection replay mismatch")
1322
+ else:
1323
+ self._conn.execute(
1324
+ """UPDATE project_event_outbox SET state='projected',
1325
+ journal_seq=?,journal_event_id=?,journal_recorded_at=?,
1326
+ projection_receipt_digest=?,
1327
+ projection_attempts=projection_attempts+1,
1328
+ last_error=NULL,updated_at=? WHERE event_key=?
1329
+ AND state='pending'""",
1330
+ (
1331
+ seq, event_id, recorded_at, receipt_digest,
1332
+ time.time(), key,
1333
+ ),
1334
+ )
1335
+ self._conn.commit()
1336
+ except Exception:
1337
+ self._conn.rollback()
1338
+ raise
1339
+ return self.project_event(key) or {}
1340
+
1341
+ def acknowledge_project_event(self, event_key: str) -> None:
1342
+ with self._lock:
1343
+ self._conn.execute("BEGIN IMMEDIATE")
1344
+ try:
1345
+ row = self._conn.execute(
1346
+ "SELECT * FROM project_event_outbox WHERE event_key=?",
1347
+ (str(event_key),),
1348
+ ).fetchone()
1349
+ if row is None or row["state"] != "projected":
1350
+ raise ValueError(
1351
+ "project event acknowledgement target is unavailable"
1352
+ )
1353
+ try:
1354
+ payload = json.loads(str(row["payload_json"]))
1355
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
1356
+ raise ValueError("project event outbox payload is invalid") from exc
1357
+ if not isinstance(payload, dict):
1358
+ raise ValueError("project event outbox payload is invalid")
1359
+ expected_envelope = self.project_event_envelope_digest(
1360
+ event_key=str(row["event_key"]),
1361
+ event_type=str(row["event_type"]),
1362
+ occurred_at=str(row["occurred_at"]),
1363
+ payload=payload,
1364
+ )
1365
+ if str(row["event_digest"] or "") != expected_envelope:
1366
+ raise ValueError("project event outbox envelope digest mismatch")
1367
+ expected_receipt = self.project_event_projection_receipt_digest(
1368
+ event_key=str(row["event_key"]),
1369
+ event_type=str(row["event_type"]),
1370
+ event_digest=expected_envelope,
1371
+ occurred_at=str(row["occurred_at"]),
1372
+ journal_seq=row["journal_seq"],
1373
+ journal_event_id=str(row["journal_event_id"] or ""),
1374
+ journal_recorded_at=str(row["journal_recorded_at"] or ""),
1375
+ )
1376
+ if (
1377
+ str(row["projection_receipt_digest"] or "")
1378
+ != expected_receipt
1379
+ ):
1380
+ raise ValueError(
1381
+ "project event projection receipt digest mismatch"
1382
+ )
1383
+ changed = self._conn.execute(
1384
+ """UPDATE project_event_outbox SET journal_acknowledged=1,
1385
+ last_error=NULL,updated_at=? WHERE event_key=?
1386
+ AND state='projected' AND projection_receipt_digest=?""",
1387
+ (time.time(), str(event_key), expected_receipt),
1388
+ ).rowcount
1389
+ if changed != 1:
1390
+ raise ValueError(
1391
+ "project event acknowledgement target is unavailable"
1392
+ )
1393
+ self._conn.commit()
1394
+ except Exception:
1395
+ self._conn.rollback()
1396
+ raise
1397
+
1398
+ def fail_project_event(self, event_key: str, error: str) -> None:
1399
+ with self._lock:
1400
+ self._conn.execute(
1401
+ """UPDATE project_event_outbox SET
1402
+ projection_attempts=projection_attempts+1,last_error=?,updated_at=?
1403
+ WHERE event_key=?""",
1404
+ (str(error or "projection_failed")[:500], time.time(), event_key),
1405
+ )
1406
+ self._conn.commit()
1407
+
1408
+ def project_event(self, event_key: str) -> Optional[Dict[str, Any]]:
1409
+ with self._lock:
1410
+ row = self._conn.execute(
1411
+ "SELECT * FROM project_event_outbox WHERE event_key=?",
1412
+ (str(event_key),),
1413
+ ).fetchone()
1414
+ if row is None:
1415
+ return None
1416
+ item = dict(row)
1417
+ raw_payload = item.pop("payload_json")
1418
+ try:
1419
+ item["payload"] = json.loads(raw_payload)
1420
+ item["payload_error"] = ""
1421
+ except (TypeError, ValueError, json.JSONDecodeError) as exc:
1422
+ item["payload"] = None
1423
+ item["payload_error"] = f"invalid project outbox JSON:{exc}"
1424
+ return item
1425
+
1426
+ def project_event_outbox_status(self) -> Dict[str, Any]:
1427
+ with self._lock:
1428
+ counts = self._conn.execute(
1429
+ "SELECT state,COUNT(*) AS n FROM project_event_outbox "
1430
+ "GROUP BY state"
1431
+ ).fetchall()
1432
+ error = self._conn.execute(
1433
+ "SELECT event_key,last_error FROM project_event_outbox "
1434
+ "WHERE last_error IS NOT NULL "
1435
+ "ORDER BY updated_at DESC LIMIT 1"
1436
+ ).fetchone()
1437
+ acknowledgement_pending = int(self._conn.execute(
1438
+ "SELECT COUNT(*) AS n FROM project_event_outbox "
1439
+ "WHERE state='projected' AND journal_acknowledged=0"
1440
+ ).fetchone()["n"])
1441
+ return {
1442
+ "counts": {row["state"]: int(row["n"]) for row in counts},
1443
+ "acknowledgement_pending": acknowledgement_pending,
1444
+ "last_error": str(error["last_error"] or "") if error else "",
1445
+ "last_error_event_key": str(error["event_key"] or "") if error else "",
1446
+ }