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,196 @@
1
+ """Optional local heartbeat reports, separate from canonical executions."""
2
+ import json
3
+ import hashlib
4
+ import math
5
+ import os
6
+ from pathlib import Path
7
+ import time
8
+
9
+
10
+ def _text(value, maximum=128):
11
+ return (isinstance(value, str) and 0 < len(value) <= maximum
12
+ and not any(ord(char) < 32 for char in value))
13
+
14
+
15
+ def _number(value):
16
+ try:
17
+ return type(value) in (int, float) and math.isfinite(value) and value >= 0
18
+ except OverflowError:
19
+ return False
20
+
21
+
22
+ def work_snapshot(value, *, now=None):
23
+ """Bounded metadata from an explicitly enrolled producer, never authority.
24
+
25
+ Producers may attach this snapshot to an existing process heartbeat. The
26
+ public reader knows no private ledger schema, recipients or machine paths.
27
+ """
28
+ if value is None:
29
+ return None
30
+ if not isinstance(value, dict) or value.get('schema') != 'ColonyWorkSnapshotV1':
31
+ return {'schema': 'ColonyWorkSnapshotV1', 'available': False, 'reason': 'unsupported_work_snapshot', 'complete': False}
32
+ if type(value.get('available')) is not bool or not _number(value.get('observed_at')):
33
+ return {'schema': 'ColonyWorkSnapshotV1', 'available': False, 'reason': 'invalid_work_snapshot', 'complete': False}
34
+ now = time.time() if now is None else now
35
+ age = now - value['observed_at']
36
+ result = {'schema': 'ColonyWorkSnapshotV1', 'available': value['available'], 'complete': False, 'observed_at': value['observed_at'],
37
+ 'freshness': 'recent' if 0 <= age <= 120 else 'stale' if age > 120 else 'unknown',
38
+ 'items': [], 'truncated': bool(value.get('truncated')), 'partial': bool(value.get('partial'))}
39
+ for key in ('total', 'pending_total', 'terminal_total', 'unmatched_provider_total'):
40
+ if type(value.get(key)) is int and value[key] >= 0:
41
+ result[key] = value[key]
42
+ for key in ('reason', 'coverage'):
43
+ if _text(value.get(key), 200):
44
+ result[key] = value[key]
45
+ statuses = value.get('source_status')
46
+ if isinstance(statuses, dict):
47
+ result['source_status'] = {key: state for key, state in list(statuses.items())[:8]
48
+ if _text(key, 32) and state in {'observed', 'unavailable', 'partial', 'not_observed'}}
49
+ states = value.get('state_counts')
50
+ if isinstance(states, dict):
51
+ result['state_counts'] = {key: n for key, n in list(states.items())[:12]
52
+ if _text(key, 48) and type(n) is int and n >= 0}
53
+ rows = value.get('items', [])
54
+ if not isinstance(rows, list):
55
+ return {'schema': 'ColonyWorkSnapshotV1', 'available': False, 'reason': 'invalid_work_snapshot', 'complete': False}
56
+ result['truncated'] |= len(rows) > 4
57
+ for row in rows[:4]:
58
+ if not isinstance(row, dict):
59
+ result['partial'] = True
60
+ continue
61
+ item = {'liveness': 'record_only'}
62
+ for key in ('delivery_id', 'intent_id', 'request_key', 'attempt_id', 'state', 'record_kind',
63
+ 'provider_state', 'observation_state'):
64
+ if _text(row.get(key), 256 if key.endswith('_id') or key == 'request_key' else 64):
65
+ item[key] = row[key]
66
+ if not item.get('delivery_id') or not item.get('state'):
67
+ result['partial'] = True
68
+ continue
69
+ for key in ('updated_at', 'provider_updated_at'):
70
+ if _number(row.get(key)):
71
+ item[key] = row[key]
72
+ if type(row.get('transport_observation_pending')) is bool:
73
+ item['transport_observation_pending'] = row['transport_observation_pending']
74
+ digest = row.get('evidence_sha256')
75
+ if isinstance(digest, str) and len(digest) == 64 and all(c in '0123456789abcdef' for c in digest):
76
+ item['evidence_sha256'] = digest
77
+ result['items'].append(item)
78
+ return result
79
+
80
+
81
+ def work_details(value, *, now=None):
82
+ """Project declared work metadata; never follow references or verify effects.
83
+
84
+ These optional fields extend existing heartbeat producers. Bad optional
85
+ values do not hide the underlying legacy state or make a task terminal.
86
+ The same projection is used for automatic request context.
87
+ """
88
+ result = {}
89
+ snapshot = work_snapshot(value.get('work_snapshot'), now=now)
90
+ if snapshot is not None:
91
+ result['work_snapshot'] = snapshot
92
+ for key in ('task_id', 'parent_task_id', 'worker_id', 'kind'):
93
+ if _text(value.get(key)):
94
+ result[key] = value[key]
95
+ for key in ('started_at', 'finished_at'):
96
+ if _number(value.get(key)):
97
+ result[key] = value[key]
98
+ if type(value.get('exit_code')) is int and -(2**31) <= value['exit_code'] < 2**31:
99
+ result['exit_code'] = value['exit_code']
100
+ progress = value.get('progress')
101
+ if isinstance(progress, list):
102
+ selected = []
103
+ for row in progress[:4]:
104
+ if (not isinstance(row, dict) or not _number(row.get('completed'))
105
+ or not _number(row.get('observed_at')) or not _text(row.get('unit'), 32)
106
+ or not _text(row.get('source'), 128)):
107
+ continue
108
+ item = {key: row[key] for key in ('completed', 'unit', 'source', 'observed_at')}
109
+ if 'total' in row:
110
+ if not _number(row['total']) or row['total'] < row['completed']:
111
+ continue
112
+ item['total'] = row['total']
113
+ selected.append(item)
114
+ if selected:
115
+ result['progress'] = selected
116
+ references = value.get('result_refs')
117
+ if isinstance(references, list):
118
+ selected = []
119
+ for row in references[:4]:
120
+ if (not isinstance(row, dict) or not _text(row.get('kind'), 64)
121
+ or not _text(row.get('reference'), 512)):
122
+ continue
123
+ item = {key: row[key] for key in ('kind', 'reference')}
124
+ digest = row.get('sha256')
125
+ if isinstance(digest, str) and len(digest) == 64 and all(c in '0123456789abcdef' for c in digest):
126
+ item['sha256'] = digest
127
+ if _number(row.get('observed_at')):
128
+ item['observed_at'] = row['observed_at']
129
+ # The producer describes its check; the reader does not endorse it.
130
+ if _text(row.get('verification'), 160):
131
+ item['verification'] = row['verification']
132
+ selected.append(item)
133
+ if selected:
134
+ result['result_refs'] = selected
135
+ return result
136
+
137
+
138
+ def reported_worker_view(*, limit=8, now=None):
139
+ configured = os.environ.get('COLONY_WORKER_STATUS_PATHS', '').strip()
140
+ if not configured:
141
+ return None
142
+ view = {'source':'configured_local_heartbeats', 'available':False,
143
+ 'items':[], 'complete':False,
144
+ 'coverage':'reported worker state only; no execution or effect verification'}
145
+ try:
146
+ paths = json.loads(configured)
147
+ if (len(configured)>8192 or not isinstance(paths,dict)
148
+ or not all(isinstance(label,str) and label and isinstance(path,str) and path
149
+ for label,path in paths.items())):
150
+ raise ValueError('Invalid heartbeat mapping')
151
+ except (ValueError,TypeError):
152
+ return {**view,'reason':'invalid_status_configuration'}
153
+ now = time.time() if now is None else now
154
+ bounded = max(1,min(int(limit),8))
155
+ items = []
156
+ for label,filename in list(paths.items())[:bounded]:
157
+ item = {'label':label[:128], 'available':False, 'liveness':'unverified'}
158
+ try:
159
+ path = Path(filename).expanduser()
160
+ if not path.is_file():
161
+ raise ValueError('Heartbeat unavailable')
162
+ with path.open('rb') as stream:
163
+ raw = stream.read(16385)
164
+ value = json.loads(raw)
165
+ if len(raw)>16384 or not isinstance(value,dict):
166
+ raise ValueError('Invalid heartbeat')
167
+ updated = value.get('updated_at')
168
+ state = value.get('state')
169
+ if isinstance(state, dict) and 'work_snapshot' in state:
170
+ value = {**value, 'work_snapshot': state['work_snapshot']}
171
+ state = 'reported_ready' if value.get('ready') is True else 'reported_unready'
172
+ if (not isinstance(state,str) or not state or len(state)>64
173
+ or isinstance(updated,bool) or not isinstance(updated,(int,float))
174
+ or not math.isfinite(updated)):
175
+ raise ValueError('Invalid heartbeat fields')
176
+ age = now-updated
177
+ item.update(available=True,state=state,updated_at=updated,
178
+ age_seconds=round(age,1) if age>=0 else None,
179
+ freshness='recent' if 0<=age<=120 else 'stale' if age>120 else 'unknown',
180
+ status_sha256=hashlib.sha256(raw).hexdigest())
181
+ for key in ('detail_code','release_commit'):
182
+ if isinstance(value.get(key),str):
183
+ item[key] = value[key][:128]
184
+ item.update(work_details(value, now=now))
185
+ if 'task_id' in item:
186
+ finished = item.get('finished_at')
187
+ terminal = (finished is not None and finished <= updated
188
+ and finished >= item.get('started_at', 0)
189
+ and state in {'exited', 'completed', 'failed', 'interrupted', 'cancelled'})
190
+ item['record_kind'] = 'terminal_report' if terminal else 'progress_report'
191
+ except (OSError,ValueError,TypeError):
192
+ item['reason'] = 'heartbeat_unavailable'
193
+ items.append(item)
194
+ partial = any(not item.get('available') or item.get('work_snapshot', {}).get('available') is False
195
+ or item.get('work_snapshot', {}).get('partial') for item in items)
196
+ return {**view,'available':True,'items':items,'truncated':len(paths)>bounded, 'partial': partial}
@@ -0,0 +1,283 @@
1
+ """Attributed source corrections in the existing source ledger and recall packet."""
2
+ from contextlib import closing
3
+ from datetime import datetime, timezone
4
+ import hashlib
5
+ import json
6
+ import re
7
+ import sqlite3
8
+
9
+
10
+ def initialize(conn):
11
+ # No correction text is duplicated here. These references survive erasure
12
+ # so deleting an annotation cannot silently revive its unqualified source.
13
+ conn.execute('''CREATE TABLE IF NOT EXISTS source_annotations (
14
+ annotation_source_id TEXT PRIMARY KEY, target_source_id TEXT NOT NULL,
15
+ target_version TEXT NOT NULL, target_message_hashes_json TEXT NOT NULL,
16
+ request_sha256 TEXT NOT NULL)''')
17
+ conn.execute('CREATE INDEX IF NOT EXISTS source_annotation_target ON source_annotations(target_source_id)')
18
+
19
+
20
+ def append(ledger, *, contact_id, session_id, annotation_id, source_id, source_version,
21
+ excerpt, correction, author_principal):
22
+ from .idempotency import canonical_turn_digest, source_message_hash, SourceErased
23
+ from .audio import source_text
24
+ for value, maximum in ((contact_id, 256), (session_id, 256), (annotation_id, 128),
25
+ (source_id, 256), (excerpt, 4096), (correction, 4096),
26
+ (author_principal, 256)):
27
+ if not isinstance(value, str) or not value.strip() or len(value) > maximum:
28
+ raise ValueError('invalid_source_annotation')
29
+ if not isinstance(source_version, str) or not re.fullmatch('[0-9a-f]{64}', source_version):
30
+ raise ValueError('invalid_source_annotation')
31
+ identity = [contact_id, author_principal, annotation_id]
32
+ annotation_source = 'source-annotation:' + canonical_turn_digest(identity)
33
+ target = {'source_id': source_id, 'source_version': source_version}
34
+ request_digest = canonical_turn_digest([identity, session_id, target, excerpt, correction])
35
+ with closing(ledger._connect()) as conn, conn:
36
+ conn.execute('BEGIN IMMEDIATE')
37
+ prior = conn.execute('SELECT * FROM source_annotations WHERE annotation_source_id=?',
38
+ (annotation_source,)).fetchone()
39
+ if prior and prior['request_sha256'] != request_digest:
40
+ raise ValueError('annotation_id_conflict')
41
+ if conn.execute('SELECT 1 FROM source_erasures WHERE turn_id=?', (annotation_source,)).fetchone():
42
+ raise SourceErased('source_erased')
43
+ row = conn.execute('''SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=?
44
+ AND (scope='person' OR session_id=?)''', (source_id, contact_id, session_id)).fetchone()
45
+ if row is None:
46
+ raise ValueError('source_not_found')
47
+ messages = json.loads(row['messages_json'])
48
+ if canonical_turn_digest(messages) != source_version:
49
+ raise ValueError('source_version_mismatch')
50
+ matched = [source_message_hash(row['session_id'], m) for m in messages
51
+ if excerpt in source_text(m.get('content'))]
52
+ if not matched:
53
+ raise ValueError('source_excerpt_mismatch')
54
+ if prior:
55
+ stored = conn.execute('SELECT messages_json FROM turn_sources WHERE turn_id=?',
56
+ (annotation_source,)).fetchone()
57
+ if stored is None:
58
+ raise SourceErased('source_erased')
59
+ annotation_messages = json.loads(stored['messages_json'])
60
+ else:
61
+ at = datetime.now(timezone.utc).isoformat()
62
+ content = {'target': target, 'excerpt': excerpt, 'correction': correction,
63
+ 'author_principal': author_principal, 'recorded_at': at,
64
+ 'state': 'attributed_correction'}
65
+ annotation_messages = [{'role': 'assistant', 'content': json.dumps(content, ensure_ascii=False),
66
+ '_supplied_sources': [target]}]
67
+ # The dedicated method owns authorship, source and relation in one
68
+ # transaction. Ordinary turn metadata cannot create a correction.
69
+ ledger._validate_dependencies(conn, annotation_source, contact_id, session_id, annotation_messages)
70
+ digest = canonical_turn_digest({'messages': annotation_messages, 'contact_id': contact_id,
71
+ 'session_id': session_id, 'scope': row['scope'], 'occurred_at': at})
72
+ try:
73
+ conn.execute('''INSERT INTO turn_sources
74
+ (turn_id,content_sha256,contact_id,session_id,scope,messages_json,occurred_at)
75
+ VALUES (?,?,?,?,?,?,?)''', (annotation_source, digest, contact_id, session_id,
76
+ row['scope'], json.dumps(annotation_messages, ensure_ascii=True, sort_keys=True,
77
+ separators=(',', ':')), at))
78
+ except sqlite3.IntegrityError as exc:
79
+ raise ValueError('annotation_id_conflict') from exc
80
+ ledger._index_messages(conn, annotation_source, annotation_messages)
81
+ from .source_vectors import enqueue
82
+ enqueue(conn, annotation_source)
83
+ conn.execute('INSERT INTO source_annotations VALUES (?,?,?,?,?)',
84
+ (annotation_source, source_id, source_version, json.dumps(matched), request_digest))
85
+ return {'accepted': True, 'created': prior is None, 'source_id': annotation_source,
86
+ 'source_version': canonical_turn_digest(annotation_messages), 'target': target}
87
+
88
+
89
+ def source_ids(row):
90
+ ids = list(row.get('source_turn_ids') or [])
91
+ if row.get('source_turn_id'):
92
+ ids.append(row['source_turn_id'])
93
+ uri = str(row.get('source_uri') or '')
94
+ if uri.startswith('turn:'):
95
+ ids.append(uri[5:])
96
+ return list(dict.fromkeys(ids))
97
+
98
+
99
+ def expand(ledger, candidates, *, contact_id, session_id, covered=()):
100
+ """Bundle exact revision corrections before ranking and character packing.
101
+
102
+ A source-backed belief/conflict bundle and a derived answer are subject to
103
+ the same rule as direct lexical/semantic excerpts. All discovered notes
104
+ remain attributed evidence; the newest note does not automatically win.
105
+ """
106
+ from .idempotency import canonical_turn_digest, source_message_hash
107
+ from .audio import source_text
108
+ result = []
109
+ with closing(ledger._connect()) as conn:
110
+ cached = {}
111
+
112
+ def source(identifier):
113
+ if identifier not in cached:
114
+ row = conn.execute('''SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=?
115
+ AND (scope='person' OR session_id=?)''', (identifier, contact_id, session_id)).fetchone()
116
+ cached[identifier] = dict(row) if row else None
117
+ if row:
118
+ cached[identifier]['messages'] = json.loads(row['messages_json'])
119
+ return cached[identifier]
120
+
121
+ def candidate_hashes(candidate, identifier):
122
+ # Assertion bundles carry canonical claim membership, not a JSON
123
+ # string to reinterpret. Semantic excerpts already have one hash.
124
+ if '_source_message_hashes' in candidate:
125
+ return set(candidate['_source_message_hashes'].get(identifier, []))
126
+ if candidate.get('source_message_hash'):
127
+ return {candidate['source_message_hash']}
128
+ if candidate.get('kind') == 'source_quote' or candidate.get('source_turn_id'):
129
+ row = source(identifier)
130
+ text, role = candidate.get('content'), candidate.get('role')
131
+ return {source_message_hash(row['session_id'], message) for message in row['messages']
132
+ if (not role or message.get('role') == role)
133
+ and isinstance(text, str) and text
134
+ and text in source_text(message.get('content'))} if row else set()
135
+ # Older graph summaries and supplied parent references attest a
136
+ # whole source version. Do not invent finer ancestry from prose.
137
+ return None
138
+
139
+ for original in candidates:
140
+ ids = source_ids(original)
141
+ pending = [(identifier, candidate_hashes(original, identifier)) for identifier in ids]
142
+ visited, notes = {}, {}
143
+ unavailable = False
144
+ while pending:
145
+ identifier, selected_hashes = pending.pop()
146
+ row = source(identifier)
147
+ if row is None:
148
+ continue
149
+ version = canonical_turn_digest(row['messages'])
150
+ hashes = {source_message_hash(row['session_id'], m) for m in row['messages']}
151
+ selected_hashes = hashes if selected_hashes is None else hashes.intersection(selected_hashes)
152
+ selected_hashes -= visited.get(identifier, set())
153
+ if not selected_hashes:
154
+ continue
155
+ visited.setdefault(identifier, set()).update(selected_hashes)
156
+ for relation in conn.execute('SELECT * FROM source_annotations WHERE target_source_id=?', (identifier,)):
157
+ # A partial erase can change the source revision while
158
+ # retaining the annotated message. Its removed correction
159
+ # still fences that message; unrelated survivors remain.
160
+ same_version = relation['target_version'] == version
161
+ retained_message = selected_hashes.intersection(json.loads(relation['target_message_hashes_json']))
162
+ if not retained_message:
163
+ continue
164
+ annotation = source(relation['annotation_source_id'])
165
+ if annotation is None or not same_version:
166
+ unavailable = True
167
+ break
168
+ data = json.loads(annotation['messages'][0]['content'])
169
+ notes[relation['annotation_source_id']] = data
170
+ pending.append((relation['annotation_source_id'], None))
171
+ if unavailable:
172
+ break
173
+ own_annotation = conn.execute('SELECT * FROM source_annotations WHERE annotation_source_id=?',
174
+ (identifier,)).fetchone()
175
+ for message in row['messages']:
176
+ if (message.get('role') == 'assistant'
177
+ and source_message_hash(row['session_id'], message) in selected_hashes):
178
+ for ref in message.get('_supplied_sources', []):
179
+ parent = source(ref['source_id'])
180
+ if parent and canonical_turn_digest(parent['messages']) == ref['source_version']:
181
+ # Dedicated annotations have exact target message
182
+ # membership. Their own source link must not widen
183
+ # a selected excerpt back to all sibling messages.
184
+ target_hashes = (set(json.loads(own_annotation['target_message_hashes_json']))
185
+ if own_annotation and own_annotation['target_source_id'] == ref['source_id']
186
+ and own_annotation['target_version'] == ref['source_version'] else None)
187
+ pending.append((ref['source_id'], target_hashes))
188
+ if unavailable:
189
+ continue
190
+ if not notes:
191
+ # An empty correction set is also a selection-time snapshot.
192
+ # A first note can arrive while ranking awaits; preserve exact
193
+ # message membership so a sibling's note does not hide this row.
194
+ row = dict(original)
195
+ if visited:
196
+ row['_annotation_source_refs'] = [{'source_id': identifier,
197
+ 'source_version': canonical_turn_digest(source(identifier)['messages'])}
198
+ for identifier in visited]
199
+ row['_annotation_ids'] = ()
200
+ row['_annotation_message_hashes'] = {identifier: sorted(hashes)
201
+ for identifier, hashes in visited.items()}
202
+ result.append(row)
203
+ continue
204
+ row = dict(original)
205
+ if (original.get('kind') == 'belief'
206
+ or 'kind' not in original and not original.get('source_turn_id')):
207
+ row['_recall_memory_id'] = original.get('_recall_memory_id', original['id'])
208
+ # If the annotation itself matched the query, show its exact
209
+ # target excerpt first instead of presenting an orphan note.
210
+ if len(ids) == 1 and ids[0] in notes:
211
+ note = notes[ids[0]]
212
+ original_evidence = {'source': note['target'], 'quote': note['excerpt']}
213
+ row['_annotation_only'] = True
214
+ else:
215
+ original_evidence = {'source_ids': ids, 'content': str(original.get('content') or '')}
216
+ corrections = [dict(data, source_id=identifier) for identifier, data in sorted(
217
+ notes.items(), key=lambda pair: (pair[1]['recorded_at'], pair[0]))]
218
+ row.update(content=json.dumps({'original': original_evidence, 'corrections': corrections,
219
+ 'interpretation': 'Read the original in light of these attributed corrections; conflicting corrections remain unresolved.'},
220
+ ensure_ascii=False), atomic_evidence=True, epistemic_state='correction_evidence',
221
+ source_turn_ids=list(dict.fromkeys(ids + sorted(visited) + list(notes))))
222
+ row['_annotation_source_refs'] = [{'source_id': identifier,
223
+ 'source_version': canonical_turn_digest(source(identifier)['messages'])}
224
+ for identifier in row['source_turn_ids'] if source(identifier)]
225
+ row['_annotation_ids'] = tuple(sorted(notes))
226
+ row['_annotation_message_hashes'] = {identifier: sorted(hashes) for identifier, hashes in visited.items()}
227
+ row['ranking_text'] = '\n'.join(
228
+ 'Attributed correction (not independently verified):\n' + note['correction']
229
+ for note in corrections) + '\nOriginal evidence:\n' + str(
230
+ original.get('ranking_text') or original.get('content') or '')
231
+ row['id'] = 'annotated:' + hashlib.sha256(json.dumps([original['id'], list(notes)], sort_keys=True).encode()).hexdigest()
232
+ result.append(row)
233
+ # A lexical/semantic annotation hit adds no evidence when an original
234
+ # excerpt already carries the same complete correction set. Preserve all
235
+ # distinct original excerpts; this is not content or semantic merging.
236
+ seen = {row['_annotation_ids'] for row in [*covered, *result]
237
+ if row.get('_annotation_ids') and not row.get('_annotation_only')}
238
+ retained = []
239
+ for row in result:
240
+ if row.get('_annotation_only'):
241
+ if row['_annotation_ids'] in seen:
242
+ continue
243
+ seen.add(row['_annotation_ids'])
244
+ retained.append(row)
245
+ return retained
246
+
247
+
248
+ def inputs_unannotated(ledger, refs):
249
+ """Check exact input membership, including retained withdrawn annotations.
250
+
251
+ Callers separately validate source ownership. This small current read also
252
+ serves the existing native freshness round trip; it adds no revision store.
253
+ """
254
+ membership = {}
255
+ for ref in refs:
256
+ membership.setdefault(ref['source_id'], set()).add(ref['input_message_hash'])
257
+ if not membership:
258
+ return True
259
+ with closing(ledger._connect()) as conn:
260
+ rows = conn.execute('SELECT target_source_id,target_message_hashes_json '
261
+ 'FROM source_annotations WHERE target_source_id IN ('
262
+ + ','.join('?' for _ in membership) + ')', list(membership))
263
+ return not any(membership[row[0]].intersection(json.loads(row[1])) for row in rows)
264
+
265
+
266
+ def current_candidates(ledger, candidates, *, contact_id, session_id):
267
+ """Do not publish stale evidence after a correction changes during ranking."""
268
+ required = [ref for row in candidates for ref in row.get('_annotation_source_refs', [])]
269
+ if not required:
270
+ return candidates
271
+ current = {r['source_id']: r['source_version'] for r in ledger.source_references(
272
+ [r['source_id'] for r in required], contact_id=contact_id, session_id=session_id)}
273
+ notes = {}
274
+ with closing(ledger._connect()) as conn:
275
+ for identifier, version in current.items():
276
+ notes[identifier] = {row[0]: set(json.loads(row[1])) for row in conn.execute('''SELECT annotation_source_id,target_message_hashes_json
277
+ FROM source_annotations WHERE target_source_id=? AND target_version=?''', (identifier, version))}
278
+ return [row for row in candidates if all(current.get(ref['source_id']) == ref['source_version']
279
+ for ref in row.get('_annotation_source_refs', [])) and (
280
+ not row.get('_annotation_source_refs') or set(row['_annotation_ids']) == set().union(
281
+ *({identifier for identifier, hashes in notes.get(ref['source_id'], {}).items()
282
+ if hashes.intersection(row['_annotation_message_hashes'].get(ref['source_id'], []))}
283
+ for ref in row['_annotation_source_refs'])))]
@@ -0,0 +1,154 @@
1
+ """Explicit participant corrections without rewriting or erasing source text.
2
+
3
+ The host authorizes the exact old/new people and selected sources. This module
4
+ owns the source transaction; contact handles live in their existing store and
5
+ are reconciled separately using the same operation ID. No source is inferred
6
+ from a name, similarity, session or contact-wide history sweep.
7
+ """
8
+ from contextlib import closing
9
+ from datetime import datetime, timezone
10
+ import hashlib
11
+ import json
12
+
13
+
14
+ def initialize(conn):
15
+ conn.execute('''CREATE TABLE IF NOT EXISTS source_attribution_operations (
16
+ operation_id TEXT PRIMARY KEY,request_sha256 TEXT NOT NULL,
17
+ result_json TEXT NOT NULL,recorded_at TEXT NOT NULL)''')
18
+ conn.execute('''CREATE TABLE IF NOT EXISTS source_attribution_revisions (
19
+ operation_id TEXT NOT NULL,source_id TEXT NOT NULL,old_contact_id TEXT NOT NULL,
20
+ contact_id TEXT NOT NULL,original_digest TEXT NOT NULL,source_version TEXT NOT NULL,
21
+ PRIMARY KEY(operation_id,source_id))''')
22
+ conn.execute('CREATE INDEX IF NOT EXISTS attribution_source ON source_attribution_revisions(source_id)')
23
+ conn.execute('''CREATE TABLE IF NOT EXISTS source_attribution_invalidations (
24
+ source_id TEXT NOT NULL,corrected_source_id TEXT NOT NULL,operation_id TEXT NOT NULL,
25
+ PRIMARY KEY(source_id,corrected_source_id,operation_id))''')
26
+
27
+
28
+ def correct(ledger, *, operation_id, performed_by, old_contact_id, contact_id,
29
+ source_ids, evidence_refs):
30
+ """Correct an explicit source set and invalidate derived interpretations.
31
+
32
+ The original ingest digest, messages, media links, session and timestamps
33
+ remain unchanged. Changing the participant invalidates cached source views
34
+ and their current projections; it does not adopt old judgments for a new
35
+ person. Returned refs let other stores purge their own source projections.
36
+ """
37
+ from apsimo.contacts.identity_links import _refs, _json
38
+ from .idempotency import canonical_turn_digest, SourceErased
39
+ selected, evidence = _refs(source_ids), _refs(evidence_refs)
40
+ if not selected or not evidence or old_contact_id == contact_id:
41
+ raise ValueError('invalid_source_attribution')
42
+ for value in (operation_id, performed_by, old_contact_id, contact_id):
43
+ if not isinstance(value, str) or not value.strip() or len(value) > 256:
44
+ raise ValueError('invalid_source_attribution')
45
+ request_hash = hashlib.sha256(_json([performed_by, old_contact_id, contact_id, selected, evidence]).encode()).hexdigest()
46
+ with closing(ledger._connect()) as conn, conn:
47
+ conn.execute('BEGIN IMMEDIATE')
48
+ initialize(conn)
49
+ prior = conn.execute('SELECT * FROM source_attribution_operations WHERE operation_id=?', (operation_id,)).fetchone()
50
+ if prior:
51
+ if prior['request_sha256'] != request_hash:
52
+ raise ValueError('source_attribution_operation_conflict')
53
+ return json.loads(prior['result_json'])
54
+ rows = []
55
+ for sid in selected:
56
+ if conn.execute('SELECT 1 FROM source_erasures WHERE turn_id=?', (sid,)).fetchone():
57
+ raise SourceErased('source_erased')
58
+ row = conn.execute("SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=? AND scope='person'",
59
+ (sid, old_contact_id)).fetchone()
60
+ if row is None:
61
+ raise ValueError('source_attribution_preimage_changed')
62
+ rows.append(row)
63
+ # Discover source-linked descendant answers for invalidation, not silent
64
+ # reassignment. Their original prose remains historical source evidence.
65
+ descendants = set()
66
+ pending = set(selected)
67
+ candidates = conn.execute("SELECT turn_id,messages_json FROM turn_sources WHERE contact_id=?", (old_contact_id,)).fetchall()
68
+ while pending:
69
+ found = set()
70
+ for row in candidates:
71
+ if row['turn_id'] in selected or row['turn_id'] in descendants:
72
+ continue
73
+ if any(ref.get('source_id') in pending for message in json.loads(row['messages_json'])
74
+ for ref in message.get('_supplied_sources', [])):
75
+ found.add(row['turn_id'])
76
+ descendants.update(found)
77
+ pending = found
78
+ affected = sorted(set(selected) | descendants)
79
+ for sid in descendants:
80
+ # Existing projection invalidation table owns graph/ToM cleanup.
81
+ for original in selected:
82
+ conn.execute('INSERT OR IGNORE INTO source_projection_erasures VALUES (?,?)', (sid, original))
83
+ conn.execute('INSERT OR IGNORE INTO source_attribution_invalidations VALUES (?,?,?)',
84
+ (sid, original, operation_id))
85
+ from apsimo.beliefs.source_projection import erase_removed as erase_claims, enqueue
86
+ from apsimo.self_model.perspective import erase_removed as erase_preferences
87
+ from apsimo.self_model.judgments import erase_removed as erase_judgments
88
+ from .source_vectors import enqueue as enqueue_vectors
89
+ for row in rows:
90
+ sid = row['turn_id']
91
+ messages = json.loads(row['messages_json'])
92
+ prior_job = conn.execute('SELECT timezone FROM source_claim_jobs WHERE turn_id=?', (sid,)).fetchone()
93
+ conn.execute('INSERT INTO source_attribution_revisions VALUES (?,?,?,?,?,?)',
94
+ (operation_id, sid, old_contact_id, contact_id, row['content_sha256'], canonical_turn_digest(messages)))
95
+ # Revoke claim-worker leases before changing their input attribution.
96
+ erase_claims(conn, sid, row['session_id'], [])
97
+ erase_preferences(conn, sid, row['session_id'], [])
98
+ erase_judgments(conn, sid, row['session_id'], [])
99
+ conn.execute('UPDATE turn_sources SET contact_id=? WHERE turn_id=?', (contact_id, sid))
100
+ conn.execute('UPDATE turn_ingestion SET response_json=NULL,error=NULL WHERE turn_id=?', (sid,))
101
+ # Preserve admission and timezone. Reviewed source-only imports
102
+ # must not start learning merely because their identity was fixed.
103
+ if prior_job:
104
+ enqueue(conn, sid, messages, scope=row['scope'], timezone_name=prior_job['timezone'])
105
+ enqueue_vectors(conn, sid)
106
+ for sid in descendants:
107
+ row = conn.execute('SELECT session_id FROM turn_sources WHERE turn_id=?', (sid,)).fetchone()
108
+ if row:
109
+ conn.execute('UPDATE turn_ingestion SET response_json=NULL,error=NULL WHERE turn_id=?', (sid,))
110
+ erase_claims(conn, sid, row['session_id'], [])
111
+ erase_preferences(conn, sid, row['session_id'], [])
112
+ erase_judgments(conn, sid, row['session_id'], [])
113
+ try:
114
+ from apsimo.self_model.appraisals import invalidate_source_attribution
115
+ except ModuleNotFoundError as exc:
116
+ if exc.name != 'apsimo.self_model.appraisals':
117
+ raise
118
+ else:
119
+ invalidate_source_attribution(conn, affected, old_contact_id, contact_id)
120
+ result = {'schema': 'SourceAttributionCorrectionV1', 'operation_id': operation_id,
121
+ 'old_contact_id': old_contact_id, 'contact_id': contact_id,
122
+ 'source_ids': selected, 'affected_source_ids': affected,
123
+ 'invalidated_source_ids': sorted(descendants), 'evidence_refs': evidence,
124
+ 'recorded_at': datetime.now(timezone.utc).isoformat(), 'performed_by': performed_by,
125
+ 'authority_granted': False, 'source_text_preserved': True}
126
+ conn.execute('INSERT INTO source_attribution_operations VALUES (?,?,?,?)',
127
+ (operation_id, request_hash, _json(result), result['recorded_at']))
128
+ return result
129
+
130
+
131
+ def history(ledger, *, source_id, contact_id):
132
+ """Correction provenance only for the source's currently authorized person."""
133
+ with closing(ledger._connect()) as conn:
134
+ if not conn.execute('SELECT 1 FROM sqlite_master WHERE name=?', ('source_attribution_revisions',)).fetchone():
135
+ return []
136
+ if not conn.execute('SELECT 1 FROM turn_sources WHERE turn_id=? AND contact_id=?', (source_id, contact_id)).fetchone():
137
+ return []
138
+ rows = conn.execute('''SELECT r.*,o.recorded_at FROM source_attribution_revisions r
139
+ JOIN source_attribution_operations o USING(operation_id) WHERE r.source_id=?
140
+ ORDER BY o.recorded_at,r.operation_id''', (source_id,)).fetchall()
141
+ return [dict(row) for row in rows]
142
+
143
+
144
+ def is_invalidated(conn, source_id):
145
+ if not conn.execute("SELECT 1 FROM sqlite_master WHERE name='source_attribution_invalidations'").fetchone():
146
+ return False
147
+ return bool(conn.execute('SELECT 1 FROM source_attribution_invalidations WHERE source_id=? LIMIT 1',
148
+ (source_id,)).fetchone())
149
+
150
+
151
+ def visible_hits(ledger, hits):
152
+ """Drop stale derived-answer copies, never the corrected direct evidence."""
153
+ with closing(ledger._connect()) as conn:
154
+ return [hit for hit in hits if not is_invalidated(conn, hit.get('turn_id') or hit.get('source_turn_id'))]