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,1128 @@
1
+ """Durable, scoped host-event to cognitive-concern reduction.
2
+
3
+ The host event journal is the source of truth. This reducer never consumes
4
+ transient WebSocket buffers and never asks a model to guess scope, salience, or
5
+ deduplication. Concern mutation, the idempotency receipt, and the durable
6
+ cursor are committed together by :class:`ConcernStore`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from datetime import datetime
13
+ import hashlib
14
+ import json
15
+ import os
16
+ import re
17
+ from typing import Any, Callable, Dict, Mapping, Optional
18
+
19
+ from apsimo.events.journal import current_sequence, replay_events
20
+ from apsimo.self_model.workspace import ConcernStore
21
+
22
+
23
+ _CONSUMER_ID = "workspace-concerns-v1"
24
+ _EXTERNAL_CONSUMER_ID = "workspace-external-concerns-v1"
25
+ _TURN_CONSUMER_ID = "workspace-turn-concerns-v1"
26
+ _SAFE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/+\-]{0,127}$")
27
+ _SAFE_EVENT_TYPE = re.compile(r"^[a-z0-9][a-z0-9_.:\-]{0,127}$")
28
+ _SAFE_CHANNEL_LANE = re.compile(r"^[a-z0-9][a-z0-9_.\-]{0,63}$")
29
+ _SAFE_SESSION_PREFIX = re.compile(r"^[a-z0-9][a-z0-9_.:@/+\-]{0,127}$")
30
+ _SHAREABILITY = {"owner_private", "subject_private", "shared", "public"}
31
+ _SUMMARY_FIELDS = ("summary", "description", "title", "reason", "observation", "service")
32
+ _REF_FIELDS = (
33
+ "commitment_id", "project_id", "work_order_id", "step_id", "approval_id",
34
+ "expectation_id", "surprise_id", "anomaly_id", "service_id", "source_ref",
35
+ )
36
+
37
+ _INTERNAL_TURN_CHANNELS = frozenset({
38
+ "agent", "api", "cognition", "cron", "event", "health", "internal",
39
+ "service", "system", "tool", "worker",
40
+ })
41
+
42
+
43
+ def event_concern_mode() -> str:
44
+ value = os.environ.get("COLONY_EVENT_CONCERNS", "off").strip().lower()
45
+ return value if value in {"off", "shadow", "live"} else "off"
46
+
47
+
48
+ def event_concerns_enabled() -> bool:
49
+ return event_concern_mode() in {"shadow", "live"}
50
+
51
+
52
+ def external_event_concern_mode() -> str:
53
+ """External-report concern producer mode; invalid/unset fails off."""
54
+
55
+ value = os.environ.get(
56
+ "COLONY_EXTERNAL_EVENT_CONCERNS", "off",
57
+ ).strip().lower()
58
+ return value if value in {"off", "shadow", "live"} else "off"
59
+
60
+
61
+ def external_event_concerns_enabled() -> bool:
62
+ return external_event_concern_mode() in {"shadow", "live"}
63
+
64
+
65
+ def turn_concern_mode() -> str:
66
+ """Completed-conversation concern producer mode; invalid/unset is off."""
67
+
68
+ value = os.environ.get("COLONY_TURN_CONCERNS", "off").strip().lower()
69
+ return value if value in {"off", "shadow", "live"} else "off"
70
+
71
+
72
+ def turn_concerns_enabled() -> bool:
73
+ return turn_concern_mode() in {"shadow", "live"}
74
+
75
+
76
+ def _turn_channel_config() -> tuple[tuple[str, ...], bool]:
77
+ """Return the exact configured channel-lane allowlist.
78
+
79
+ Channel IDs are ``lane:conversation``. Configuration names only the lane
80
+ because per-conversation identifiers are dynamic. One malformed entry
81
+ invalidates the complete allowlist rather than broadening a typo.
82
+ """
83
+
84
+ raw = os.environ.get("COLONY_TURN_CONCERNS_CHANNELS", "")
85
+ if not raw.strip():
86
+ return (), False
87
+ parts = raw.split(",")
88
+ if any(not item.strip() for item in parts):
89
+ return (), False
90
+ values = tuple(item.strip().lower() for item in parts)
91
+ if any(not _SAFE_CHANNEL_LANE.fullmatch(item) for item in values):
92
+ return (), False
93
+ return tuple(dict.fromkeys(values)), True
94
+
95
+
96
+ def turn_concern_channels() -> tuple[str, ...]:
97
+ return _turn_channel_config()[0]
98
+
99
+
100
+ def _turn_excluded_platform_config() -> tuple[tuple[str, ...], bool]:
101
+ raw = os.environ.get(
102
+ "COLONY_TURN_CONCERNS_EXCLUDED_PLATFORMS", "",
103
+ )
104
+ if not raw.strip():
105
+ return (), True
106
+ parts = raw.split(",")
107
+ if any(not item.strip() for item in parts):
108
+ return (), False
109
+ values = tuple(item.strip().lower() for item in parts)
110
+ if any(not _SAFE_CHANNEL_LANE.fullmatch(item) for item in values):
111
+ return (), False
112
+ return tuple(dict.fromkeys(values)), True
113
+
114
+
115
+ def turn_concern_excluded_platforms() -> tuple[str, ...]:
116
+ return _turn_excluded_platform_config()[0]
117
+
118
+
119
+ def _turn_excluded_session_prefix_config() -> tuple[tuple[str, ...], bool]:
120
+ raw = os.environ.get(
121
+ "COLONY_TURN_CONCERNS_EXCLUDED_SESSION_PREFIXES", "",
122
+ )
123
+ if not raw.strip():
124
+ return (), True
125
+ parts = raw.split(",")
126
+ if any(not item.strip() for item in parts):
127
+ return (), False
128
+ values = tuple(item.strip().lower() for item in parts)
129
+ if any(not _SAFE_SESSION_PREFIX.fullmatch(item) for item in values):
130
+ return (), False
131
+ return tuple(dict.fromkeys(values)), True
132
+
133
+
134
+ def turn_concern_excluded_session_prefixes() -> tuple[str, ...]:
135
+ return _turn_excluded_session_prefix_config()[0]
136
+
137
+
138
+ def _bootstrap_mode() -> str:
139
+ value = os.environ.get("COLONY_EVENT_CONCERNS_BOOTSTRAP", "tail").strip().lower()
140
+ return value if value in {"tail", "replay"} else "tail"
141
+
142
+
143
+ def _external_bootstrap_mode() -> str:
144
+ # External intake already made these reports durable while this consumer
145
+ # was off. First enablement therefore replays retained history instead of
146
+ # silently losing the off-period backlog.
147
+ return "replay"
148
+
149
+
150
+ def _turn_bootstrap_mode() -> str:
151
+ value = os.environ.get(
152
+ "COLONY_TURN_CONCERNS_BOOTSTRAP", "tail",
153
+ ).strip().lower()
154
+ return value if value in {"tail", "replay"} else "tail"
155
+
156
+
157
+ def _owner_person_id() -> str:
158
+ return (
159
+ os.environ.get("COLONY_OWNER_PERSON_ID", "").strip()
160
+ or os.environ.get("COLONY_OWNER_CONTACT_ID", "").strip()
161
+ or "owner"
162
+ )[:128]
163
+
164
+
165
+ def _clean_id(value: Any) -> str:
166
+ candidate = str(value or "").strip()
167
+ return candidate if _SAFE_ID.fullmatch(candidate) else ""
168
+
169
+
170
+ def _clean_text(value: Any, maximum: int = 300) -> str:
171
+ raw = str(value or "")
172
+ cleaned = " ".join(
173
+ "".join(char if 32 <= ord(char) < 127 else " " for char in raw).split()
174
+ )
175
+ return cleaned[:maximum]
176
+
177
+
178
+ def _digest(value: Any) -> str:
179
+ encoded = json.dumps(
180
+ value,
181
+ ensure_ascii=False,
182
+ separators=(",", ":"),
183
+ sort_keys=True,
184
+ default=str,
185
+ ).encode("utf-8")
186
+ return hashlib.sha256(encoded).hexdigest()
187
+
188
+
189
+ @dataclass(frozen=True)
190
+ class EventProjection:
191
+ operation: str
192
+ kind: str
193
+ summary: str
194
+ salience: float
195
+ dedup_key: str
196
+ sources: tuple[str, ...]
197
+ subject_person_id: str
198
+ viewer_scope: str
199
+ shareability: str
200
+ occurred_at: str
201
+ material_digest: str
202
+ note: str = ""
203
+ producer_name: str = "event_concerns"
204
+ producer_mode: str = ""
205
+ producer_revision: str = "event-concern-reducer-v2"
206
+ external_event_projection: bool = False
207
+
208
+ def store_payload(self) -> Dict[str, Any]:
209
+ return {
210
+ "operation": self.operation,
211
+ "kind": self.kind,
212
+ "summary": self.summary,
213
+ "salience": self.salience,
214
+ "dedup_key": self.dedup_key,
215
+ "sources": list(self.sources),
216
+ "subject_person_id": self.subject_person_id,
217
+ "viewer_scope": self.viewer_scope,
218
+ "shareability": self.shareability,
219
+ "occurred_at": self.occurred_at,
220
+ "note": self.note,
221
+ "producer_name": self.producer_name,
222
+ "producer_mode": self.producer_mode or event_concern_mode(),
223
+ "producer_revision": self.producer_revision,
224
+ "external_event_projection": self.external_event_projection,
225
+ }
226
+
227
+
228
+ def _event_identity(event: Mapping[str, Any]) -> tuple[int, str, str, str, Mapping[str, Any]]:
229
+ sequence = int(event.get("seq") or 0)
230
+ event_id = _clean_id(event.get("ulid")) or f"journal-seq-{sequence}"
231
+ event_type = str(event.get("type") or "").strip().lower()
232
+ occurred_at = _clean_text(
233
+ event.get("occurredAt") or event.get("recordedAt") or "", 64,
234
+ )
235
+ data = event.get("data")
236
+ if sequence < 1 or not _SAFE_EVENT_TYPE.fullmatch(event_type):
237
+ raise ValueError("malformed journal event identity")
238
+ if not isinstance(data, Mapping):
239
+ raise ValueError("malformed journal event data")
240
+ return sequence, event_id, event_type, occurred_at, data
241
+
242
+
243
+ def _subject_and_scope(data: Mapping[str, Any]) -> tuple[str, str, str]:
244
+ subject = ""
245
+ for key in ("subject_person_id", "person_id", "contact_id"):
246
+ subject = _clean_id(data.get(key))
247
+ if subject:
248
+ break
249
+ subject = subject or _owner_person_id()
250
+
251
+ requested = str(
252
+ data.get("shareability") or data.get("privacy_scope") or "owner_private"
253
+ ).strip().lower()
254
+ shareability = requested if requested in _SHAREABILITY else "owner_private"
255
+ # A payload can originate at a transport boundary. Shared/public is
256
+ # accepted only when a server-side boundary explicitly attested it.
257
+ if shareability in {"shared", "public"} and data.get("boundary_attested") is not True:
258
+ shareability = "owner_private"
259
+ if shareability == "subject_private":
260
+ viewer_scope = f"person:{subject}"
261
+ elif shareability in {"shared", "public"}:
262
+ viewer_scope = shareability
263
+ else:
264
+ viewer_scope = "owner"
265
+ return subject, viewer_scope, shareability
266
+
267
+
268
+ def _entity(data: Mapping[str, Any], event_type: str, summary: str) -> str:
269
+ if event_type.startswith(("work_order.", "work.")):
270
+ ordered_fields = (
271
+ "work_order_id", "step_id", "project_id",
272
+ *tuple(key for key in _REF_FIELDS if key not in {
273
+ "work_order_id", "step_id", "project_id",
274
+ }),
275
+ )
276
+ elif event_type.startswith("project."):
277
+ ordered_fields = (
278
+ "project_id",
279
+ *tuple(key for key in _REF_FIELDS if key != "project_id"),
280
+ )
281
+ else:
282
+ ordered_fields = _REF_FIELDS
283
+ for key in ordered_fields:
284
+ value = _clean_id(data.get(key))
285
+ if value:
286
+ return value
287
+ generic = _clean_id(data.get("id"))
288
+ if generic:
289
+ return generic
290
+ return _digest({"type": event_type, "summary": summary})[:24]
291
+
292
+
293
+ def _summary(event_type: str, data: Mapping[str, Any]) -> str:
294
+ for key in _SUMMARY_FIELDS:
295
+ value = _clean_text(data.get(key))
296
+ if value:
297
+ return value
298
+ return event_type.replace(".", " ").replace("_", " ")
299
+
300
+
301
+ def _source_refs(sequence: int, event_id: str, data: Mapping[str, Any]) -> tuple[str, ...]:
302
+ values = [f"journal:{sequence}:{event_id}"]
303
+ for key in _REF_FIELDS:
304
+ value = _clean_id(data.get(key))
305
+ if value:
306
+ values.append(f"{key.removesuffix('_id')}:{value}")
307
+ return tuple(dict.fromkeys(values))[:20]
308
+
309
+
310
+ def project_event(event: Mapping[str, Any]) -> tuple[Optional[EventProjection], str, str]:
311
+ """Return ``(projection, skip_reason, raw_digest)`` for one host event."""
312
+
313
+ sequence, event_id, event_type, occurred_at, data = _event_identity(event)
314
+ raw_digest = _digest({
315
+ "type": event_type,
316
+ "occurred_at": occurred_at,
317
+ "data": data,
318
+ })
319
+ summary = _summary(event_type, data)
320
+ subject, viewer_scope, shareability = _subject_and_scope(data)
321
+ sources = _source_refs(sequence, event_id, data)
322
+
323
+ category = ""
324
+ operation = "upsert"
325
+ kind = "thread"
326
+ salience = 0.5
327
+
328
+ if event_type == "commitment.overdue":
329
+ category, kind, salience = "commitment", "goal", 0.75
330
+ elif event_type in {"commitment.fulfilled", "commitment.cancelled"}:
331
+ category, operation, kind = "commitment", "resolve", "goal"
332
+ elif event_type in {"surprise.high", "surprise.accumulation", "expectation.missed"}:
333
+ category, kind, salience = "expectation", "question", 0.72
334
+ elif event_type in {"expectation.hit", "expectation.resolved"}:
335
+ category, operation, kind = "expectation", "resolve", "question"
336
+ elif "anomaly" in event_type or event_type.startswith("health.")\
337
+ or event_type.startswith("service."):
338
+ category = "service" if event_type.startswith(("health.", "service.")) else "anomaly"
339
+ kind = "maintenance" if category == "service" else "anomaly"
340
+ terminal = event_type.endswith((".recovered", ".resolved", ".healthy"))
341
+ operation = "resolve" if terminal else "upsert"
342
+ salience = 0.78 if operation == "upsert" else 0.0
343
+ elif event_type.startswith(("project.", "work_order.", "work.")):
344
+ category, kind = "project", "goal"
345
+ status = str(data.get("status") or event_type.rsplit(".", 1)[-1]).lower()
346
+ if status in {
347
+ "blocked", "failed", "overdue", "stalled", "unverified",
348
+ "verification_pending", "abandoned",
349
+ }:
350
+ operation, salience = "upsert", 0.8
351
+ elif status in {"completed", "verified", "cancelled", "resolved"}:
352
+ operation, salience = "resolve", 0.0
353
+ else:
354
+ return None, "non_material_project_event", raw_digest
355
+ elif event_type.startswith("approval."):
356
+ category, kind = "approval", "maintenance"
357
+ status = str(data.get("status") or event_type.rsplit(".", 1)[-1]).lower()
358
+ if status in {"pending", "required", "awaiting_approval"}:
359
+ operation, salience = "upsert", 0.7
360
+ elif status in {"approved", "denied", "expired", "cancelled", "consumed"}:
361
+ operation, salience = "resolve", 0.0
362
+ else:
363
+ return None, "non_material_approval_event", raw_digest
364
+ elif event_type in {"relationship.follow_up_due", "contact.cadence_overdue"}:
365
+ category, kind, salience = "relationship", "thread", 0.58
366
+ elif event_type in {"relationship.followed_up", "contact.cadence_satisfied"}:
367
+ category, operation, kind = "relationship", "resolve", "thread"
368
+ else:
369
+ return None, "unmapped_event_type", raw_digest
370
+
371
+ entity = _entity(data, event_type, summary)
372
+ dedup_key = f"event:{category}:{subject}:{entity}"[:200]
373
+ material = {
374
+ "operation": operation,
375
+ "category": category,
376
+ "entity": entity,
377
+ "subject": subject,
378
+ "summary": summary if operation == "upsert" else "",
379
+ "shareability": shareability,
380
+ "status": str(data.get("status") or event_type.rsplit(".", 1)[-1]).lower(),
381
+ }
382
+ projection = EventProjection(
383
+ operation=operation,
384
+ kind=kind,
385
+ summary=summary,
386
+ salience=salience,
387
+ dedup_key=dedup_key,
388
+ sources=sources,
389
+ subject_person_id=subject,
390
+ viewer_scope=viewer_scope,
391
+ shareability=shareability,
392
+ occurred_at=occurred_at,
393
+ material_digest=_digest(material),
394
+ note=(f"resolved by {event_type}" if operation == "resolve" else ""),
395
+ )
396
+ return projection, "", raw_digest
397
+
398
+
399
+ def _turn_text(value: Any, maximum: int = 300) -> str:
400
+ if type(value) is not str:
401
+ return ""
402
+ return " ".join(
403
+ "".join(
404
+ char if char >= " " and char != "\x7f" else " "
405
+ for char in value
406
+ ).split()
407
+ )[:maximum]
408
+
409
+
410
+ def _turn_id(value: Any) -> str:
411
+ if (
412
+ type(value) is not str
413
+ or not value
414
+ or value != value.strip()
415
+ or len(value) > 256
416
+ or any(ord(char) < 32 or ord(char) == 127 for char in value)
417
+ ):
418
+ return ""
419
+ return value
420
+
421
+
422
+ def _turn_session(value: Any) -> str:
423
+ if (
424
+ type(value) is not str
425
+ or not value
426
+ or value != value.strip()
427
+ or len(value) > 256
428
+ or any(ord(char) < 32 or ord(char) == 127 for char in value)
429
+ ):
430
+ return ""
431
+ return value
432
+
433
+
434
+ def _turn_channel(value: Any) -> tuple[str, str]:
435
+ if (
436
+ type(value) is not str
437
+ or not value
438
+ or value != value.strip()
439
+ or len(value) > 512
440
+ or any(ord(char) < 32 or ord(char) == 127 for char in value)
441
+ ):
442
+ return "", ""
443
+ lane = value.split(":", 1)[0].lower()
444
+ if not _SAFE_CHANNEL_LANE.fullmatch(lane):
445
+ return "", ""
446
+ return value, lane
447
+
448
+
449
+ def _channel_lane_matches(lane: str, blocked: frozenset[str]) -> bool:
450
+ return any(
451
+ lane == value
452
+ or any(lane.startswith(value + separator) for separator in ("-", "_", "."))
453
+ for value in blocked
454
+ )
455
+
456
+
457
+ def _configured_turn_owner() -> str:
458
+ value = (
459
+ os.environ.get("COLONY_OWNER_PERSON_ID", "").strip()
460
+ or os.environ.get("COLONY_OWNER_CONTACT_ID", "").strip()
461
+ )
462
+ return value if _SAFE_ID.fullmatch(value) else ""
463
+
464
+
465
+ def project_turn_concern_hold_reason(project: Any, concern_store: Any) -> str:
466
+ """Return the runtime hold for exact cognition-spine turn provenance."""
467
+
468
+ if str(getattr(project, "source", "") or "") != "cognition_spine":
469
+ return ""
470
+ concern_id = str(getattr(project, "concern_id", "") or "")
471
+ if not concern_id:
472
+ return ""
473
+ if concern_store is None:
474
+ return "project_hold_reason_unavailable"
475
+ concern = concern_store.get(concern_id)
476
+ if concern is None:
477
+ return "project_hold_reason_unavailable"
478
+ if str(getattr(concern, "producer_name", "") or "") != "turn_concerns":
479
+ return ""
480
+ if turn_concern_mode() != "live":
481
+ return "turn_concerns_current_mode_not_live"
482
+ return ""
483
+
484
+
485
+ def project_conversation_turn(
486
+ event: Mapping[str, Any],
487
+ ) -> tuple[Optional[EventProjection], str, str]:
488
+ """Project one server-attested completed human turn into a concern.
489
+
490
+ Conversation prose is untrusted evidence. This adapter accepts no action,
491
+ capability, approval, receipt, or effect fields; it only preserves the
492
+ server-produced subject and private viewer lane for the existing policy
493
+ spine to evaluate later.
494
+ """
495
+
496
+ sequence, event_id, event_type, occurred_at, data = _event_identity(event)
497
+ raw_digest = _digest({
498
+ "type": event_type,
499
+ "occurred_at": occurred_at,
500
+ "data": data,
501
+ })
502
+ if event_type != "conversation.turn":
503
+ return None, "not_conversation_turn", raw_digest
504
+ if data.get("turn_scope_schema") != "ConversationTurnJournalScopeV1":
505
+ return None, "missing_turn_scope_envelope", raw_digest
506
+
507
+ session_id = _turn_session(data.get("session_id"))
508
+ if not session_id:
509
+ return None, "missing_turn_session", raw_digest
510
+ excluded_prefixes, session_config_valid = (
511
+ _turn_excluded_session_prefix_config()
512
+ )
513
+ if not session_config_valid:
514
+ return None, "session_prefix_config_invalid", raw_digest
515
+ if any(session_id.lower().startswith(prefix) for prefix in excluded_prefixes):
516
+ return None, "excluded_session_prefix", raw_digest
517
+
518
+ turn_id = _turn_id(data.get("turn_id"))
519
+ if not turn_id:
520
+ return None, "missing_turn_id", raw_digest
521
+ turn_id_source = data.get("turn_id_source")
522
+ if (
523
+ data.get("turn_id_attested") is not True
524
+ or turn_id_source not in {"client_idempotency_key", "server_digest"}
525
+ ):
526
+ return None, "turn_id_not_attested", raw_digest
527
+
528
+ channel_id, channel_lane = _turn_channel(data.get("channel_id"))
529
+ if not channel_lane:
530
+ return None, "missing_channel", raw_digest
531
+ if _channel_lane_matches(channel_lane, _INTERNAL_TURN_CHANNELS):
532
+ return None, "internal_channel", raw_digest
533
+ if channel_lane not in turn_concern_channels():
534
+ return None, "channel_not_allowed", raw_digest
535
+
536
+ source_platform = data.get("source_platform")
537
+ if (
538
+ type(source_platform) is not str
539
+ or not _SAFE_CHANNEL_LANE.fullmatch(source_platform)
540
+ or data.get("source_platform_attested") is not True
541
+ ):
542
+ return None, "source_platform_not_attested", raw_digest
543
+ excluded_platforms, platform_config_valid = (
544
+ _turn_excluded_platform_config()
545
+ )
546
+ if not platform_config_valid:
547
+ return None, "excluded_platform_config_invalid", raw_digest
548
+ if source_platform in excluded_platforms:
549
+ return None, "excluded_source_platform", raw_digest
550
+
551
+ contact = data.get("contact_id")
552
+ subject_value = data.get("subject_person_id")
553
+ subject = (
554
+ subject_value
555
+ if type(subject_value) is str and _SAFE_ID.fullmatch(subject_value)
556
+ else ""
557
+ )
558
+ if (
559
+ not subject
560
+ or type(contact) is not str
561
+ or contact != subject
562
+ or subject == "system"
563
+ ):
564
+ return None, "missing_attested_subject", raw_digest
565
+ if data.get("identity_attested") is not True:
566
+ return None, "turn_identity_not_attested", raw_digest
567
+ if data.get("scope_attested") is not True:
568
+ return None, "turn_scope_not_attested", raw_digest
569
+
570
+ principal = data.get("source_principal_id")
571
+ attribution = data.get("attribution_method")
572
+ if (
573
+ type(principal) is not str
574
+ or not _SAFE_ID.fullmatch(principal)
575
+ or type(attribution) is not str
576
+ or not _SAFE_ID.fullmatch(attribution)
577
+ ):
578
+ return None, "missing_sender_attestation", raw_digest
579
+
580
+ owner = _configured_turn_owner()
581
+ if not owner:
582
+ return None, "owner_identity_unconfigured", raw_digest
583
+ expected_scope = "owner" if subject == owner else f"person:{subject}"
584
+ expected_sharing = (
585
+ "owner_private" if subject == owner else "subject_private"
586
+ )
587
+ viewer_scope = data.get("viewer_scope")
588
+ shareability = data.get("shareability")
589
+ if (
590
+ viewer_scope != expected_scope
591
+ or shareability != expected_sharing
592
+ or data.get("boundary_attested") is not False
593
+ ):
594
+ return None, "unsafe_turn_scope", raw_digest
595
+
596
+ summary = _turn_text(data.get("summary"))
597
+ if not summary:
598
+ return None, "missing_turn_summary", raw_digest
599
+ turn_digest = hashlib.sha256(turn_id.encode("utf-8")).hexdigest()
600
+ sources = [
601
+ f"journal:{sequence}:{event_id}",
602
+ f"turn:{turn_digest}",
603
+ f"channel:{channel_lane}",
604
+ f"platform:{source_platform}",
605
+ f"principal:{principal}",
606
+ ]
607
+ session_digest = hashlib.sha256(session_id.encode("utf-8")).hexdigest()
608
+ sources.append(f"conversation:{session_digest}")
609
+ material = {
610
+ "namespace": "turn_concerns",
611
+ "turn_digest": turn_digest,
612
+ "turn_id_source": turn_id_source,
613
+ "session_digest": session_digest,
614
+ "channel_lane": channel_lane,
615
+ "source_platform": source_platform,
616
+ "subject_person_id": subject,
617
+ "viewer_scope": viewer_scope,
618
+ "shareability": shareability,
619
+ "summary": summary,
620
+ "source_principal_id": principal,
621
+ "attribution_method": attribution,
622
+ "evidence_status": "reported/unverified",
623
+ }
624
+ return EventProjection(
625
+ operation="upsert",
626
+ kind="thread",
627
+ summary=summary,
628
+ salience=0.5,
629
+ dedup_key=f"turn:{subject}:{turn_digest}"[:200],
630
+ sources=tuple(dict.fromkeys(sources)),
631
+ subject_person_id=subject,
632
+ viewer_scope=str(viewer_scope),
633
+ shareability=str(shareability),
634
+ occurred_at=occurred_at,
635
+ material_digest=_digest(material),
636
+ producer_name="turn_concerns",
637
+ producer_mode=turn_concern_mode(),
638
+ producer_revision="conversation-turn-concern-reducer-v1",
639
+ ), "", raw_digest
640
+
641
+
642
+ def _external_occurred_at(value: Any) -> str:
643
+ if type(value) is not str or not value or value != value.strip():
644
+ raise ValueError("external cognition occurredAt is not canonical text")
645
+ text = value
646
+ candidate = text[:-1] + "+00:00" if text.endswith("Z") else text
647
+ try:
648
+ parsed = datetime.fromisoformat(candidate)
649
+ except ValueError as exc:
650
+ raise ValueError("external cognition occurredAt is invalid") from exc
651
+ if parsed.tzinfo is None:
652
+ raise ValueError("external cognition occurredAt requires timezone authority")
653
+ return text
654
+
655
+
656
+ def project_external_event(
657
+ event: Mapping[str, Any],
658
+ ) -> tuple[Optional[EventProjection], str, str]:
659
+ """Project one strictly validated external report into its own namespace.
660
+
661
+ Terminal reports may only close the concern with the exact deterministic
662
+ subject/producer/kind/entity identity. They never join a Project, action,
663
+ grant, effect, or competence ledger.
664
+ """
665
+
666
+ if type(event.get("seq")) is not int or event.get("seq") < 1:
667
+ raise ValueError("external cognition journal sequence is not canonical")
668
+ from apsimo.cognition.external_events import (
669
+ validate_external_journal_event_id,
670
+ validate_external_journal_projection,
671
+ )
672
+
673
+ validate_external_journal_event_id(event.get("ulid"))
674
+ sequence, event_id, event_type, _occurred, data = _event_identity(event)
675
+ raw_digest = _digest({
676
+ "type": event_type,
677
+ "occurred_at": event.get("occurredAt"),
678
+ "data": data,
679
+ })
680
+ if not event_type.startswith("cognition.external."):
681
+ return None, "not_external_cognition_event", raw_digest
682
+ if type(event.get("ulid")) is not str or event.get("ulid") != event_id:
683
+ raise ValueError("external cognition journal ID is not canonical")
684
+ if type(event.get("type")) is not str or event.get("type") != event_type:
685
+ raise ValueError("external cognition journal type is not canonical")
686
+
687
+ projected = validate_external_journal_projection(event_type, data)
688
+ occurred_at = _external_occurred_at(event.get("occurredAt"))
689
+ if occurred_at != projected["external_occurred_at"]:
690
+ raise ValueError(
691
+ "external cognition host time differs from its server projection"
692
+ )
693
+ kind = projected["kind"]
694
+ attributes = dict(projected["attributes"])
695
+ producer = projected["producer_principal_id"]
696
+ subject = projected["subject_person_id"]
697
+ sharing = projected["shareability"]
698
+ viewer_scope = projected["viewer_scope"]
699
+ summary = (
700
+ attributes["observation"]
701
+ if kind == "text_turn_observation"
702
+ else projected["summary"]
703
+ )
704
+
705
+ entity_fields = {
706
+ "action_outcome": ("action_id",),
707
+ "delivery_outcome": ("delivery_ref", "message_ref"),
708
+ "service_state": ("service",),
709
+ "approval_state": ("request_id",),
710
+ "operator_reaction": ("target_ref",),
711
+ "text_turn_observation": ("turn_id",),
712
+ }
713
+ entity = next(
714
+ str(attributes.get(field) or "")
715
+ for field in entity_fields[kind]
716
+ if attributes.get(field)
717
+ )
718
+ state_field = {
719
+ "action_outcome": "outcome",
720
+ "delivery_outcome": "outcome",
721
+ "service_state": "state",
722
+ "approval_state": "state",
723
+ "operator_reaction": "reaction",
724
+ "text_turn_observation": "channel",
725
+ }[kind]
726
+ state = str(attributes[state_field])
727
+ resolve_states = {
728
+ "action_outcome": {"succeeded", "cancelled"},
729
+ "delivery_outcome": {"delivered", "read", "acknowledged"},
730
+ "service_state": {"healthy"},
731
+ "approval_state": {"approved", "rejected", "expired", "cancelled"},
732
+ "operator_reaction": {"positive", "neutral", "dismissed"},
733
+ "text_turn_observation": set(),
734
+ }
735
+ operation = "resolve" if state in resolve_states[kind] else "upsert"
736
+ concern_kind = (
737
+ "thread"
738
+ if kind in {"operator_reaction", "text_turn_observation"}
739
+ else "maintenance"
740
+ )
741
+ salience = {
742
+ "action_outcome": 0.78,
743
+ "delivery_outcome": 0.66,
744
+ "service_state": 0.76,
745
+ "approval_state": 0.62,
746
+ "operator_reaction": 0.58,
747
+ "text_turn_observation": 0.5,
748
+ }[kind] if operation == "upsert" else 0.0
749
+ identity = {
750
+ "namespace": "external_event_concerns",
751
+ "subject_person_id": subject,
752
+ "producer_principal_id": producer,
753
+ "kind": kind,
754
+ "entity": entity,
755
+ }
756
+ dedup_key = f"external:{kind}:{_digest(identity)}"
757
+ sources = (
758
+ f"journal:{sequence}:{event_id}",
759
+ f"xevent:{projected['external_event_id']}",
760
+ f"xdigest:{projected['external_event_digest']}",
761
+ f"external_producer:{producer}",
762
+ f"external_revision:{projected['producer_revision']}",
763
+ f"external_kind:{kind}",
764
+ f"xentity:{entity}",
765
+ )
766
+ material = {
767
+ **identity,
768
+ "operation": operation,
769
+ "state": state,
770
+ "summary": summary if operation == "upsert" else "",
771
+ "attributes": attributes,
772
+ "external_event_digest": projected["external_event_digest"],
773
+ "external_producer_revision": projected["producer_revision"],
774
+ "reducer_revision": "external-event-concern-reducer-v1",
775
+ "scope_digest": projected["scope_digest"],
776
+ "boundary_attested": False,
777
+ "evidence_status": "reported/unverified",
778
+ }
779
+ return EventProjection(
780
+ operation=operation,
781
+ kind=concern_kind,
782
+ summary=summary,
783
+ salience=salience,
784
+ dedup_key=dedup_key,
785
+ sources=sources,
786
+ subject_person_id=subject,
787
+ viewer_scope=viewer_scope,
788
+ shareability=sharing,
789
+ occurred_at=occurred_at,
790
+ material_digest=_digest(material),
791
+ note=(
792
+ f"resolved by reported external {kind} state {state}"
793
+ if operation == "resolve" else ""
794
+ ),
795
+ producer_name="external_event_concerns",
796
+ producer_mode=external_event_concern_mode(),
797
+ producer_revision="external-event-concern-reducer-v1",
798
+ external_event_projection=True,
799
+ ), "", raw_digest
800
+
801
+
802
+ class EventConcernReducer:
803
+ """Idempotently consume the durable host journal into ``ConcernStore``."""
804
+
805
+ def __init__(
806
+ self,
807
+ store: ConcernStore,
808
+ *,
809
+ consumer_id: str = _CONSUMER_ID,
810
+ replay_fn: Callable[..., Mapping[str, Any]] = replay_events,
811
+ current_sequence_fn: Callable[[], int] = current_sequence,
812
+ projection_fn: Callable[
813
+ [Mapping[str, Any]],
814
+ tuple[Optional[EventProjection], str, str],
815
+ ] = project_event,
816
+ mode_fn: Callable[[], str] = event_concern_mode,
817
+ bootstrap_mode_fn: Callable[[], str] = _bootstrap_mode,
818
+ gap_policy_env: str = "COLONY_EVENT_CONCERNS_GAP_POLICY",
819
+ malformed_skip_reason: str = "",
820
+ ) -> None:
821
+ self.store = store
822
+ self.consumer_id = consumer_id
823
+ self._replay = replay_fn
824
+ self._current_sequence = current_sequence_fn
825
+ self._project = projection_fn
826
+ self._mode = mode_fn
827
+ self._bootstrap = bootstrap_mode_fn
828
+ self._gap_policy_env = str(gap_policy_env)
829
+ self._malformed_skip_reason = str(malformed_skip_reason or "")[:100]
830
+
831
+ @property
832
+ def mode(self) -> str:
833
+ return self._mode()
834
+
835
+ def status(self) -> Dict[str, Any]:
836
+ state = self.store.event_reducer_status(self.consumer_id)
837
+ state.update({
838
+ "enabled": self.mode != "off",
839
+ "mode": self.mode,
840
+ "consumer_id": self.consumer_id,
841
+ "healthy": not bool(state.get("last_error")),
842
+ "journal_high_water": int(self._current_sequence()),
843
+ })
844
+ return state
845
+
846
+ @staticmethod
847
+ def _replay_integrity_error(snapshot: Mapping[str, Any]) -> str:
848
+ if snapshot.get("replayError"):
849
+ return "event_journal_unavailable"
850
+ try:
851
+ corrupt = int(snapshot.get("corruptCount") or 0)
852
+ first = int(snapshot.get("firstAvailableSeq") or 0)
853
+ high = int(snapshot.get("journalLastSeq") or 0)
854
+ except (TypeError, ValueError):
855
+ return "event_journal_metadata_invalid"
856
+ if corrupt < 0 or first < 0 or high < 0 or (first and first > high):
857
+ return "event_journal_metadata_invalid"
858
+ if corrupt:
859
+ return f"journal_corruption_detected:{corrupt}"
860
+ if not isinstance(snapshot.get("events") or (), (list, tuple)):
861
+ return "event_journal_metadata_invalid"
862
+ return ""
863
+
864
+ def _gap_policy(self) -> str:
865
+ return os.environ.get(
866
+ self._gap_policy_env, "stop",
867
+ ).strip().lower()
868
+
869
+ def _stop(self, error: str, *, processed: int = 0,
870
+ cursor: Optional[int] = None) -> Dict[str, Any]:
871
+ self.store.set_event_error(self.consumer_id, error)
872
+ result: Dict[str, Any] = {
873
+ "enabled": True, "processed": int(processed), "error": error,
874
+ }
875
+ if cursor is not None:
876
+ result["cursor"] = int(cursor)
877
+ return result
878
+
879
+ def _initialize(self) -> Dict[str, Any]:
880
+ snapshot = self._replay(after_seq=0, limit=1)
881
+ bootstrap = self._bootstrap()
882
+ error = self._replay_integrity_error(snapshot)
883
+ if error:
884
+ initial = 0
885
+ else:
886
+ first = int(snapshot.get("firstAvailableSeq") or 0)
887
+ high_water = int(snapshot.get("journalLastSeq") or 0)
888
+ initial = high_water if bootstrap == "tail" else max(0, first - 1)
889
+ cursor = self.store.initialize_event_cursor(
890
+ self.consumer_id,
891
+ initial,
892
+ bootstrap_mode=bootstrap,
893
+ )
894
+ result = {
895
+ "bootstrapped": True, "bootstrap_mode": bootstrap, "cursor": cursor,
896
+ }
897
+ if error:
898
+ self.store.set_event_error(self.consumer_id, error)
899
+ result["error"] = error
900
+ return result
901
+
902
+ def run_once(self, *, limit: int = 100) -> Dict[str, Any]:
903
+ if self.mode == "off":
904
+ return {"enabled": False, "processed": 0}
905
+ cursor = self.store.event_cursor(self.consumer_id)
906
+ if cursor is None:
907
+ initialized = self._initialize()
908
+ cursor = int(initialized["cursor"])
909
+ if initialized.get("error"):
910
+ return self._stop(str(initialized["error"]), cursor=cursor)
911
+ if initialized["bootstrap_mode"] == "tail":
912
+ return {**initialized, "enabled": True, "processed": 0}
913
+
914
+ batch = self._replay(after_seq=cursor, limit=max(1, min(500, int(limit))))
915
+ integrity_error = self._replay_integrity_error(batch)
916
+ if integrity_error:
917
+ return self._stop(integrity_error, cursor=cursor)
918
+ journal_high = int(batch.get("journalLastSeq") or 0)
919
+ if cursor > journal_high:
920
+ return self._stop(
921
+ f"event_journal_rewind:{cursor}:{journal_high}",
922
+ cursor=cursor,
923
+ )
924
+
925
+ first = int(batch.get("firstAvailableSeq") or 0)
926
+ if first and cursor < first - 1:
927
+ message = f"journal_retention_gap:{cursor}:{first}"
928
+ if self._gap_policy() == "acknowledge":
929
+ cursor = self.store.acknowledge_event_gap(
930
+ self.consumer_id,
931
+ prior_cursor=cursor,
932
+ resume_after=first - 1,
933
+ reason=message,
934
+ )
935
+ batch = self._replay(
936
+ after_seq=cursor, limit=max(1, min(500, int(limit))),
937
+ )
938
+ integrity_error = self._replay_integrity_error(batch)
939
+ if integrity_error:
940
+ return self._stop(integrity_error, cursor=cursor)
941
+ journal_high = int(batch.get("journalLastSeq") or 0)
942
+ if cursor > journal_high:
943
+ return self._stop(
944
+ f"event_journal_rewind:{cursor}:{journal_high}",
945
+ cursor=cursor,
946
+ )
947
+ else:
948
+ return self._stop(message, cursor=cursor)
949
+
950
+ counts: Dict[str, int] = {}
951
+ last_seq = cursor
952
+ for raw in batch.get("events") or []:
953
+ try:
954
+ if not isinstance(raw, Mapping):
955
+ raise ValueError("journal event is not an object")
956
+ sequence = int(raw.get("seq") or 0)
957
+ if sequence <= last_seq:
958
+ raise ValueError("journal event sequence is not increasing")
959
+ if sequence != last_seq + 1:
960
+ message = f"journal_sequence_gap:{last_seq}:{sequence}"
961
+ if self._gap_policy() != "acknowledge":
962
+ return self._stop(
963
+ message, processed=sum(counts.values()),
964
+ cursor=last_seq,
965
+ )
966
+ last_seq = self.store.acknowledge_event_gap(
967
+ self.consumer_id,
968
+ prior_cursor=last_seq,
969
+ resume_after=sequence - 1,
970
+ reason=message,
971
+ )
972
+ sequence, event_id, event_type, _, _ = _event_identity(raw)
973
+ projection, skip_reason, raw_digest = self._project(raw)
974
+ result = self.store.apply_event(
975
+ consumer_id=self.consumer_id,
976
+ event_seq=sequence,
977
+ event_id=event_id,
978
+ event_type=event_type,
979
+ material_digest=(
980
+ projection.material_digest if projection is not None else raw_digest
981
+ ),
982
+ projection=(projection.store_payload() if projection is not None else None),
983
+ skip_reason=skip_reason,
984
+ )
985
+ except Exception as exc:
986
+ # If the journal supplied a usable sequence, record malformed
987
+ # content as skipped and advance. Identity-less records stop
988
+ # the batch because advancing would guess a cursor.
989
+ sequence = int(raw.get("seq") or 0) if isinstance(raw, Mapping) else 0
990
+ if sequence < 1:
991
+ self.store.set_event_error(self.consumer_id, "malformed_event_without_sequence")
992
+ return {
993
+ "enabled": True,
994
+ "processed": sum(counts.values()),
995
+ "error": "malformed_event_without_sequence",
996
+ }
997
+ event_id = _clean_id(raw.get("ulid")) or f"journal-seq-{sequence}"
998
+ event_type = str(raw.get("type") or "malformed")[:128]
999
+ result = self.store.apply_event(
1000
+ consumer_id=self.consumer_id,
1001
+ event_seq=sequence,
1002
+ event_id=event_id,
1003
+ event_type=(event_type if _SAFE_EVENT_TYPE.fullmatch(event_type) else "malformed"),
1004
+ material_digest=_digest({"malformed": raw}),
1005
+ projection=None,
1006
+ skip_reason=(
1007
+ self._malformed_skip_reason
1008
+ or f"malformed_event:{type(exc).__name__}"
1009
+ ),
1010
+ )
1011
+ disposition = str(result.get("disposition") or result.get("status") or "unknown")
1012
+ counts[disposition] = counts.get(disposition, 0) + 1
1013
+ last_seq = max(last_seq, int(result.get("cursor") or last_seq))
1014
+
1015
+ journal_high = int(batch.get("journalLastSeq") or 0)
1016
+ if not bool(batch.get("hasMore")) and journal_high > last_seq:
1017
+ message = f"journal_sequence_gap:{last_seq}:{journal_high + 1}"
1018
+ if self._gap_policy() != "acknowledge":
1019
+ return self._stop(
1020
+ message, processed=sum(counts.values()), cursor=last_seq,
1021
+ )
1022
+ last_seq = self.store.acknowledge_event_gap(
1023
+ self.consumer_id,
1024
+ prior_cursor=last_seq,
1025
+ resume_after=journal_high,
1026
+ reason=message,
1027
+ )
1028
+
1029
+ return {
1030
+ "enabled": True,
1031
+ "processed": sum(counts.values()),
1032
+ "dispositions": counts,
1033
+ "cursor": last_seq,
1034
+ "has_more": bool(batch.get("hasMore")),
1035
+ }
1036
+
1037
+
1038
+ class ExternalEventConcernReducer(EventConcernReducer):
1039
+ """Independent durable consumer for reported external cognition events."""
1040
+
1041
+ def __init__(
1042
+ self,
1043
+ store: ConcernStore,
1044
+ *,
1045
+ consumer_id: str = _EXTERNAL_CONSUMER_ID,
1046
+ replay_fn: Callable[..., Mapping[str, Any]] = replay_events,
1047
+ current_sequence_fn: Callable[[], int] = current_sequence,
1048
+ ) -> None:
1049
+ super().__init__(
1050
+ store,
1051
+ consumer_id=consumer_id,
1052
+ replay_fn=replay_fn,
1053
+ current_sequence_fn=current_sequence_fn,
1054
+ projection_fn=project_external_event,
1055
+ mode_fn=external_event_concern_mode,
1056
+ bootstrap_mode_fn=_external_bootstrap_mode,
1057
+ gap_policy_env="COLONY_EXTERNAL_EVENT_CONCERNS_GAP_POLICY",
1058
+ malformed_skip_reason="malformed_event:ValueError",
1059
+ )
1060
+
1061
+
1062
+ class ConversationTurnConcernReducer(EventConcernReducer):
1063
+ """Independent cursor for eligible, server-attested completed turns."""
1064
+
1065
+ def __init__(
1066
+ self,
1067
+ store: ConcernStore,
1068
+ *,
1069
+ consumer_id: str = _TURN_CONSUMER_ID,
1070
+ replay_fn: Callable[..., Mapping[str, Any]] = replay_events,
1071
+ current_sequence_fn: Callable[[], int] = current_sequence,
1072
+ ) -> None:
1073
+ super().__init__(
1074
+ store,
1075
+ consumer_id=consumer_id,
1076
+ replay_fn=replay_fn,
1077
+ current_sequence_fn=current_sequence_fn,
1078
+ projection_fn=project_conversation_turn,
1079
+ mode_fn=turn_concern_mode,
1080
+ bootstrap_mode_fn=_turn_bootstrap_mode,
1081
+ gap_policy_env="COLONY_TURN_CONCERNS_GAP_POLICY",
1082
+ malformed_skip_reason="malformed_turn_event",
1083
+ )
1084
+
1085
+ @staticmethod
1086
+ def _config_error() -> str:
1087
+ _channels, channels_valid = _turn_channel_config()
1088
+ _prefixes, prefixes_valid = _turn_excluded_session_prefix_config()
1089
+ _platforms, platforms_valid = _turn_excluded_platform_config()
1090
+ if not channels_valid:
1091
+ return "turn_concern_channels_config_invalid"
1092
+ if not prefixes_valid:
1093
+ return "turn_concern_session_prefix_config_invalid"
1094
+ if not platforms_valid:
1095
+ return "turn_concern_excluded_platform_config_invalid"
1096
+ if not _configured_turn_owner():
1097
+ return "turn_concern_owner_identity_config_invalid"
1098
+ return ""
1099
+
1100
+ def run_once(self, *, limit: int = 100) -> Dict[str, Any]:
1101
+ if self.mode == "off":
1102
+ return super().run_once(limit=limit)
1103
+ error = self._config_error()
1104
+ if error:
1105
+ return self._stop(error)
1106
+ # A corrected configuration immediately releases a prior config stop;
1107
+ # the generic reducer then initializes/replays from the retained event.
1108
+ self.store.set_event_error(self.consumer_id, "")
1109
+ return super().run_once(limit=limit)
1110
+
1111
+ def status(self) -> Dict[str, Any]:
1112
+ state = super().status()
1113
+ channels, channels_valid = _turn_channel_config()
1114
+ state["allowed_channels"] = list(channels)
1115
+ state["channel_config_valid"] = channels_valid
1116
+ prefixes, valid = _turn_excluded_session_prefix_config()
1117
+ state["excluded_session_prefixes"] = list(prefixes)
1118
+ state["session_prefix_config_valid"] = valid
1119
+ platforms, platforms_valid = _turn_excluded_platform_config()
1120
+ state["excluded_platforms"] = list(platforms)
1121
+ state["excluded_platform_config_valid"] = platforms_valid
1122
+ state["owner_identity_configured"] = bool(_configured_turn_owner())
1123
+ config_error = self._config_error() if self.mode != "off" else ""
1124
+ state["config_error"] = config_error
1125
+ if config_error:
1126
+ state["last_error"] = config_error
1127
+ state["healthy"] = False
1128
+ return state