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,183 @@
1
+ """Offline, rollback-first competence reconciliation CLI.
2
+
3
+ Dry-run is the default and operates on a temporary SQLite backup, so merely
4
+ validating a legacy database cannot run schema migrations against it.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import hashlib
11
+ import json
12
+ import sqlite3
13
+ import sys
14
+ import tempfile
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional
18
+
19
+ from apsimo.self_model.store import CompetenceStore, event_fingerprint
20
+
21
+
22
+ def _parse_ts(value: str) -> float:
23
+ try:
24
+ return float(value)
25
+ except ValueError:
26
+ dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
27
+ if dt.tzinfo is None:
28
+ dt = dt.replace(tzinfo=timezone.utc)
29
+ return dt.timestamp()
30
+
31
+
32
+ def sqlite_backup(source: Path, destination: Path) -> str:
33
+ """Create one consistent SQLite backup; never overwrite a prior backup."""
34
+ if not source.is_file():
35
+ raise ValueError(f"database does not exist: {source}")
36
+ if destination.exists():
37
+ raise ValueError(f"backup already exists: {destination}")
38
+ destination.parent.mkdir(parents=True, exist_ok=True)
39
+ src = sqlite3.connect(f"file:{source}?mode=ro", uri=True)
40
+ dst = sqlite3.connect(str(destination))
41
+ try:
42
+ src.backup(dst)
43
+ finally:
44
+ dst.close()
45
+ src.close()
46
+ digest = hashlib.sha256()
47
+ with destination.open("rb") as handle:
48
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
49
+ digest.update(chunk)
50
+ return digest.hexdigest()
51
+
52
+
53
+ def inspect_database(db_path: Path, domain: str, since: float,
54
+ until: float) -> Dict[str, Any]:
55
+ """Read legacy or current raw events without migrating the source DB."""
56
+ conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
57
+ conn.row_factory = sqlite3.Row
58
+ try:
59
+ rows = [dict(r) for r in conn.execute(
60
+ "SELECT * FROM competence_events WHERE domain=? "
61
+ "AND ts>=? AND ts<? ORDER BY ts ASC, id ASC",
62
+ ((domain or "").strip().lower(), since, until)).fetchall()]
63
+ try:
64
+ ledger_count = int(conn.execute(
65
+ "SELECT COUNT(*) FROM competence_reconciliations"
66
+ ).fetchone()[0])
67
+ except sqlite3.OperationalError:
68
+ ledger_count = 0
69
+ finally:
70
+ conn.close()
71
+ candidates: List[Dict[str, Any]] = []
72
+ for row in rows:
73
+ candidates.append({
74
+ "event_id": int(row["id"]),
75
+ "target_fingerprint": event_fingerprint(row),
76
+ "domain": row["domain"],
77
+ "recorded_outcome": row["outcome"],
78
+ "shadow": bool(row.get("shadow")),
79
+ "violation": bool(row.get("violation")),
80
+ "stated_confidence": row.get("stated_confidence"),
81
+ "source": row.get("source") or "legacy_unattributed",
82
+ "source_ref": row.get("source_ref"),
83
+ "evidence_status": (
84
+ row.get("evidence_status") or "legacy_unattributed"),
85
+ "outcome_contract": (
86
+ row.get("outcome_contract") or "legacy.unversioned"),
87
+ "ts": float(row["ts"]),
88
+ })
89
+ return {
90
+ "database": str(db_path), "domain": domain,
91
+ "since_ts": since, "until_ts": until,
92
+ "candidate_count": len(candidates), "candidates": candidates,
93
+ "ledger_entries": ledger_count, "mutated": False,
94
+ }
95
+
96
+
97
+ def _load_manifest(path: Path) -> Dict[str, Any]:
98
+ value = json.loads(path.read_text(encoding="utf-8"))
99
+ if not isinstance(value, dict):
100
+ raise ValueError("manifest root must be an object")
101
+ return value
102
+
103
+
104
+ def dry_run(db_path: Path, manifest: Dict[str, Any]) -> Dict[str, Any]:
105
+ """Validate against a consistent disposable copy of the source DB."""
106
+ with tempfile.TemporaryDirectory(prefix="colony-competence-dryrun-") as td:
107
+ copy_path = Path(td) / "self-model.db"
108
+ sqlite_backup(db_path, copy_path)
109
+ store = CompetenceStore(str(copy_path))
110
+ try:
111
+ result = store.plan_reconciliation(manifest)
112
+ finally:
113
+ store.close()
114
+ return {**result, "mode": "dry-run", "database_mutated": False}
115
+
116
+
117
+ def commit(db_path: Path, manifest: Dict[str, Any],
118
+ backup_path: Path) -> Dict[str, Any]:
119
+ """Back up first, then atomically append the manifest."""
120
+ # Validate on a disposable consistent copy before opening (and therefore
121
+ # migrating) the source database.
122
+ dry_run(db_path, manifest)
123
+ backup_sha256 = sqlite_backup(db_path, backup_path)
124
+ store = CompetenceStore(str(db_path))
125
+ try:
126
+ result = store.apply_reconciliation(manifest)
127
+ finally:
128
+ store.close()
129
+ return {**result, "mode": "commit", "database_mutated": True,
130
+ "backup": str(backup_path), "backup_sha256": backup_sha256}
131
+
132
+
133
+ def build_parser() -> argparse.ArgumentParser:
134
+ parser = argparse.ArgumentParser(
135
+ description="Inspect or reconcile Colony competence evidence")
136
+ sub = parser.add_subparsers(dest="command", required=True)
137
+ inspect = sub.add_parser("inspect", help="list exact raw event targets")
138
+ inspect.add_argument("--db", required=True, type=Path)
139
+ inspect.add_argument("--domain", required=True)
140
+ inspect.add_argument("--since", required=True, type=_parse_ts)
141
+ inspect.add_argument("--until", required=True, type=_parse_ts)
142
+ apply = sub.add_parser("apply", help="validate/apply a v1 manifest")
143
+ apply.add_argument("--db", required=True, type=Path)
144
+ apply.add_argument("--manifest", required=True, type=Path)
145
+ apply.add_argument(
146
+ "--commit", action="store_true",
147
+ help="append after a backup (otherwise uses a disposable dry-run)")
148
+ apply.add_argument(
149
+ "--backup", type=Path,
150
+ help="new backup path; required with --commit and never overwritten")
151
+ return parser
152
+
153
+
154
+ def main(argv: Optional[List[str]] = None) -> int:
155
+ parser = build_parser()
156
+ args = parser.parse_args(argv)
157
+ try:
158
+ if args.command == "inspect":
159
+ if args.since >= args.until:
160
+ raise ValueError("--since must be earlier than --until")
161
+ result = inspect_database(
162
+ args.db, args.domain, args.since, args.until)
163
+ else:
164
+ manifest = _load_manifest(args.manifest)
165
+ if args.commit:
166
+ if args.backup is None:
167
+ raise ValueError("--backup is required with --commit")
168
+ result = commit(args.db, manifest, args.backup)
169
+ else:
170
+ if args.backup is not None:
171
+ raise ValueError("--backup is only valid with --commit")
172
+ result = dry_run(args.db, manifest)
173
+ except (OSError, TypeError, ValueError, sqlite3.Error,
174
+ json.JSONDecodeError) as exc:
175
+ print(json.dumps({"ok": False, "error": str(exc)}, sort_keys=True),
176
+ file=sys.stderr)
177
+ return 2
178
+ print(json.dumps({"ok": True, **result}, sort_keys=True, indent=2))
179
+ return 0
180
+
181
+
182
+ if __name__ == "__main__": # pragma: no cover
183
+ raise SystemExit(main())
@@ -0,0 +1,381 @@
1
+ """Observe exact replies by a declared horizon, without scheduling or sending.
2
+
3
+ An accepted dispatch is not delivery/read evidence. The fixed 0.5 probability
4
+ is an explicitly uncalibrated baseline, not a judgment about the recipient.
5
+ Only existing trusted callbacks can issue a forecast; reads only reconcile it.
6
+ """
7
+ from contextlib import closing
8
+ from datetime import datetime, timezone
9
+ import json
10
+ import logging
11
+ import os
12
+ import sqlite3
13
+ import time
14
+
15
+ from apsimo import get_state_dir
16
+ from apsimo.contacts.transport_ingress import TransportIngress
17
+ from apsimo.turns import get_turn_idempotency_ledger
18
+ from .expectations import expectations_enabled
19
+ from .runtime_forecasts import _current, _digest
20
+
21
+ METHOD = 'accepted-dispatch-reply-by-declared-horizon-v1'
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _parts():
26
+ from apsimo.api.routers import host
27
+ owner = (os.environ.get('COLONY_OWNER_PERSON_ID') or os.environ.get('COLONY_OWNER_CONTACT_ID', '')).strip()
28
+ if (not owner or not expectations_enabled() or host._expectations is None
29
+ or host._commitment_store is None or host._comms_log is None):
30
+ return None
31
+ return (host._expectations.store, get_turn_idempotency_ledger(get_state_dir()),
32
+ host._commitment_store, host._comms_log, owner)
33
+
34
+
35
+ def _fid(wait_id):
36
+ return 'expected-reply:'+_digest(wait_id)
37
+
38
+
39
+ def _metadata(ledger, source):
40
+ with closing(ledger._connect()) as db:
41
+ row = db.execute('SELECT messages_json FROM turn_sources WHERE turn_id=?', (source,)).fetchone()
42
+ return json.loads(row[0])[0].get('_expected_reply_facts') if row else None
43
+
44
+
45
+ def _retain(ledger, source, person, facts, stamp, dependencies=None, session_id=''):
46
+ previous = _metadata(ledger, source)
47
+ if previous is None:
48
+ ledger.record_source(source, contact_id=person, session_id=session_id or source,
49
+ occurred_at=datetime.fromtimestamp(stamp, timezone.utc).isoformat(), derive_claims=False,
50
+ messages=[{'role':'assistant', 'content':'', '_expected_reply_facts':facts,
51
+ '_supplied_sources':[{'source_id':sid, 'source_version':version}
52
+ for sid, version in (dependencies or {}).items()]}])
53
+ previous = _metadata(ledger, source)
54
+ refs = ledger.source_references([source], contact_id=person, session_id='')
55
+ if not refs or ledger.is_projection_erased(source) or previous.get('method') != METHOD:
56
+ raise ValueError('reply_forecast_evidence_unavailable')
57
+ return {'receipt:'+source:refs[0]['source_version']}, previous
58
+
59
+
60
+ def _parent_current(ledger, commitments, conditions, owner):
61
+ from apsimo.api.routers.temporal_followups import source_bindings
62
+ parent = commitments.get(conditions['commitment_id'])
63
+ with closing(commitments._connect()) as db:
64
+ row = db.execute('SELECT material_digest FROM temporal_followups WHERE wait_id=?',
65
+ (conditions['wait_id'],)).fetchone()
66
+ return bool(parent and parent['person_id'] == owner and row
67
+ and row[0] == conditions['wait_material_digest']
68
+ and not source_bindings(conditions['parent_sources'], conditions['parent_sources'],
69
+ person=owner, session_id=conditions['parent_session_id']))
70
+
71
+
72
+ def _reply(ingress, ledger, conditions, item):
73
+ row = ingress.get(item['receipt_ref'])
74
+ if not row or row['state'] != 'completed' or row['outcome'] != 'captured':
75
+ return None
76
+ metadata = json.loads(row['metadata_json'])
77
+ channel = conditions['channel']
78
+ if (row['producer'] != conditions['producer'] or row['account_id'] != conditions['account_id']
79
+ or row['contact_id'] != conditions['recipient_id'] or not row['media_available']
80
+ or metadata.get('channel') != channel or metadata.get('from_owner') or metadata.get('is_group')
81
+ or channel+':'+metadata.get('reply_to_ref', '') != conditions['provider_external_ref']
82
+ or channel+':'+row['event_id'] != item['external_ref']):
83
+ return None
84
+ native = json.loads(row['native_turn_json'] or '{}')
85
+ versions = json.loads(row['source_versions_json'] or '{}')
86
+ if set(versions) != {native.get('turn_id')}:
87
+ return None
88
+ from apsimo.api.routers.temporal_followups import source_bindings
89
+ if source_bindings(versions, versions, person=row['contact_id'], session_id=native['session_id']):
90
+ return None
91
+ return row
92
+
93
+
94
+ def evidence_current(prediction, observation=None):
95
+ """Narrow read predicate; raw forecast_history remains historical evidence."""
96
+ if prediction.detail.get('method') != METHOD:
97
+ return True
98
+ try:
99
+ parts = _parts()
100
+ if parts is None:
101
+ return False
102
+ store, ledger, commitments, comms, owner = parts
103
+ c = prediction.detail['conditions']
104
+ if (prediction.subject_person_id != owner or prediction.viewer_scope != owner
105
+ or prediction.shareability != 'owner_private'
106
+ or not _parent_current(ledger, commitments, c, owner)
107
+ or not _current(ledger, prediction.evidence_refs, prediction.detail['source_versions'], owner)):
108
+ return False
109
+ if observation is None:
110
+ outcomes = store.forecast_history(prediction.detail['forecast_id'])['outcomes']
111
+ observation = outcomes[-1] if outcomes else None
112
+ if observation is None:
113
+ return True
114
+ facts = _metadata(ledger, observation['receipt_ref'].removeprefix('receipt:'))
115
+ if not facts or facts.get('method') != METHOD:
116
+ return False
117
+ person = c['recipient_id'] if facts['kind'] == 'reply' else owner
118
+ if not _current(ledger, observation['evidence_refs'], observation['source_versions'], person):
119
+ return False
120
+ if facts['kind'] == 'wait_stopped':
121
+ return observation['status'] == 'censored'
122
+ with closing(comms.read_connection()) as db:
123
+ ingress = TransportIngress(db)
124
+ if facts['kind'] == 'reply':
125
+ row = _reply(ingress, ledger, c, facts['reply'])
126
+ return bool(row and json.loads(row['source_versions_json']) == facts['reply_sources'])
127
+ coverage = ingress.coverage(producer=c['producer'], account_id=c['account_id'],
128
+ contact_id=c['recipient_id'], since=prediction.detail['origin_at'], until=prediction.horizon,
129
+ now=facts['coverage']['observed_at'], observation=facts['coverage'])
130
+ return coverage['observed'] and coverage['observed_through'] >= prediction.horizon
131
+ except (KeyError, TypeError, ValueError, OSError, sqlite3.Error):
132
+ return False
133
+
134
+
135
+ def observe_dispatch(waits, row, *, producer, receipt, created):
136
+ parts = _parts()
137
+ if parts is None or receipt.direction != 'out':
138
+ return
139
+ store, ledger, commitments, comms, owner = parts
140
+ fid = _fid(row['wait_id'])
141
+ if store.forecast_history(fid)['forecasts']:
142
+ return
143
+ source = fid+':dispatch'
144
+ retained = _metadata(ledger, source)
145
+ # A later ledger scan cannot invent a forecast. An interrupted first
146
+ # callback can replay its already frozen declaration, never a new horizon.
147
+ if not created and retained is None:
148
+ return
149
+ if retained is None:
150
+ now = time.time()
151
+ parent = commitments.get(row['commitment_id'])
152
+ if (not parent or parent['person_id'] != owner or row['reply'] or row['state'] not in {'open','deferred'}
153
+ or row['dispatch_receipt_ref'] != receipt.receipt_ref
154
+ or row['outbound_ref'] not in {receipt.outbound_ref, receipt.channel+':'+receipt.external_ref}
155
+ or not row['expected_at'] or row['expected_at'] <= now or row['expires_at'] <= now):
156
+ return
157
+ with closing(comms.read_connection()) as db:
158
+ accounts = db.execute('SELECT account_id FROM transport_ingress_coverage WHERE producer=?',
159
+ (producer,)).fetchall()
160
+ # This first method uses the existing one-account WhatsApp contract.
161
+ if receipt.channel != 'whatsapp' or len(accounts) != 1:
162
+ return
163
+ with closing(commitments._connect()) as db:
164
+ material = db.execute('SELECT material_digest FROM temporal_followups WHERE wait_id=?',
165
+ (row['wait_id'],)).fetchone()[0]
166
+ c = {'wait_id':row['wait_id'], 'commitment_id':row['commitment_id'], 'work_id':row['work_id'],
167
+ 'wait_material_digest':material, 'parent_sources':row['source_versions'],
168
+ 'parent_origin':parent['made_at'],
169
+ 'parent_session_id':row['source_session_id'], 'recipient_id':row['contact_id'],
170
+ 'producer':producer, 'account_id':accounts[0]['account_id'], 'channel':receipt.channel,
171
+ 'provider_external_ref':receipt.channel+':'+receipt.external_ref,
172
+ 'dispatch_receipt_ref':receipt.receipt_ref, 'dispatch_status':receipt.status,
173
+ 'outbound_ref':row['outbound_ref'], 'probability_basis':'fixed uncalibrated 0.5 baseline'}
174
+ if not _parent_current(ledger, commitments, c, owner):
175
+ return
176
+ retained = {'method':METHOD, 'kind':'dispatch', 'conditions':c,
177
+ 'origin_at':row['dispatch_occurred_at'], 'horizon':row['expected_at'], 'issued_at':now}
178
+ c = retained['conditions']
179
+ if not _parent_current(ledger, commitments, c, owner):
180
+ return
181
+ versions, retained = _retain(ledger, source, owner, retained, retained['issued_at'],
182
+ c['parent_sources'], c['parent_session_id'])
183
+ store.issue_forecast(forecast_id=fid, subject=fid, domain='expected_reply',
184
+ expectation='An exact reply is observed by the declared horizon after the recorded dispatch acknowledgment.',
185
+ confidence=.5, horizon=retained['horizon'], origin_at=retained['origin_at'], issued_at=retained['issued_at'],
186
+ evidence_refs=versions, source_versions=versions, source_kind='transport_receipt',
187
+ cohort='declared-reply:whatsapp:'+c['dispatch_status'], method=METHOD,
188
+ model_provenance={'served_model':None}, subject_person_id=owner, viewer_scope=owner,
189
+ shareability='owner_private', conditions=c)
190
+
191
+
192
+ def _stop_facts(ledger, commitments, prediction, now):
193
+ """Freeze the earliest known stop, without guessing a legacy event time."""
194
+ from apsimo.commitments.store import OPEN_STATUSES
195
+ c = prediction.detail['conditions']
196
+ retained = _metadata(ledger, _fid(c['wait_id'])+':stopped')
197
+ if retained:
198
+ return retained
199
+ with closing(commitments._connect()) as db:
200
+ row = db.execute('SELECT state,revision,payload FROM temporal_followups WHERE wait_id=?',
201
+ (c['wait_id'],)).fetchone()
202
+ if row is None:
203
+ return None # _parent_current already rejects a missing wait.
204
+ wait = json.loads(row['payload'])
205
+ parent = commitments.get(c['commitment_id'])
206
+ stops = []
207
+ if not parent or parent['status'] not in OPEN_STATUSES:
208
+ stamp = ((parent or {}).get('metadata') or {}).get('resolution', {}).get('at')
209
+ stamp = stamp or (parent or {}).get('fulfilled_at')
210
+ stops.append(('cancelled', 'cancelled', datetime.fromisoformat(stamp).timestamp() if stamp else None))
211
+ if row['state'] == 'cancelled' and (not stops or wait.get('cancelled_at') is not None):
212
+ stops.append(('cancelled', 'cancelled', wait.get('cancelled_at')))
213
+ if wait.get('followup_receipt_ref'):
214
+ stops.append(('intervened', 'intervened', wait.get('followup_observed_at')))
215
+ if row['state'] == 'expired' or wait['expires_at'] <= now:
216
+ stops.append(('expired', 'unavailable', wait['expires_at']))
217
+ if not stops:
218
+ return None
219
+ state, reason, cutoff = min(stops, key=lambda s: s[2] if s[2] is not None else float('-inf'))
220
+ return {'method':METHOD, 'kind':'wait_stopped', 'state':state, 'reason':reason,
221
+ 'stopped_at':cutoff,
222
+ 'wait_revision':row['revision'], 'observed_at':now,
223
+ 'resolution_ref':wait.get('resolution_ref'),
224
+ 'followup_receipt_ref':wait.get('followup_receipt_ref')}
225
+
226
+
227
+ def _record_stop(store, ledger, prediction, latest, owner, facts, now):
228
+ c = prediction.detail['conditions']
229
+ versions, facts = _retain(ledger, _fid(c['wait_id'])+':stopped', owner, facts, now,
230
+ c['parent_sources'], c['parent_session_id'])
231
+ store.record_forecast_outcome(forecast_id=prediction.detail['forecast_id'],
232
+ receipt_ref=next(iter(versions)), evidence_refs=versions, source_versions=versions,
233
+ source_kind='work_receipt', observed_at=facts['observed_at'], recorded_at=now,
234
+ subject_person_id=owner, viewer_scope=owner, shareability='owner_private',
235
+ status='censored', value=None, reason=facts['reason'],
236
+ previous_revision=latest['revision'] if latest else 0)
237
+
238
+
239
+ def reconcile(wait_id):
240
+ parts = _parts()
241
+ if parts is None:
242
+ return
243
+ store, ledger, commitments, comms, owner = parts
244
+ history = store.forecast_history(_fid(wait_id))
245
+ if not history['forecasts']:
246
+ return
247
+ raw = history['forecasts'][0]
248
+ prediction = store.get(raw['prediction_id'])
249
+ if not evidence_current(prediction):
250
+ return
251
+ latest = history['outcomes'][-1] if history['outcomes'] else None
252
+ if latest and latest['status'] == 'observed':
253
+ return
254
+ c = prediction.detail['conditions']
255
+ now = time.time()
256
+ stop = _stop_facts(ledger, commitments, prediction, now)
257
+ recorded_receipts = {o['receipt_ref'] for o in history['outcomes']}
258
+ with closing(comms.read_connection()) as db:
259
+ ingress = TransportIngress(db)
260
+ matches = comms.match_reply(contact_id=c['recipient_id'], outbound_ref=c['provider_external_ref'],
261
+ since_iso=c['parent_origin'],
262
+ until_iso=datetime.fromtimestamp(now, timezone.utc).isoformat(), connection=db)
263
+ facts = None
264
+ for item in matches['matches']:
265
+ row = _reply(ingress, ledger, c, item)
266
+ if row is None:
267
+ continue
268
+ observed = row['occurred_at']
269
+ # Canonical admission can lag the actual reply or an already
270
+ # recorded censor. Only a reply known to precede the stop can
271
+ # replace that censor. Legacy missing timestamps stay unscored.
272
+ if stop and (stop.get('stopped_at') is None or observed >= stop['stopped_at']
273
+ or not prediction.created_at <= observed <= prediction.horizon):
274
+ continue
275
+ state = ('retrospective_unscored' if observed < prediction.created_at else
276
+ 'reply_observed_in_time' if observed <= prediction.horizon else 'deadline_elapsed_unobserved')
277
+ facts = {'method':METHOD, 'kind':'reply', 'state':state, 'reply':item,
278
+ 'reply_sources':json.loads(row['source_versions_json']), 'observed_at':observed}
279
+ source = _fid(wait_id)+':reply:'+_digest(row['receipt_id'])
280
+ if 'receipt:'+source in recorded_receipts:
281
+ continue
282
+ versions, facts = _retain(ledger, source, c['recipient_id'], facts, now,
283
+ facts['reply_sources'], json.loads(row['native_turn_json'])['session_id'])
284
+ store.record_forecast_outcome(forecast_id=_fid(wait_id), receipt_ref=next(iter(versions)),
285
+ evidence_refs=versions, source_versions=versions, source_kind='transport_receipt',
286
+ observed_at=facts['observed_at'], recorded_at=now, subject_person_id=owner,
287
+ viewer_scope=owner, shareability='owner_private',
288
+ status='observed' if state == 'reply_observed_in_time' else 'unresolved',
289
+ value=True if state == 'reply_observed_in_time' else None,
290
+ reason='' if state == 'reply_observed_in_time' else 'unknown',
291
+ previous_revision=latest['revision'] if latest else 0)
292
+ if state == 'reply_observed_in_time':
293
+ return
294
+ latest = store.forecast_history(_fid(wait_id))['outcomes'][-1]
295
+ if stop:
296
+ if not latest or latest['status'] != 'censored':
297
+ _record_stop(store, ledger, prediction, latest, owner, stop, now)
298
+ return
299
+ if now < prediction.horizon:
300
+ return
301
+ accounts = db.execute('SELECT * FROM transport_ingress_coverage WHERE producer=?',
302
+ (c['producer'],)).fetchall()
303
+ if len(accounts) != 1 or accounts[0]['account_id'] != c['account_id']:
304
+ return
305
+ coverage = dict(accounts[0])
306
+ checked = ingress.coverage(producer=c['producer'], account_id=c['account_id'],
307
+ contact_id=c['recipient_id'], since=prediction.detail['origin_at'], until=prediction.horizon, now=now)
308
+ if not checked['observed'] or checked['observed_through'] < prediction.horizon:
309
+ return
310
+ facts = {'method':METHOD, 'kind':'coverage', 'state':'no_reply_with_coverage', 'coverage':coverage}
311
+ versions, facts = _retain(ledger, _fid(wait_id)+':coverage', owner, facts, now,
312
+ c['parent_sources'], c['parent_session_id'])
313
+ store.record_forecast_outcome(forecast_id=_fid(wait_id), receipt_ref=next(iter(versions)),
314
+ evidence_refs=versions, source_versions=versions, source_kind='transport_receipt',
315
+ observed_at=prediction.horizon, coverage_until=facts['coverage']['observed_at'], recorded_at=now,
316
+ subject_person_id=owner, viewer_scope=owner, shareability='owner_private', value=False,
317
+ previous_revision=latest['revision'] if latest else 0)
318
+
319
+
320
+ def safe(call, *args, **kwargs):
321
+ try:
322
+ return call(*args, **kwargs)
323
+ except Exception as error:
324
+ logger.warning('Declared reply forecast observation unavailable: %s', type(error).__name__)
325
+
326
+
327
+ def reconcile_existing(*, receipt=None, producer=None, coverage=None):
328
+ """At most 100 newest relevant waits, never a FIFO of historical unknowns.
329
+
330
+ Exact incoming lineage bypasses unrelated waits. Coverage considers only
331
+ this account's observed connected interval. Older broad-coverage history
332
+ is not exhaustively drained; an owner wait read reconciles that exact row.
333
+ """
334
+ parts = _parts()
335
+ if parts is None:
336
+ return
337
+ store = parts[0]
338
+ query = "SELECT detail FROM predictions WHERE domain='expected_reply' AND source=? AND outcome IN ('pending','unresolved') "
339
+ parameters = [METHOD]
340
+ if receipt is not None:
341
+ metadata = json.loads(receipt['metadata_json'])
342
+ if not metadata.get('reply_to_ref') or not receipt['contact_id']:
343
+ return
344
+ for name, value in {'recipient_id':receipt['contact_id'], 'producer':receipt['producer'],
345
+ 'account_id':receipt['account_id'],
346
+ 'provider_external_ref':metadata['channel']+':'+metadata['reply_to_ref']}.items():
347
+ query += "AND json_extract(detail,'$.conditions."+name+"')=? "
348
+ parameters.append(value)
349
+ elif coverage is not None and coverage['connected'] and coverage['connected_since'] is not None:
350
+ query += ("AND json_extract(detail,'$.conditions.producer')=? "
351
+ "AND json_extract(detail,'$.conditions.account_id')=? "
352
+ "AND json_extract(detail,'$.origin_at')>=? AND horizon<=? ")
353
+ parameters += [producer, coverage['account_id'], coverage['connected_since'], coverage['observed_at']]
354
+ else:
355
+ return
356
+ with store._lock:
357
+ rows = store._conn.execute(query+"ORDER BY created_at DESC,prediction_id LIMIT 100", parameters).fetchall()
358
+ for row in rows:
359
+ c = json.loads(row[0])['conditions']
360
+ safe(reconcile, c['wait_id'])
361
+
362
+
363
+ def project(wait_id):
364
+ result = {'status':'no_prospective_forecast', 'suggestion_enabled':False}
365
+ parts = _parts()
366
+ if parts is None:
367
+ return result
368
+ safe(reconcile, wait_id)
369
+ store = parts[0]
370
+ history = store.forecast_history(_fid(wait_id))
371
+ if not history['forecasts']:
372
+ return result
373
+ p = store.get(history['forecasts'][0]['prediction_id'])
374
+ result.update(status='pending' if time.time() < p.horizon else 'deadline_elapsed_unobserved',
375
+ forecast_id=_fid(wait_id), horizon=p.horizon, probability=.5, probability_calibrated=False)
376
+ if not evidence_current(p):
377
+ return {**result, 'status':'source_unavailable'}
378
+ if history['outcomes']:
379
+ facts = _metadata(parts[1], history['outcomes'][-1]['receipt_ref'].removeprefix('receipt:'))
380
+ result['status'] = facts['state']
381
+ return result