@prismer/runtime 2.0.7 → 2.2.55

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 (291) hide show
  1. package/CHANGELOG.md +3631 -0
  2. package/README.md +34 -12
  3. package/apc/skills/FIELD-DICTIONARY.md +111 -0
  4. package/apc/skills/bug-reproduce/SKILL.md +150 -0
  5. package/apc/skills/bug-reproduce/skill.json +96 -0
  6. package/apc/skills/code-review/SKILL.md +198 -0
  7. package/apc/skills/code-review/skill.json +124 -0
  8. package/apc/skills/design-review/SKILL.md +122 -0
  9. package/apc/skills/design-review/skill.json +88 -0
  10. package/apc/skills/doc-sync/SKILL.md +168 -0
  11. package/apc/skills/doc-sync/skill.json +81 -0
  12. package/apc/skills/env-doctor/SKILL.md +194 -0
  13. package/apc/skills/env-doctor/skill.json +209 -0
  14. package/apc/skills/git-ops/SKILL.md +189 -0
  15. package/apc/skills/git-ops/skill.json +94 -0
  16. package/apc/skills/impact-trace/SKILL.md +168 -0
  17. package/apc/skills/impact-trace/skill.json +104 -0
  18. package/apc/skills/observability/SKILL.md +195 -0
  19. package/apc/skills/observability/skill.json +116 -0
  20. package/apc/skills/release-db-config-sync/SKILL.md +186 -0
  21. package/apc/skills/release-db-config-sync/skill.json +109 -0
  22. package/apc/skills/release-ota-promote/SKILL.md +195 -0
  23. package/apc/skills/release-ota-promote/skill.json +176 -0
  24. package/apc/skills/release-preflight/SKILL.md +174 -0
  25. package/apc/skills/release-preflight/skill.json +175 -0
  26. package/apc/skills/release-rollback/SKILL.md +214 -0
  27. package/apc/skills/release-rollback/skill.json +230 -0
  28. package/apc/skills/release-tag/SKILL.md +194 -0
  29. package/apc/skills/release-tag/skill.json +94 -0
  30. package/apc/skills/releasing-prod/SKILL.md +49 -0
  31. package/apc/skills/releasing-test/SKILL.md +135 -0
  32. package/apc/skills/sdk-release/SKILL.md +200 -0
  33. package/apc/skills/spec-intake/SKILL.md +169 -0
  34. package/apc/skills/spec-intake/skill.json +93 -0
  35. package/apc/skills/test-result-feedback/SKILL.md +239 -0
  36. package/apc/skills/test-result-feedback/skill.json +193 -0
  37. package/apc/skills/test-runner/SKILL.md +169 -0
  38. package/apc/skills/test-runner/skill.json +103 -0
  39. package/apc/skills/ui-align/SKILL.md +209 -0
  40. package/apc/skills/ui-align/skill.json +114 -0
  41. package/apc/skills/ui-canvas/SKILL.md +148 -0
  42. package/apc/skills/ui-canvas/skill.json +127 -0
  43. package/built-in-skills/agent-coordination/SKILL.md +257 -0
  44. package/built-in-skills/agent-meta/SKILL.md +53 -0
  45. package/built-in-skills/assets/SKILL.md +133 -0
  46. package/built-in-skills/browser-use/SKILL.md +93 -0
  47. package/built-in-skills/canvas-design/LICENSE.txt +202 -0
  48. package/built-in-skills/canvas-design/SKILL.md +157 -0
  49. package/built-in-skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  50. package/built-in-skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  51. package/built-in-skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  52. package/built-in-skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +93 -0
  53. package/built-in-skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  54. package/built-in-skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +93 -0
  55. package/built-in-skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
  56. package/built-in-skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  57. package/built-in-skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  58. package/built-in-skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  59. package/built-in-skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  60. package/built-in-skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  61. package/built-in-skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  62. package/built-in-skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  63. package/built-in-skills/canvas-design/canvas-fonts/DMMono-OFL.txt +93 -0
  64. package/built-in-skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
  65. package/built-in-skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +94 -0
  66. package/built-in-skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
  67. package/built-in-skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
  68. package/built-in-skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +93 -0
  69. package/built-in-skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
  70. package/built-in-skills/canvas-design/canvas-fonts/Gloock-OFL.txt +93 -0
  71. package/built-in-skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
  72. package/built-in-skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  73. package/built-in-skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  74. package/built-in-skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  75. package/built-in-skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  76. package/built-in-skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  77. package/built-in-skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  78. package/built-in-skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  79. package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  80. package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  81. package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  82. package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  83. package/built-in-skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  84. package/built-in-skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  85. package/built-in-skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  86. package/built-in-skills/canvas-design/canvas-fonts/Italiana-OFL.txt +93 -0
  87. package/built-in-skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
  88. package/built-in-skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  89. package/built-in-skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  90. package/built-in-skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  91. package/built-in-skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
  92. package/built-in-skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
  93. package/built-in-skills/canvas-design/canvas-fonts/Jura-OFL.txt +93 -0
  94. package/built-in-skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  95. package/built-in-skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  96. package/built-in-skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
  97. package/built-in-skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  98. package/built-in-skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
  99. package/built-in-skills/canvas-design/canvas-fonts/Lora-OFL.txt +93 -0
  100. package/built-in-skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
  101. package/built-in-skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
  102. package/built-in-skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +93 -0
  103. package/built-in-skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
  104. package/built-in-skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  105. package/built-in-skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  106. package/built-in-skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
  107. package/built-in-skills/canvas-design/canvas-fonts/Outfit-OFL.txt +93 -0
  108. package/built-in-skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
  109. package/built-in-skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  110. package/built-in-skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +93 -0
  111. package/built-in-skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +93 -0
  112. package/built-in-skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  113. package/built-in-skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  114. package/built-in-skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +93 -0
  115. package/built-in-skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  116. package/built-in-skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +93 -0
  117. package/built-in-skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  118. package/built-in-skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  119. package/built-in-skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +93 -0
  120. package/built-in-skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
  121. package/built-in-skills/canvas-design/canvas-fonts/Tektur-OFL.txt +93 -0
  122. package/built-in-skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
  123. package/built-in-skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
  124. package/built-in-skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  125. package/built-in-skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
  126. package/built-in-skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +93 -0
  127. package/built-in-skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
  128. package/built-in-skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +93 -0
  129. package/built-in-skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  130. package/built-in-skills/claim-agent-ownership/SKILL.md +255 -0
  131. package/built-in-skills/claude-api/LICENSE.txt +202 -0
  132. package/built-in-skills/claude-api/SKILL.md +325 -0
  133. package/built-in-skills/claude-api/csharp/claude-api.md +402 -0
  134. package/built-in-skills/claude-api/curl/examples.md +216 -0
  135. package/built-in-skills/claude-api/curl/managed-agents.md +336 -0
  136. package/built-in-skills/claude-api/go/claude-api.md +421 -0
  137. package/built-in-skills/claude-api/go/managed-agents/README.md +561 -0
  138. package/built-in-skills/claude-api/java/claude-api.md +432 -0
  139. package/built-in-skills/claude-api/java/managed-agents/README.md +442 -0
  140. package/built-in-skills/claude-api/php/claude-api.md +375 -0
  141. package/built-in-skills/claude-api/php/managed-agents/README.md +435 -0
  142. package/built-in-skills/claude-api/python/claude-api/README.md +420 -0
  143. package/built-in-skills/claude-api/python/claude-api/batches.md +185 -0
  144. package/built-in-skills/claude-api/python/claude-api/files-api.md +165 -0
  145. package/built-in-skills/claude-api/python/claude-api/streaming.md +162 -0
  146. package/built-in-skills/claude-api/python/claude-api/tool-use.md +590 -0
  147. package/built-in-skills/claude-api/python/managed-agents/README.md +332 -0
  148. package/built-in-skills/claude-api/ruby/claude-api.md +113 -0
  149. package/built-in-skills/claude-api/ruby/managed-agents/README.md +389 -0
  150. package/built-in-skills/claude-api/shared/agent-design.md +101 -0
  151. package/built-in-skills/claude-api/shared/error-codes.md +213 -0
  152. package/built-in-skills/claude-api/shared/live-sources.md +135 -0
  153. package/built-in-skills/claude-api/shared/managed-agents-api-reference.md +378 -0
  154. package/built-in-skills/claude-api/shared/managed-agents-client-patterns.md +209 -0
  155. package/built-in-skills/claude-api/shared/managed-agents-core.md +238 -0
  156. package/built-in-skills/claude-api/shared/managed-agents-environments.md +215 -0
  157. package/built-in-skills/claude-api/shared/managed-agents-events.md +195 -0
  158. package/built-in-skills/claude-api/shared/managed-agents-memory.md +197 -0
  159. package/built-in-skills/claude-api/shared/managed-agents-multiagent.md +99 -0
  160. package/built-in-skills/claude-api/shared/managed-agents-onboarding.md +114 -0
  161. package/built-in-skills/claude-api/shared/managed-agents-outcomes.md +106 -0
  162. package/built-in-skills/claude-api/shared/managed-agents-overview.md +68 -0
  163. package/built-in-skills/claude-api/shared/managed-agents-self-hosted-sandboxes.md +173 -0
  164. package/built-in-skills/claude-api/shared/managed-agents-tools.md +321 -0
  165. package/built-in-skills/claude-api/shared/managed-agents-webhooks.md +110 -0
  166. package/built-in-skills/claude-api/shared/model-migration.md +779 -0
  167. package/built-in-skills/claude-api/shared/models.md +121 -0
  168. package/built-in-skills/claude-api/shared/prompt-caching.md +171 -0
  169. package/built-in-skills/claude-api/shared/tool-use-concepts.md +327 -0
  170. package/built-in-skills/claude-api/typescript/claude-api/README.md +333 -0
  171. package/built-in-skills/claude-api/typescript/claude-api/batches.md +106 -0
  172. package/built-in-skills/claude-api/typescript/claude-api/files-api.md +98 -0
  173. package/built-in-skills/claude-api/typescript/claude-api/streaming.md +178 -0
  174. package/built-in-skills/claude-api/typescript/claude-api/tool-use.md +527 -0
  175. package/built-in-skills/claude-api/typescript/managed-agents/README.md +359 -0
  176. package/built-in-skills/codebase-design/DEEPENING.md +37 -0
  177. package/built-in-skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  178. package/built-in-skills/codebase-design/LICENSE +21 -0
  179. package/built-in-skills/codebase-design/SKILL.md +116 -0
  180. package/built-in-skills/conversation-compaction/SKILL.md +114 -0
  181. package/built-in-skills/council-creator/SKILL.md +426 -0
  182. package/built-in-skills/diagnosing-bugs/LICENSE +21 -0
  183. package/built-in-skills/diagnosing-bugs/SKILL.md +136 -0
  184. package/built-in-skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  185. package/built-in-skills/doc-coauthoring/SKILL.md +376 -0
  186. package/built-in-skills/document-generation/SKILL.md +105 -0
  187. package/built-in-skills/domain-modeling/ADR-FORMAT.md +47 -0
  188. package/built-in-skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  189. package/built-in-skills/domain-modeling/LICENSE +21 -0
  190. package/built-in-skills/domain-modeling/SKILL.md +76 -0
  191. package/built-in-skills/frontend-design/LICENSE.txt +177 -0
  192. package/built-in-skills/frontend-design/SKILL.md +43 -0
  193. package/built-in-skills/human-approval/SKILL.md +129 -0
  194. package/built-in-skills/image-generate/SKILL.md +128 -0
  195. package/built-in-skills/image-generate/scripts/generate-and-deliver.mjs +289 -0
  196. package/built-in-skills/ingest/SKILL.md +73 -0
  197. package/built-in-skills/internal-comms/LICENSE.txt +202 -0
  198. package/built-in-skills/internal-comms/SKILL.md +33 -0
  199. package/built-in-skills/internal-comms/examples/3p-updates.md +47 -0
  200. package/built-in-skills/internal-comms/examples/company-newsletter.md +65 -0
  201. package/built-in-skills/internal-comms/examples/faq-answers.md +30 -0
  202. package/built-in-skills/internal-comms/examples/general-comms.md +16 -0
  203. package/built-in-skills/liteparse/SKILL.md +176 -0
  204. package/built-in-skills/mcp-builder/LICENSE.txt +202 -0
  205. package/built-in-skills/mcp-builder/SKILL.md +237 -0
  206. package/built-in-skills/mcp-builder/reference/evaluation.md +602 -0
  207. package/built-in-skills/mcp-builder/reference/mcp_best_practices.md +249 -0
  208. package/built-in-skills/mcp-builder/reference/node_mcp_server.md +970 -0
  209. package/built-in-skills/mcp-builder/reference/python_mcp_server.md +719 -0
  210. package/built-in-skills/mcp-builder/scripts/connections.py +151 -0
  211. package/built-in-skills/mcp-builder/scripts/evaluation.py +373 -0
  212. package/built-in-skills/mcp-builder/scripts/example_evaluation.xml +22 -0
  213. package/built-in-skills/mcp-builder/scripts/requirements.txt +2 -0
  214. package/built-in-skills/memory/SKILL.md +471 -0
  215. package/built-in-skills/memory-dream/SKILL.md +339 -0
  216. package/built-in-skills/office-artifacts/SKILL.md +211 -0
  217. package/built-in-skills/okr/SKILL.md +154 -0
  218. package/built-in-skills/persona/SKILL.md +81 -0
  219. package/built-in-skills/persona-generator/SKILL.md +296 -0
  220. package/built-in-skills/pkf-svg/SKILL.md +253 -0
  221. package/built-in-skills/pkf-writing/SKILL.md +236 -0
  222. package/built-in-skills/prismer-im-collab/SKILL.md +168 -0
  223. package/built-in-skills/proactivity/SKILL.md +84 -0
  224. package/built-in-skills/remotion/SKILL.md +431 -0
  225. package/built-in-skills/role-builder/SKILL.md +203 -0
  226. package/built-in-skills/role-builder/scripts/author-role.mjs +334 -0
  227. package/built-in-skills/role-builder/scripts/ingest-role.mjs +223 -0
  228. package/built-in-skills/role-builder/scripts/instantiate-and-run.mjs +290 -0
  229. package/built-in-skills/role-builder/scripts/operation-harness.mjs +267 -0
  230. package/built-in-skills/skill-authoring/SKILL.md +134 -0
  231. package/built-in-skills/skill-authoring/skill.json +74 -0
  232. package/built-in-skills/skill-builder/SKILL.md +171 -0
  233. package/built-in-skills/skill-builder/scripts/ingest.mjs +265 -0
  234. package/built-in-skills/skill-creator/LICENSE.txt +202 -0
  235. package/built-in-skills/skill-creator/SKILL.md +227 -0
  236. package/built-in-skills/skill-creator/agents/analyzer.md +274 -0
  237. package/built-in-skills/skill-creator/agents/comparator.md +202 -0
  238. package/built-in-skills/skill-creator/agents/grader.md +223 -0
  239. package/built-in-skills/skill-creator/assets/eval_review.html +146 -0
  240. package/built-in-skills/skill-creator/eval-viewer/generate_review.py +471 -0
  241. package/built-in-skills/skill-creator/eval-viewer/viewer.html +1325 -0
  242. package/built-in-skills/skill-creator/references/external-library-import.md +110 -0
  243. package/built-in-skills/skill-creator/references/schemas.md +430 -0
  244. package/built-in-skills/skill-creator/scripts/__init__.py +0 -0
  245. package/built-in-skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  246. package/built-in-skills/skill-creator/scripts/generate_report.py +326 -0
  247. package/built-in-skills/skill-creator/scripts/import-library.mjs +475 -0
  248. package/built-in-skills/skill-creator/scripts/improve_description.py +247 -0
  249. package/built-in-skills/skill-creator/scripts/package_skill.py +136 -0
  250. package/built-in-skills/skill-creator/scripts/quick_validate.py +103 -0
  251. package/built-in-skills/skill-creator/scripts/run_eval.py +310 -0
  252. package/built-in-skills/skill-creator/scripts/run_loop.py +328 -0
  253. package/built-in-skills/skill-creator/scripts/utils.py +47 -0
  254. package/built-in-skills/slack-gif-creator/LICENSE.txt +202 -0
  255. package/built-in-skills/slack-gif-creator/SKILL.md +291 -0
  256. package/built-in-skills/slack-gif-creator/core/easing.py +234 -0
  257. package/built-in-skills/slack-gif-creator/core/frame_composer.py +176 -0
  258. package/built-in-skills/slack-gif-creator/core/gif_builder.py +269 -0
  259. package/built-in-skills/slack-gif-creator/core/validators.py +136 -0
  260. package/built-in-skills/slack-gif-creator/requirements.txt +4 -0
  261. package/built-in-skills/tasks/SKILL.md +413 -0
  262. package/built-in-skills/tdd/LICENSE +21 -0
  263. package/built-in-skills/tdd/SKILL.md +110 -0
  264. package/built-in-skills/tdd/mocking.md +59 -0
  265. package/built-in-skills/tdd/refactoring.md +10 -0
  266. package/built-in-skills/tdd/tests.md +61 -0
  267. package/built-in-skills/team/SKILL.md +77 -0
  268. package/built-in-skills/web-artifacts-builder/LICENSE.txt +202 -0
  269. package/built-in-skills/web-artifacts-builder/SKILL.md +105 -0
  270. package/built-in-skills/web-artifacts-builder/scripts/bundle-artifact.sh +54 -0
  271. package/built-in-skills/web-artifacts-builder/scripts/init-artifact.sh +334 -0
  272. package/built-in-skills/web-artifacts-builder/scripts/shadcn-components.tar.gz +0 -0
  273. package/built-in-skills/webapp-testing/LICENSE.txt +202 -0
  274. package/built-in-skills/webapp-testing/SKILL.md +97 -0
  275. package/built-in-skills/webapp-testing/examples/console_logging.py +35 -0
  276. package/built-in-skills/webapp-testing/examples/element_discovery.py +40 -0
  277. package/built-in-skills/webapp-testing/examples/static_html_automation.py +33 -0
  278. package/built-in-skills/webapp-testing/scripts/with_server.py +106 -0
  279. package/built-in-skills/wechat-pay/SKILL.md +59 -0
  280. package/dist/cli.cjs +72577 -16101
  281. package/dist/cli.js +72752 -16232
  282. package/dist/index.cjs +72632 -16024
  283. package/dist/index.d.cts +4956 -640
  284. package/dist/index.d.ts +4956 -640
  285. package/dist/index.js +72564 -15963
  286. package/package.json +39 -6
  287. package/plugins/memory/prismer/__init__.py +1211 -0
  288. package/plugins/memory/prismer/plugin.yaml +8 -0
  289. package/plugins/memory/prismer/tool-schemas.generated.json +249 -0
  290. package/plugins/tools/prismer-recall/__init__.py +282 -0
  291. package/plugins/tools/prismer-recall/plugin.yaml +15 -0
package/CHANGELOG.md CHANGED
@@ -1,7 +1,3625 @@
1
1
  # Changelog — @prismer/runtime
2
2
 
3
+ ## 2.2.13 — 2026-08-17
4
+
5
+ - ACS boot recovery fixes (merged via fix/acs-boot-recovery): manifest Pod
6
+ reachability base, Dockerfile Hermes install retry on China egress,
7
+ canonical millisecond ISO timestamps in the sandbox manager, runner empty
8
+ adoption storm guard, and dev-local stream + reclaim.
9
+ - Runtime version bumped 2.2.12 → 2.2.13 because daemon sources changed
10
+ since the 2.2.12 GA publish (immutable content-addressed releases).
11
+
3
12
  ## Unreleased
4
13
 
14
+ ### SDK 边界 Wave2 B2/B3 — daemon 离线改名落库 + CLI rename --handle
15
+
16
+ - **fix(daemon): `onAgentChanged` 补 local.db `agents` 表持久化**——改名
17
+ (PATCH /agents/:id)不 bump profile version ⇒ host.acked / syncProfileFromCloud
18
+ 不会触发,旧实现改完只驻内存,重启后 declare/healthz/task-agentName 全回退旧名。
19
+ 现在 `agent.changed` 的 displayName 直达 `UPDATE agents SET name`(dirty=0),
20
+ 是离线窗口改名收敛的唯一落点;`AgentChangedPayload.fields` 同步补 `username`
21
+ 字段声明(daemon 不持久化——本地表无 username 列)。
22
+ - **feat(cli): `prismer agent rename` 增 `--handle <new-username>`**——PATCH 体
23
+ `{ displayName, username? }`(handle 传时才带),与 PATCH /agents/:id 双字段
24
+ 契约对齐;handle 在 CLI 侧先按 `/^[a-z][a-z0-9-]{2,30}$/` 校验(坏轴根本不出网)。
25
+ - **fix(cli): register 校验收窄到 `/^[a-z][a-z0-9-]{2,30}$/`**——旧轴
26
+ `[a-zA-Z0-9_-]`(大写/下划线,如 `Prod_Manager`)不再可新建(CLI 侧 400
27
+ 之前拒绝;云端 register 端点不动,存量 handle 保留)。`slugifyUsername`
28
+ 同步收窄:不再产出 `_`,数字开头派生名补 `agent-` 前缀保证字母开头。
29
+
30
+ ### bugfix211 G2-R R-2 — runtime terminal-sessions 插件模块(workspace 终端)
31
+
32
+ - **feat(daemon): 新增 `daemon/terminal-sessions/` 插件模块**——daemon 协议
33
+ PTY(spec §T-1 唯一轨),wire 四方法 `terminal.open/write/resize/close` +
34
+ 事件 `terminal.opened/data/exit/error`;会话卫生:per-daemon 并发上限 2、
35
+ 空闲回收(`PRISMER_TERMINAL_IDLE_TIMEOUT_MS`,默认 10min、floor 30s,按
36
+ device-metrics interval 的 ConfigDelivery 模式)、ACL 逐字复用
37
+ shell-executor 的 `enabled`/`allowedWorkspaces`/cwd 语义。能力位
38
+ `runtime.terminal` 仅在 node-pty 真正加载到时宣告。
39
+ - **node-pty 分发形态(owner 裁决方案 A)**:只进 devDependencies,OTA
40
+ bundle 持续 JS-only(staging `npm ci --omit=dev --ignore-scripts` 不装
41
+ dev + `assertNoNativePayload` 对任何 `.node` die 兜底);pod 侧经 image
42
+ native ABI floor 供给(Dockerfile.base 源码编译 + install-builtin-runtime
43
+ `--native-module-root` 借用,循 better-sqlite3 先例)。`loadPty()` 两路
44
+ 懒加载:常规 require(本机 dev/测试)→ floor 根
45
+ `/opt/prismer/native/node_modules` → null ⇒ 模块降级(不宣告能力位,
46
+ terminal.open 回类型化 `terminal_unavailable`,绝不伪造 PTY)。
47
+ - 测试:`test/terminal-sessions.test.ts` 30 例(真 PTY 生命周期 echo/exit
48
+ 码/resize、上限/ACL/cwd 拒、idle reaper fake timers、负控:loadPty 强制
49
+ null ⇒ 降级、malformed 帧吞掉不进 dispatch、runner 未挂载 ⇒ 无能力位);
50
+ darwin spawn-helper 执行位兜底进 `test/setup/node-pty-exec-bit.ts`
51
+ (上游 microsoft/node-pty#850)。
52
+ - **feat(daemon): daemon 侧窗口输出帽(spec §T-5 follow-up 落地)**——per-
53
+ session 滚动窗字节预算(`PRISMER_TERMINAL_OUTPUT_BUDGET_BYTES` /
54
+ `PRISMER_TERMINAL_OUTPUT_BUDGET_WINDOW_MS`,默认 32MiB/10s,与 idle
55
+ timeout 同一条 ConfigDelivery 面)。洪流超预算 ⇒ kill 会话 +
56
+ `terminal.exit` 带 typed `reason=terminal_output_budget`(relay 透传、
57
+ 前端独立横幅);kill 触发 onExit 二次广播已抑制。cloud relay 512KB/1s
58
+ 保持浏览器侧收口,此帽是源端封顶——空闲回收在输出洪流下不再被续命。
59
+ 测试新增 5 例(resolver 契约 + fake pty 越限 kill + 窗内正常 + 未装配
60
+ 负控)。
61
+ - **fix(daemon): 输出帽系统评审三项修复**——① 窗口账本每次无条件排空(此前
62
+ length>64 才 filter,稀疏流下旧账不滚、累计误杀);② kill 后 onData 存活守卫
63
+ (孙进程持 slave fd 续 drain 时不再 touch/timer 重挂/重复 exit 帧);
64
+ ③ env 显式 `0`/`off`/`disabled` 可关帽(此前任何 env 组合都落默认常开,
65
+ ConfigDelivery 无 off 面)。评审回归钉 3 例 + 语义测试更新。
66
+
67
+ ### bugfix211 G3 — host.acked adopt/reclaim × rejection 分离(declare↔ack 自噬环)
68
+
69
+ - **fix(daemon): adoption 不再清掉同一轮 ack 刚拉黑的所有权黑名单。** 半成型
70
+ agent(有 profile 行、无 owner userId、无 binding)每轮 declare 同时命中
71
+ rejectedAgents("不是你的")与 adoptAgents("没人绑,来收养"):ownership-lock
72
+ 删除+拉黑,syncHostAckAdoptions 又清黑名单重建,环实测 ~350ms/轮
73
+ (skills/ack+declare+me/agents+runtime 连环刷屏)。daemon 侧
74
+ syncHostAckAdoptions 增 rejectedThisRound 参数,本轮被拒 agent 跳过
75
+ reclaim/adopt(保留黑名单);cloud 侧在 ack 组装口做统一剔除(见主仓
76
+ handler.ts partitionAdoptableAgainstRejections)。
77
+
78
+ ### bugfix211 G2 — ConfigDelivery 容器分类修正(workspace pod 被误判 ACS)
79
+
80
+ - **fix(daemon): 环境判据从镜像默认 `ALLOW_FAKE_API_KEY` 换成 env key 本身。**
81
+ dev 镜像把 `PRISMER_ALLOW_FAKE_API_KEY=true` 烧为默认(v200 时代 ACS 占位
82
+ key boot 的遗留),ACS checkpoint 与 workspace k8s pod 共享同一镜像,于是
83
+ 持有真 `sk-prismer-live-*` 凭证的 workspace pod 被判成 "no env-injected
84
+ key" 的 ACS 世界:启动即打 "ACS sandbox detected",env-inference 兜底被
85
+ 禁,B1 永久 401/403 时拒绝安全的最后手段而 Hermes 永不配置。新判据
86
+ `containerEnvIsKeyless`(runner.ts,三处消费点统一):container 且 env 无
87
+ `sk-prismer-live-` 形态 key 才是 keyless 沙箱;持真 key 一律走 ACK 分支。
88
+ 配套 cloud 侧 pod-spec 显式注入 `PRISMER_ALLOW_FAKE_API_KEY=false`(覆盖
89
+ 镜像 ENV;ACS pod 不受影响)。测试 `test/container-env-keyless.test.ts` 5 例。
90
+
91
+ ### memory211 — pkf_reply_inline cross-turn delivery + office-artifacts interpreter
92
+
93
+ - **fix(pkf): `pkf_reply_inline` read roots now include the session dir.** In a
94
+ hermes LONG session the task dirs persist across turns under
95
+ `sessions/<sid>/tasks/<tid>/`, and the agent re-delivers a `.pkf` it authored
96
+ in a PREVIOUS turn's scratch; `bindPkfReplyInlineScope` computed `sessionDir`
97
+ but never added it to `allowedReadRoots`, so the new dispatch rejected that
98
+ path (`pkf_reply_inline_path_outside`) at tool time AND at terminal
99
+ re-validation. Rooting the session dir makes directory-prefix containment
100
+ cover every historical `tasks/*/scratch` under the session (referencing old
101
+ scratch is the natural shape, not an escape). Unbind stays taskId-keyed, so
102
+ the wider read set cannot outlive its own dispatch. Tests: red→green pair in
103
+ `pkf-reply-inline-tool.test.ts` — cross-turn delivery through `handleDispatch`
104
+ with the marker landing in THIS run's scratch, plus the same-session-bound
105
+ negative control (another session's file is still rejected).
106
+ - **docs(office-artifacts skill): the baked office libs live in the image venv
107
+ interpreter `/home/user/.venv/bin/python3`**, not the system
108
+ `/usr/bin/python3` (which has none of them). The Runtime baseline section now
109
+ says to run generator scripts with the venv interpreter and to re-probe there
110
+ before concluding a dependency is missing (agents probing system python were
111
+ reporting a false "missing deps"). Still no `pip install` on the fly. The
112
+ authoritative copy is `sdk/cloud/catalog/skills/office-artifacts/SKILL.md`
113
+ (`sdk/prismer/built-in-skills/` is the gitignored build-time copy, refreshed
114
+ in this checkout to stay byte-identical); the catalog source-contract
115
+ skills-manifest hash was re-pinned accordingly
116
+ (`catalog-source-contract.test.ts`).
117
+
118
+ ### memory211 external review — P1 (chunk lane cap boundary)
119
+
120
+ - **fix(P1, security, asset raw text): the daemon `memory_search` chunk lane
121
+ was fail-OPEN for asset ACLs.** `runWorkspaceSearch` post-filtered hits with
122
+ `store.loadById(r.pageId)`, but a T3 chunk hit's `pageId` mirrors an ASSET id
123
+ and `memory_pages` never has that row — every chunk hit fell through the
124
+ "no page row" pass and was KEPT, so a scoped sub-agent cap on a shared daemon
125
+ could read any materialized asset's raw body (the hook recall path fails
126
+ CLOSED on the same shape). Chunk hits are now judged by the new
127
+ `canCapReadAsset(cap, asset)` boundary predicate (acl-predicate.ts) against
128
+ the asset's `visibility` + `ownerImUserId`, mirrored onto the daemon's own
129
+ asset projection: `asset_metadata_index` gains both columns (local schema
130
+ v17) fed by the SAME `/assets/index` DTO it already mirrors — no endpoint or
131
+ wire change — and the runner wires `resolveAssetAcl` (a synchronous local
132
+ read, no I/O in the search hot path). Strictly narrower than the cloud asset
133
+ ACL by design: `workspace` or own-asset grants, everything else DENIES
134
+ (`user` / `task:*` / `quarantined` / `aclJson` grants stay cloud-side). An
135
+ asset with no verdict (never synced, pre-v17 mirror row, older cloud) is
136
+ DENIED — fail-closed — so the lane degrades to wiki-only instead of leaking.
137
+ Tests: `memory-search-asset-cap.test.ts` (real RPC surface; the system cap
138
+ sees the same chunk the scoped cap is denied, proving the fixture) +
139
+ `canCapReadAsset` matrix in `memory-acl-predicate.test.ts` + the projection
140
+ round-trip in `asset-metadata-index.test.ts`; mutation (removing the
141
+ chunk-lane boundary) turns 2/4 red.
142
+
143
+ ### memory211 review round 2 — P3 + P5
144
+
145
+ - **fix(P3, recall ranking): `mergeChunkHits` compared the COMPOSITE score
146
+ instead of the LEXICAL evidence it documented.** Its comment promised that
147
+ workspace priors never bury the upload, but `existing.score` already folds
148
+ recency (+0.2) / supersede (−0.8) / stale (−0.5) — so a freshly-touched wiki
149
+ page outranked a chunk that carried the query term far more strongly (the
150
+ exact D9 shape). The merge now reads `components.matchScore` (the
151
+ pre-adjustment BM25 relevance), the same basis the cloud `mergeRawTier`
152
+ compares on. `MemorySearchResult` gains the additive optional
153
+ `components: { matchScore }` decomposition (cloud parity), populated on both
154
+ the wiki FTS leg and the chunk leg.
155
+ - **fix(P5, prompt doctrine): the dispatched agent prompt still taught the
156
+ retired ">1M sharding" trigger.** `MEMORY_CORE_DIRECTIVE` (injected into
157
+ every dispatch) plus five comment sites (extract.ts ×3, runner.ts,
158
+ hook-server.ts) predate §6.9 裁决 4's 64K-character re-threshold, teaching an
159
+ agent to shard a source the gate now admits whole up to 64K characters. All
160
+ six now carry the 64K-character口径; `dispatch.test.ts` and
161
+ `memory-skill-degrammar.test.ts` pin it (and pin that "1M" never returns).
162
+ - **fix(P6, deliverable gate): an image-only deliverable's page was gated out
163
+ wholesale.** The pairing side declares a deliverable source on ANY
164
+ `prismer://asset/<id>` occurrence — including the `<figure><img src>` form the
165
+ extraction prompt itself teaches for IMAGE/CHART assets — while G9 recognized
166
+ only `<a rel="derived-from">`. The gate now accepts the inline image pointer
167
+ as the same materializable jump pointer (an image cannot be distilled into
168
+ prose; its embed IS the jump), and the 422 message teaches both forms. An
169
+ `<img>`/`<a>` pointing at a DIFFERENT asset, or an untyped `<a>`, still fails.
170
+
171
+ - **fix(recall): the T3 chunk lane collapsed an upload's chunks to ONE hit**
172
+ (found as the W4 span-mint suite's order-dependent red). The lane deduped its
173
+ FTS candidates by `path`, but every chunk of an asset shares the same
174
+ `asset:<id>#<hash>` path — so an 11-chunk upload recalled exactly one chunk,
175
+ whichever the (near-tied) bm25 ordering surfaced first, and mint-即升层 was
176
+ invisible whenever the cited chunk lost that coin flip. Deduping is now by
177
+ chunk identity (`assetId#ordinal`), and bm25-equal chunks tie-break by ordinal
178
+ so the window cannot reshuffle between recalls on an unchanged store.
179
+
180
+
181
+ ### memory211/01 §6.11 — W7 behavioural-closure wave (read-side telemetry)
182
+
183
+ - **Read-side `[memory-trace]` pipeline (D11-5)** — search / load / browse
184
+ (place-context) / the search miss-lane's navigation now emit the same
185
+ `[memory-trace] stage=…` lines the write lane has logged since memory203/18
186
+ R8.2 (13 write sites, 0 read sites before W7). Lines carry the query COUNT,
187
+ `navigation_used`, the tier histogram, result count, duration and a traceId
188
+ (`?traceId=` honoured, else minted `rd_…`). The query TEXT never enters a
189
+ read trace. Full-volume local stderr, no sampling.
190
+ - **CLI recall telemetry (D11-1)** — `prismer memory recall/search` enqueues one
191
+ `recall_pull` (metadata `via: 'cli'`) into the daemon outbox via the existing
192
+ `/local/memory/observability/emit` pass-through, attributed to the presented
193
+ cap's `sub` claim (no subject ⇒ no event, never a fabricated actor). Best-effort:
194
+ a telemetry failure never changes the command's exit code. NOTE: a scoped-cap
195
+ CLI run now produces both a daemon tool-channel row and this `via:'cli'` row for
196
+ the same intent — consumers filter on `via`.
197
+ - **CLI batch faces (D11-2)** — `prismer memory search --queries '["a","b"]'`
198
+ (array passed through; the daemon owns the ≤8 bound and its `truncated` flag)
199
+ and `prismer memory load-batch <paths…>` (≤10 point loads, per-path verdict,
200
+ exit 1 only on a total miss). New `daemonGet` keeps a load 404 as a real
201
+ per-path verdict instead of `tryDaemon`'s route-probing 404-continue. Contract:
202
+ `maxItems` now survives into the regenerated plugin schema artifact; the
203
+ memory SKILL CLI appendix teaches both faces.
204
+ - **Tool-sequence ring + turn metrics (D11-4)** — new
205
+ `daemon/memory/tool-sequence.ts`: per-workspace ring (cap 200, in-process)
206
+ recording one entry per search/load/browse/write the daemon serves. PRIVACY
207
+ BOUNDARY: verb + page path + duration + timestamp + the `navigation` /
208
+ query-COUNT flags only; `ALLOWED_ENTRY_KEYS` is a closed set enforced by test
209
+ and the query text is unrecoverable. The dispatch finally block reads the
210
+ turn's slice and emits `turn.navigation_used` (browse, or a load after a
211
+ miss-lane search — repeated searches read 0) and `turn.shortcuts_taken`
212
+ (loads with no preceding browse), only when the turn made a memory call.
213
+
214
+ ### memory211/01 §6.9 — W6 (owner 终裁执行)
215
+
216
+ - memory211/01 §6.9 裁决 2 (W6) — **digest budget cap replaced by the 32K extreme
217
+ guard** (`MEMORY_DIGEST_EXTREME_GUARD_TOKENS`, Nacos/env key
218
+ `PRISMER_MEMORY_DIGEST_BUDGET_TOKENS` kept as the knob name). Full injection is
219
+ the default; the guard is the only cut and it marks itself. The INDEX-TOC is no
220
+ longer pre-budgeted to a share of the guard — that share silently
221
+ skeletonized the map of a workspace whose digest was still far below the
222
+ ceiling (covered by a red/green test in `memory-w3-upload-pipeline`).
223
+
224
+ - memory211/01 §6.9 裁决 1 (W6 fix round 1) — **the daemon miss navigation now
225
+ respects the per-cap read boundary**. `runWorkspaceSearch` filtered hits
226
+ through `canCapReadPage` but passed `navigation.startPoints` straight through,
227
+ so a hub/INDEX the actor cannot read still surfaced as path + title +
228
+ childrenCount. Start points are now filtered by the same predicate (an
229
+ unreadable entry is dropped; if none survive the navigation is omitted) —
230
+ matching the cloud side, which already applies `allowedPageIds` + the per-row
231
+ read verdict.
232
+
233
+ - memory211/01 §6.9 裁决 5 ①② (W6) — the ingest bookkeeping gaps:
234
+ ① **ingest terminal jump** (`reconcileWorkspaceIngestTasks` /
235
+ `reconcileIngestTasks`): a completed ingest task flips its asset to
236
+ `ingested`, a failed/cancelled one requeues it as `pending` with the attempt
237
+ recorded on the asset row, and the third failure parks it `failed` (cap 3,
238
+ mirroring the dream merge leg). The scheduler's ingest drain runs the
239
+ reconciliation first. Fix round 1: a LIVE (non-terminal) task outranks a stale
240
+ terminal one, so a gen1 failure no longer gets re-applied over a gen2 that is
241
+ still running — that misjudgement burned the retry budget twice, surfaced
242
+ gen1's error as if fresh, and could park `failed` an asset whose current
243
+ generation was fine (a false stall, since the drain never sweeps `failed`).
244
+ ② **`prismer memory ingest`** — the workspace upload-ingest operational switch
245
+ (owner/admin, cloud `/api/im/memory/ingest-settings`, merge-safe: it patches
246
+ one metadata key instead of replacing the object).
247
+
248
+ - memory211/01 §6.9 裁决 5 ③ (W6) — **daemon-first search scaffold removed** rather than wired: `setDaemonRpcSender`
249
+ had no responder (nothing in `src/im/ws/` ever registered presence or answered a
250
+ `memory.search` RPC), so `FF_MEMORY_SEARCH_DAEMON_FIRST` was a flag whose ON path
251
+ was unreachable in production. Cloud is the single authoritative recall path;
252
+ `SearchHit` collapses back to the cloud payload shape. Wiring daemon-first recall
253
+ would be a new designed seam (WS route + trace + parity tests), not a resurrected
254
+ stub.
255
+
256
+ - memory211/01 §6.9 裁决 4 (W6) — **sharding threshold 1M → 64K characters**.
257
+ The daemon deliverable gate's constant is renamed
258
+ `SHARDING_THRESHOLD_BYTES` → `SHARDING_THRESHOLD_CHARS` (`64 * 1024`) because
259
+ the ruling's 口径 is characters, not bytes (a CJK source is ~3 bytes per
260
+ char). The gate now applies the ceiling to the PAGE BODY in characters
261
+ (每页 ≤64K, 422 `sharding_required`); the cloud pipeline plans the source half
262
+ from the exact chunk character count.
263
+
264
+ - memory211/01 §6.9 裁决 3 (W6) — **memory/SKILL.md regenerated for the new loop**
265
+ (three-stage recall protocol: structure route → semantic search → navigation
266
+ miss-fallback; batch `queries[]` usage; the wiki/asset/raw tier trust bands;
267
+ copy + reference writing rules; 64K-char sharding). The skill now teaches
268
+ "startPoints are NOT answers — walk them". The prompt-size receipt budget is
269
+ rebased 360 → 460 lines for the mandated content (owner ruling: rebuild without
270
+ historical baggage, request budget rather than drop content). The plugin tool
271
+ schema artifact was regenerated from the TS spec (memory_search description now
272
+ teaches the navigation contract).
273
+
274
+ - memory211/01 §6.9 裁决 2 (W6) — **digest budget cap removed**: the W3-era
275
+ ~2K-token routine cap on the turn-start digest is GONE. `buildMemoryDigest`
276
+ now injects the FULL map (INDEX-TOC + every hub summary); the only remaining
277
+ bound is the extreme guard `MEMORY_DIGEST_EXTREME_GUARD_TOKENS = 32_000`
278
+ (env/Nacos key `PRISMER_MEMORY_DIGEST_BUDGET_TOKENS` keeps its name — it now
279
+ tunes the guard, not a routine cap), above which the body is cut with the
280
+ existing observable `digest truncated` marker. Determinism and content-hash
281
+ versioning are unchanged.
282
+ - memory211/01 §6.9 裁决 1 (W6) — **miss-path navigation redesign**: on a text
283
+ miss `memory_search` no longer returns the structural lane dressed up as a
284
+ ranked hit list (a miss carries no rank evidence — the old band was a random
285
+ order). The response now carries `navigation: {reason:'text-miss',
286
+ startPoints:[{path,title,pageType,childrenCount,why:'structural-entry'}],
287
+ guidance}` (INDEX + hubs, children first) on the daemon response, the TS
288
+ adapter types (`MemoryNavigation`) and — via the plugin's JSON passthrough —
289
+ the tool result. `hybrid()` keeps its shape for existing callers; use
290
+ `hybridWithNavigation()` for the payload. The W1a dynamic miss band
291
+ (`GRAPH_MISS_PROMOTED_BAND` / `applyMissBands`) is removed: graph hits now
292
+ exist only as a supplement to real FTS evidence and always land in the sink
293
+ band. Hits with lexical evidence (FTS pages, T3 chunks) are unchanged.
294
+ - memory211/01 v3.3 W5 (轴H 单源契约 + 轴G section curate 动词):
295
+ - **Single schema source (D1 extinction)**: the five memory tool schemas in
296
+ `plugins/memory/prismer/__init__.py` are no longer hand-written — they are
297
+ generated from the frozen TS spec (`src/adapters/memory-tools.ts`) into
298
+ `tool-schemas.generated.json` and LOADED at runtime. Regenerate with
299
+ `npx tsx scripts/memory211/generate-memory-tool-contract.ts`; the
300
+ three-surface contract test
301
+ (`scripts/__tests__/memory211-tool-contract.test.ts` on the cloud repo)
302
+ fails on any divergence between tool schema, plugin handler, CLI flags and
303
+ the SKILL.md parameter table. The placement parameter is now camelCase
304
+ `parentHubPath` on EVERY surface (was `parent_hub_path` on the Hermes
305
+ plugin while the skill taught `parentHubPath`).
306
+ - Plugin forwards the batch `queries` + `pageType` memory_search params
307
+ (W1a leftover) and the `visibility` write param.
308
+ - Daemon RPC: `/local/memory/curate` routes the new `section_merge` /
309
+ `section_supersede` / `rewire` ops to cloud; new `/local/memory/delete`
310
+ (cloud-adjudicated soft delete + local mirror invalidation) and
311
+ `/local/memory/sync` (alias of flush).
312
+ - CLI: `prismer memory delete|sync` now hit the real daemon routes (they
313
+ previously probed only legacy paths no daemon exposes), `memory write
314
+ --title`, `memory curate promote-to-hub --child-paths`, and three new
315
+ subcommands `curate section-merge|section-supersede|rewire`.
316
+
317
+ - memory211/01 v3.3 W4 (轴F 溯源闭合), daemon runtime side — **lazy span mint +
318
+ mint 即升层**:
319
+ - Local store schema V5 → V6: `asset_chunks` gains `sid` (additive column,
320
+ pragma-guarded ALTER). NULL = the chunk was never cited by a distilled page
321
+ = still T3 `raw`; non-NULL = cited + minted = T2 `asset`.
322
+ - New `daemon/memory/span-mint.ts`: after an agent-authored write
323
+ (`memory_write`) or a post-turn extracted page commits, the content is
324
+ scanned for asset provenance pointers (`prismer://workspace/<ws>/asset/<sha256>`
325
+ scoped, `prismer://asset/<id>` bare, `asset:<id>#<hash>` token — each with an
326
+ optional `#chunk-<ordinal>` span anchor). Every cited chunk of the asset's
327
+ CURRENT revision gets a DETERMINISTIC span sid (`sp-<16hex>`, derived from
328
+ assetId + contentHash + ordinal + text hash — byte-identical to the cloud
329
+ twin `src/im/services/memory-span-sid.ts#mintChunkSid`, so both sides
330
+ converge on one identity), the local mirror row is promoted, and the mint
331
+ rides the SAME idempotent `asset.chunk.upsert` channel with the `sid`
332
+ attached so the cloud authoritative row promotes too. The mint is a derived
333
+ projection: it never throws into the write path and is a one-way latch
334
+ (a sid-less replay never un-mints).
335
+ - Local recall honours the promotion: a chunk hit whose row carries a sid
336
+ surfaces as `tier:'asset'` with `spanSid`, and no longer pays the
337
+ `RAW_TRUST_DISCOUNT` (spec 轴C: T2 sits above T3).
338
+
339
+ - memory211/01 v3.3 W3 (upload ingestion pipeline + turn-start warmup), daemon
340
+ runtime side:
341
+ - **T3 raw-chunk mirror (轴D, F2 裁决)** — local store schema V4 → V5 adds
342
+ `asset_chunks` + `asset_chunks_fts` (additive migration, no rebuild). A
343
+ materialized upload is chunked locally (deterministic ~2K-token chunks with
344
+ 10% overlap; byte-identical contract with the cloud chunker) and recalled as
345
+ `tier:'raw'` hits, so offline recall now includes uploads, not only wiki
346
+ pages. The same rows ride the existing memory outbox to the cloud
347
+ authoritative `im_asset_chunks` via the new `asset.chunk.upsert` event
348
+ (idempotent per `workspaceId:assetId:contentHash:ordinal`).
349
+ - **pdf ingestion** — pdf uploads go through the bundled liteparse `lit`
350
+ (digital fast path `--no-ocr`, OCR fallback, 60s kill). A missing/failed
351
+ parser SKIPS loudly; it never throws into the `asset.materialize` ack.
352
+ - **Stable memory digest (轴E)** — new `daemon/memory/digest.ts` builds the
353
+ turn-start digest (INDEX-TOC + hub one-liners): deterministic (path-sorted,
354
+ no timestamps/volatile fields), sha256 versioned, bounded to ~2K tokens
355
+ (`PRISMER_MEMORY_DIGEST_BUDGET_TOKENS`), with an observable truncation
356
+ marker. `FF_MEMORY_INDEX_INJECT_ENABLED` now defaults ON (explicit
357
+ `false|0|off` to disable).
358
+ - **Hermes assembly (轴E)** — the adapter injects the digest at the TAIL of
359
+ the per-turn `instructions` (i.e. the tail of the composed system message)
360
+ so the rarely-changing blocks above it keep their prompt-cache prefix; the
361
+ block is byte-stable for a given digest version.
362
+ - **Miss-lane anchors (轴D 追加项 D①)** — when the text leg misses entirely,
363
+ the daemon miss lane now RETURNS the INDEX/hub anchors as `via:'graph'`
364
+ hits (they were seeds only) and a routing term (childrenCount) keeps a hub
365
+ above its own leaves in the elevated band.
366
+ - **Review fixes** — the deliverable gate no longer strips
367
+ `contentHash`/`sizeBytes` from a declared source (the G10 sharding trigger
368
+ was unreachable from extraction), both extraction legs report
369
+ `extractionGatedOut`/`shardingRequired`, and the dead `distillOverBudget`
370
+ counter is gone.
371
+
372
+ - memory211/01 v3.3 W2 (内容模型 + 写入门 + CJK 分词) — the extraction-thinning
373
+ reversal, on the daemon runtime side:
374
+ - **CJK recall fixed (W2 #7, W0 baseline finding)** — the daemon FTS index
375
+ gains a `cjk` bigram projection column (`memory_fts.cjk`, store schema
376
+ V4 with a drop/recreate/re-index migration) and the query side offers the
377
+ query's own CJK bigrams as FTS alternatives. Before: FTS5's unicode61
378
+ tokenizer kept a whole CJK run as ONE token, so a 2-char Chinese term inside
379
+ a longer run could never match — a pure-Chinese query was a guaranteed zero.
380
+ ASCII queries are byte-for-byte unchanged.
381
+ - **G10 redefined in place (轴A / §0.1)** — the anti-copy budget
382
+ (`min(32KiB, 10% × source)` → 422 `distill_over_budget`) is GONE; copy +
383
+ reference means a distilled page may carry the source's near-full content.
384
+ The only budget left is the sharding trigger: a source over 1,000,000 bytes
385
+ is refused with 422 `sharding_required` (the W5 sharding pipeline will own
386
+ the actual splitting). G9 (derived-from pointer) is unchanged.
387
+ - **one gate, two surfaces (轴H / D3)** — the deliverable gate moved to
388
+ `daemon/memory/deliverable-gate.ts` and the MAIN extraction path now runs
389
+ it: a page that declares a deliverable source but references none of them is
390
+ dropped loudly (trace + `extractionGatedOut` counter) instead of written.
391
+ - **write gates ②③ (轴H)** — `memory_write` now rejects a NEW PKF page with no
392
+ frontmatter description (422 `description_required`) and a body that fails
393
+ the bundled `validatePkf` structure audit (422 `pkf_invalid`, warnings never
394
+ reject). Markdown (legacy input) is exempt from both, edits of existing
395
+ pages are exempt from the description gate, and the PKF gate runs AFTER the
396
+ bare-asset upgrade so the taught `prismer://asset/<id>` pointer form is not
397
+ re-flagged. The placement gate additionally honours an existing child-of
398
+ graph edge (轴H ①).
399
+ - **extraction prompt re-cast (轴A)** — the "pointers not copies" doctrine is
400
+ replaced by copy + reference, with two acceptance rules the model must
401
+ self-check: LEXICON COVERAGE (proper nouns / codes / 中英对照 terms survive
402
+ verbatim) and SECTION EDGES (typed `<a rel>` links anchored at `#section`,
403
+ which the cloud materializer lands as `im_memory_links.sourceSection` /
404
+ `targetSection`). `MAX_PAGES_PER_TURN` and the 32768 extract budget are
405
+ unchanged, and the recorded `lexicon_coverage` metric
406
+ (`extractLexicon` / `coverage`) is the W2 acceptance measure — recorded,
407
+ never gating.
408
+ - memory search surface re-cast (memory211/01 §3 轴C W1a) — the recall payload
409
+ now carries what an agent needs to decide 直读 vs 多跳 without a second call,
410
+ and recall no longer dies when the text leg does:
411
+ - **hop-decision payload**: every `memory_search` hit adds `pagePath`
412
+ (mirror of the frozen `path`), `hubPath` (reverse `child-of` placement,
413
+ null for a root/INDEX page or a dangling edge), `version`,
414
+ `sectionAnchor`/`sectionPreview`, `tier: 'wiki'` (the T1-only enum is
415
+ pinned now; T2/T3 land with the W3 ingestion pipeline),
416
+ `inboundLinkCount`, `childrenCount` and `outboundPreview[]`. All of it is
417
+ aggregated IN PLACE from the existing `memory_links` mirror
418
+ (`MemoryStore.pageGraphContext`) — no new table, no new sync channel.
419
+ Legacy fields (`path`/`title`/`snippet`/`score`/`tokenCount`) are
420
+ untouched, so an un-upgraded consumer keeps working.
421
+ - **dynamic graph band** (spec open ruling point 3, W1 experiment): the old
422
+ constant "every graph hit one unit below the weakest FTS hit" is replaced.
423
+ When the text leg has hits, graph hits stay the supplementary band (the
424
+ old invariant still holds and is still tested). When text misses
425
+ ENTIRELY — the memory211 §1 paraphrase failure, where the answering page
426
+ shares zero tokens with the query — the graph leg becomes the recall lane:
427
+ it seeds from INDEX + hubs and scores a structurally-anchored hit
428
+ (≥1 inbound link or a resolvable hub) in the normal 0-1 band instead of
429
+ the sub-zero decay band. `graph: false` keeps the legacy pure-FTS path.
430
+ - **batch recall**: `GET /local/memory/search` accepts `queries` (JSON
431
+ array, ≤8; over-limit is sliced with `truncated: true`) and returns
432
+ `resultsByQuery: [{query, results}…]` plus the legacy `query`/`results`
433
+ fields, which stay the FIRST query's so an old consumer sees today's
434
+ shape. One `recall_pull` event is emitted per query (a batch carries N
435
+ recall intents, not one). The single-`q` form is a batch of one.
436
+ - **tree-shaped browse**: every hub in `GET /local/memory/place-context`
437
+ (and in the `memory_browse` payload) now carries
438
+ `children: [{path, title}]` from the local `child-of` edges; `nearest` is
439
+ deliberately unchanged (graph-distance ordering is deferred to W1b).
440
+ - **load 离线 links**: when the cloud does NOT answer `GET
441
+ /memory/pages/:id/links` (unreachable, non-2xx, or never wired), the load
442
+ response now falls back to the LOCAL link mirror instead of silently
443
+ omitting `links`, assembled in the same `{outbound, backlinks}` envelope.
444
+ An authoritative cloud answer is never overridden.
445
+ - Covered by `test/memory-search-payload.test.ts` (payload fields + 升带,
446
+ with the re-band and the offline-links fallback each carrying a mutation
447
+ negative control), `test/memory-search-batch.test.ts`,
448
+ `test/memory-browse-children.test.ts` and
449
+ `test/memory-load-links-offline.test.ts`.
450
+ - metric outbox replay (B-P1a): the per-agent `metrics.jsonl` offline outbox
451
+ (written by `daemonMetricEmit` whenever cloud is unreachable) finally has a
452
+ reader. `MetricOutboxReplayWorker` (new `daemon/metric-outbox-replay.ts`,
453
+ wired into the runner lifecycle) drains it into `POST /api/im/metrics/batch`
454
+ — the same endpoint the online path uses, with the same per-event field
455
+ shape (`eventType` splits at the first dot into `namespace.name`; value /
456
+ dims / ts pass through as serialised; the entry's top-level workspace /
457
+ project / task mirrors repair dims lost in the payload). Delivery is
458
+ **at-least-once**: the file is frozen with `renameSync` BEFORE it is read and
459
+ unlinked only after every ≤500-event batch chunk came back HTTP-ok, so a
460
+ crash anywhere replays the file (additive metrics tolerate the duplicate).
461
+ Freezing before reading (rather than truncating in place) is what makes a
462
+ line appended by a concurrent emit during the cloud round-trip survive — it
463
+ lands in a fresh live file and drains next tick. Bad-JSON lines and non
464
+ `metric.event` kinds are counted, logged and dropped (they would retry
465
+ forever otherwise); cloud-side per-event rejections are likewise logged and
466
+ dropped, HTTP-level failures retain the whole file with 5s→10min backoff and
467
+ a warn→error escalation after 5 failed ticks. Triggered on daemon startup and
468
+ every 60s (timer unref'd); no notify hook on the write path — offline metrics
469
+ are additive and a ≤60s lag is acceptable. Covered by
470
+ `test/metric-outbox-replay.test.ts` (batch content, retention + retry,
471
+ empty/missing no-op, chunking, backoff escalation, and a mutation negative
472
+ control proving a mid-flight append is not lost under truncate).
473
+ - turn metrics (B-P0, agent performance pipeline): the daemon now emits eight
474
+ turn-level metric events from the dispatch `finally` block alongside the
475
+ existing `agent.dispatch` / `skill.invoked` batch — `turn.count`,
476
+ `turn.tokens_input`, `turn.tokens_output`, `turn.tokens_cache_read`,
477
+ `turn.tokens_cache_write`, `turn.duration_ms`, `turn.first_event_ms`,
478
+ `turn.tool_calls`. Dims: required `workspaceId` + `agentId`; optional
479
+ `conversationId`, `taskId`, `model`, `provider`, `status` (`ok`|`error`).
480
+ Usage comes from the TERMINAL attempt's hermes usage only — retry inflation
481
+ stays in `agent.dispatch`. Token rows are emitted per-field only when the
482
+ terminal attempt reported them. `turn.first_event_ms` is new instrumentation
483
+ on the hermes sessions lane: `consumeSessionsSse` captures the first
484
+ stall-guard activation (gateway acks excluded) and derives it against the
485
+ dispatcher's `startedAt`; `TaskResult.metrics` carries the new optional
486
+ `firstEventMs`. Emit failures stay non-fatal (observability path). Covered
487
+ by `test/daemon-metric-emit.test.ts` + `test/sessions-sse-first-event.test.ts`.
488
+ - config bootstrap (B1): 403 `SETUP_PIN_PENDING` (cloud: the workspace setup
489
+ runner has provisioned the pod but has not persisted the canonical API-key
490
+ pin yet) now backs off with the existing curve instead of terminal-stopping,
491
+ bounded by `SETUP_PIN_RETRY_WINDOW_MS` (5 min) after which it degrades to the
492
+ ordinary terminal 403. `BootstrapFetchResult` carries the cloud `error.code`
493
+ and `BootstrapState.pinPendingSince` holds the window anchor — cleared by
494
+ `resetBootstrapStopped` AND on every successful fetch (per-episode, so a
495
+ later absent-pin episode gets a fresh window). Fixes fresh workspaces coming
496
+ up memoryless when the
497
+ pod daemon's first B1 fetch raced the pin persist (2026-09-02 ws-72d282f9).
498
+ Covered by `test/config-bootstrap.test.ts`.
499
+ - memory write: write-time bare-asset-URI upgrade (pkf v1.1 §4.7). The
500
+ memory skill / dispatch guidance / extraction prompt teach the bare
501
+ `prismer://asset/<id>` pointer form (G9), but v1.1 strict validation
502
+ rejects it on read-back (`bare-asset-uri`, compat-read only) — the
503
+ canonical authoring form is `prismer://workspace/<wsId>/asset/<contentHash>`.
504
+ `POST /local/memory/write` now deterministically rewrites every bare
505
+ href/src pointer AFTER the G9 gate and BEFORE the page lands (store + outbox
506
+ up-sync both carry the upgraded form; rel / link text / quote style /
507
+ attribute order survive). assetId → contentHash resolves from the workspace
508
+ AssetMetadataIndex (one throttled delta pull forced on a miss); unresolvable
509
+ assets degrade to the workspace-scoped assetId form
510
+ `prismer://workspace/<ws>/asset/<assetId>` (still v1.1-valid) — the write is
511
+ NEVER blocked. Counters: `memory.counters.bareUriUpgraded` /
512
+ `bareUriScopedDegrade` + `[memory-trace] stage=bare_uri_upgrade`. Covered by
513
+ `test/memory-write-bare-uri-upgrade.test.ts`.
514
+
515
+ - daemon local gateway (desktop205 alignment round, 2026-08-31):
516
+ - `PRISMER_LOCAL_GATEWAY` is now injected by the desktop embedded fork —
517
+ the local data plane (SWR reads, SSE mirror, outbox writes) activates on
518
+ desktop instead of being structurally unreachable.
519
+ - user-Bearer acceptance: the gateway now serves the renderer's USER session
520
+ JWT / sk-prismer key in addition to the daemon key. Introspected against
521
+ the cloud (`/api/im/me`) and BOUND TO THE DAEMON OWNER — any other valid
522
+ account token is refused (review hardening: previously any valid token
523
+ could read the owner's SWR cache from any same-machine page via the
524
+ permissive CORS origin). Cache is keyed by token SHA-256 digest with
525
+ positive/negative TTLs; the raw bearer is never retained.
526
+ - CORS: Allow-Headers now includes X-IM-Workspace/X-Request-Id/
527
+ X-Idempotency-Key and Expose-Headers includes X-Data-Stale (the desktop
528
+ offline indicator can now light on gateway-served responses).
529
+ - SWR revalidation for /api/im/conversations is scoped to the daemon's
530
+ declared workspace (`workspaceId()` dep); passthrough forwards the
531
+ caller's X-IM-Workspace/X-Request-Id so multi-workspace identity survives
532
+ the loopback hop.
533
+
534
+ - hermes adapter: `stopHermesGatewayForProfile` is now a logged no-op when the
535
+ profile config fails `HermesProfileConfigSchema` (e.g. ACS sandbox fixtures
536
+ carry no `apiKey`). Previously the uncaught ZodError escaped the
537
+ `ServicePool.invalidate` disposer in `Runner.rebindHermesMemoryCapabilities`,
538
+ crashed the daemon (exit 1), and after two retries the bootstrapper
539
+ blacklisted the whole runtime bundle — sandbox healthz stuck at 503
540
+ (reproduced 2026-08-30 by the dev-local fxr acceptance fixture). Covered by
541
+ `test/hermes-stop-gateway-noop.test.ts`.
542
+ - sandbox-manager: removed the orphaned `resolve_daemon_id` wrapper (its only
543
+ caller switched to `resolve_daemon_id_for_boot` in d74e00074); the dead
544
+ symbol failed the dev-local rust-manager clippy gate (`-D warnings`).
545
+ - Memory post-turn extraction no longer mints pages from agent
546
+ self-introductions (kickoff / "你们都能干什么" turns): a deterministic
547
+ `agent_self_intro` skip in `shouldSkipExtraction` (narrow shape match on the
548
+ reply head; an explicit user retention contract still passes) plus an
549
+ `AGENT SELF-DESCRIPTIONS ARE NOT DURABLE` rule in the extraction prompt.
550
+ 2026-08-29 test incident: one role page per self-intro reply in a 3-agent
551
+ kickoff (see docs/product210/03 附录 A-4). Tests:
552
+ `sdk/prismer/test/memory-extract-self-intro.test.ts`.
553
+ - Filesystem-checkpoint restores now rebind every cached Hermes gateway after
554
+ the per-boot Cloud Memory authority snapshot first becomes available. The
555
+ gateway is atomically invalidated and prewarmed with a freshly minted
556
+ `PRISMER_MEMORY_CAP`, including when the Runtime config bytes are unchanged.
557
+ Explicit Memory GET tools also preserve structured daemon 401/403 responses
558
+ instead of misreporting them as empty data or `daemon_unreachable`. Managed
559
+ Hermes plugins now stage and directory-swap upgrades, so 0444 files restored
560
+ from a Checkpoint no longer turn a valid Runtime update into EACCES.
561
+
562
+ - Replayed Hermes profile preparation now treats an already byte-identical
563
+ `prismer-recall` plugin as a no-write success. Restored/hardened profile
564
+ trees no longer emit false `EACCES` install warnings on repeated
565
+ `host.acked`, while changed plugin bytes still fail visibly if unwritable.
566
+
567
+ - ACS workspace adoption now overlaps independent agents with a sliding,
568
+ bounded three-agent worker pool after the initial 0/0 `host.declare`;
569
+ profiles belonging to one agent remain ordered, failures remain isolated,
570
+ and the daemon emits one aggregate adoption timing before the final roster
571
+ declaration.
572
+
573
+ - Aligned Runtime package metadata with monorepo version `2.2.39` for a new
574
+ immutable sandbox image containing the workspace-neutral template marker and
575
+ clone identity behavior; Runtime OTA publication remains a separate chain.
576
+
577
+ - Promoted the canonical `remotion` skill into the common Agent baseline and
578
+ calibrated its sandbox workflow against current Remotion guidance. Rendered
579
+ media now has an explicit artifacts-dir → `cloud deliver` receipt contract,
580
+ plus version/composition, async-asset, deterministic-media, writable-cache,
581
+ browser-compatibility, adjacent-frame, and probe gates.
582
+
583
+ - ACS environment-template sources now initialize the full adapter floor without
584
+ opening Cloud transport or binding a workspace. Template clones discard the
585
+ source daemon identity and resume normal bootstrap before Cloud dispatch.
586
+ - Objective dispatch prompts now preserve the task/objective mirror fields used
587
+ by Cloud OKR flows, so hosted runtime turns keep objective context attached
588
+ through dispatch and post-turn durability.
589
+ - Sandbox runtime readiness now requires the daemon health identity to match the
590
+ claimed workspace and daemon id before Cloud accepts an ACS dialback as ready.
591
+
592
+ - Image generation delivery is now single-path and run-aware. The bundled
593
+ `image-generate` helper performs model discovery/generation and invokes
594
+ `cloud deliver` once. Runtime marks that file snapshot handled so the
595
+ dispatch-final scan makes no second upload; scan-only and offline-replay
596
+ fallbacks use the same unbound `/runs/<id>` scope for chat runs. The result
597
+ is one upload path, one user-visible root IMAsset (plus normal internal
598
+ preview derivatives), and one reply attachment.
599
+ - Explicit-delivery snapshot de-dup is scoped to the cloud dispatch and cleared
600
+ at teardown instead of living for the daemon process lifetime. This also
601
+ bridges Hermes-local run ids to cloud dispatch ids, preventing both
602
+ same-turn double uploads and later-dispatch false skips. A local :3000 live
603
+ acceptance now verifies one user-visible Asset, one attachment, and no JSON
604
+ in the message body through the real helper/CLI/daemon path.
605
+
606
+ - SS-01 bundle parser parity with Cloud: YAML `|` / `>` block-scalar
607
+ descriptions are supported, and the 50-unit description floor is
608
+ language-aware so concise CJK metadata does not require filler.
609
+
610
+ - Closed the Runtime PKF/Memory/Dream contract: bundled `memory` and
611
+ `memory-dream` now mirror the carrier transition and authoritative-candidate
612
+ rules; `parentHubPath` is consistent across schemas/prompts/errors; curate
613
+ schema exposes `conflicts` and `oversized`; and `rebuild_index` is described
614
+ truthfully as hub-TOC maintenance with graph-derived INDEX Contents. Daemon
615
+ section addressing now reads complete PKF `<section>` wrappers so section
616
+ edits cannot consume neighbouring tags.
617
+ - Long/structured replies now default to validated inline PKF across hosted
618
+ adapters without asking the user to choose a format. Runtime passes the
619
+ canonical inline source—not only its short Markdown projection—into post-turn
620
+ durability, so lasting conclusions can be distilled into searchable Memory.
621
+ - Automatic Memory extraction now rejects unclassified leaves, verifies model
622
+ placement against the browse snapshot, promotes unmatched new topics to hubs,
623
+ and emits knowledge-profile PKF with semantic sections and real descriptions.
624
+ The direct write RPC enforces placement by default.
625
+ - Removed the obsolete daemon Dream trigger path. `FF_MEMORY_DREAM_ENABLED`
626
+ cannot revive the retired page-dream scheduler; Cloud scheduling followed by
627
+ the appointed orchestrator is the sole automatic authority.
628
+
629
+ - Headless SERP + cache regression (runtime210/01, 2026-08-23): agent-first
630
+ local web search/read for in-pod agents using the sandbox image's existing
631
+ Playwright+Chromium, with results flowing back into the cloud
632
+ `im_context_cache` (decentralized data regression).
633
+ - Engine chain (`daemon/web/headless-serp.ts`): Bing (chromium, with
634
+ `bing.com/ck/a` base64url redirect decoding), Google (chromium,
635
+ `FF_WEB_HEADLESS_SERP_GOOGLE` opt-in; consent/challenge detection), DDG
636
+ (plain HTTP with `uddg=` decoding + anomaly detection). Coalescing
637
+ single-flight + 2s interval rail; engine-chain fallthrough; SSRF gate
638
+ (http(s)-only, public-address-only resolution, subresource/redirect
639
+ interception, final-URL recheck; TUN fake-IP range blocked by default,
640
+ explicit `PRISMER_HEADLESS_ALLOW_FAKE_IP=1` opt-in for dev machines).
641
+ - Deposit-dedup journal (`daemon/web/seen-journal.ts`) so agents never
642
+ overwrite each other's cache entries (ContextCacheService.deposit is a
643
+ blind upsert).
644
+ - `daemon/web/rpc.ts`: `FF_WEB_HEADLESS_SERP` routing — **default
645
+ `fallback`** (cloud Exa+Serper keeps relevance; headless rescues only when
646
+ cloud fails, relevance not promised). Explicit `off` / `local-first` still
647
+ available. Local responses are
648
+ load-API-shaped (`success/requestId/mode/results/summary/cost/
649
+ processingTime`) plus `degraded`/`providerAttempts`, so provider-shell
650
+ tools and payload bounding are untouched. Reads/search hits deposit back
651
+ via the existing free-tier `POST /api/context/save`. New
652
+ `GET /local/web/doctor` health probe.
653
+ - Real-scenario validation (2026-08-23): C6 corpus 20/20 on Bing, fetch of
654
+ real pages (~2s for a 57KB Wikipedia article), Google consent + DDG
655
+ anomaly challenge detection verified live. Expression-matrix finding:
656
+ search operators (site:/quotes/boolean) are ignored on cookieless
657
+ automated traffic — engine-side behavior, not a parser defect.
658
+ - Added the embedded `pi-core` runtime adapter floor: the SDK now pins
659
+ `@earendil-works/pi-agent-core@0.84.2` and `@earendil-works/pi-ai@0.84.2`,
660
+ registers `pi-core` as the canonical adapter/provider identity, injects
661
+ Prismer gateway routes through `PRISMER_PI_*`, and validates jailed cwd
662
+ read/write/edit tool execution. Daemon/desktop/K8s runtime bundle gates now
663
+ share the `runtime-required-entries.json` manifest so Pi Core dependencies
664
+ cannot drift across packers and loaders.
665
+
666
+ - Hosted Hermes turns now bind each native `run_*` row to its exact provider
667
+ `api_*` session and capture bounded post-response model/provider evidence via
668
+ the signed `prismer-recall` lifecycle bridge. Post-turn Memory resolution
669
+ excludes synthetic session-cache rows, rejects unknown or identity-mismatched
670
+ evidence, and no longer falls into ambiguous-agent / configured-model
671
+ fallbacks for a real sessions-API turn.
672
+
673
+ - Closed the hosted PKF execution gap found by a real Hermes task: the shipped
674
+ Python provider now registers `pkf_validate` / `pkf_outline` / `pkf_search` /
675
+ `pkf_read` and forwards them to bounded `/local/pkf/*` Runtime routes backed
676
+ by the staged `@prismer/pkf-core`. Native tool names are no longer presented
677
+ as shell binaries. Hermes startup also installs a direct managed `cloud`
678
+ symlink to the signed bundle's `@prismer/sdk/dist/cli.js`, including a safe
679
+ `~/.local/bin` alias for login-shell PATH resets.
680
+
681
+ - Agent-output policy now accepts `.pkf` as
682
+ `application/vnd.prismer.pkf+html` with the 5 MiB source budget, so a
683
+ dispatch-final forced artifact scan can upload PKF deliverables. Expensive
684
+ `host.acked` catch-up is connection-epoch/workspace idempotent: every ack
685
+ still handles governance/profile deltas, while transport (retry-on-failure),
686
+ Memory and Asset catch-up no longer repeat on each 30-second heartbeat.
687
+
688
+ - Runtime built-in delivery now ships canonical `pkf-svg` (with historical
689
+ `pkf-visual` resolved through its metadata alias), installs both `pkf-svg`
690
+ and `pkf-writing` as common coding skills, and verifies byte-identical
691
+ catalog → bundled-fallback mirroring. The skill preserves the current PKF
692
+ v1.1 production boundary: Mermaid/d3, not inline SVG authoring.
693
+
694
+ - `pkf-writing` now defines message-inline, Library `.pkf` Asset, and Memory
695
+ Page carriers with a unique Runtime extraction sentinel contract plus
696
+ canonical validation/persist/readback/Markdown-projection receipts.
697
+
698
+ - `pkf-writing` skill acceptance moved to the single-file contract
699
+ (product209/19 WP2): SKILL.md < 180 lines with the references/ directory
700
+ gone; the acceptance gates now carry explicit negative controls (over-budget
701
+ content, invented commands, references/ import, bare asset URI all go red);
702
+ the trigger matrix covers the Chinese recall intent ("recall 我的记忆" →
703
+ memory). The prebuild built-in-skills mirror follows the catalog.
704
+
705
+ - Hermes gateway health check accepts `degraded` (product209/18): hermes
706
+ 0.20.0 reports top-level status `degraded` under disk pressure (>= 90%)
707
+ while gateway + api_server are fully working; the old `status === 'ok'`
708
+ gate bricked dispatch (30s x 3 retries + kill-respawn storm). The
709
+ evaluator now requires gateway_state=running + api_server connected and
710
+ accepts ok|degraded; the spawn stamp is written before the health wait so
711
+ a slow/degraded gateway can't be misread as an un-witnessed orphan.
712
+
713
+ - Attachment blocks carry executable addresses (product209/18 Part A):
714
+ non-text non-vision attachments now emit `prismer://workspace/<ws>/asset/
715
+ <hash>` (stable reference) plus `path=file://<localPath>` for adapters
716
+ with real filesystem tools (hermes gated by toolsetScope terminal/file;
717
+ claude-code/codex/opencode). Tool-less adapters keep the legacy reminder
718
+ and never see a path.
719
+
720
+ - Asset materialization push (product209/18 Part B): new WS frames
721
+ `asset.materialize.request` (cloud → daemon, requestId=assetId) and
722
+ `asset.materialize.reply` (daemon → cloud). The daemon materializes
723
+ uploaded asset bytes into the local asset-cache on demand so the upload
724
+ progress bar completes only when bytes are in the pod; failures reply
725
+ with ok:false and cloud records no state (frontend polls and degrades).
726
+
727
+ - Canonical `pkf-writing` skill (product209/15 PKF-C1): slim SKILL.md + 6
728
+ progressive-disclosure references (core format / semantic HTML / media-data-
729
+ files / math / harness / validation-projection-export). Capability-based lane
730
+ choice (structured tools vs native filesystem lane); resolved readback
731
+ required before completion. Installed on coding agents via the common
732
+ allowlist; description routes recall/curation to `memory`.
733
+
734
+ - PKF filesystem service (product209/15 PKF-H5): durable `pkf_checkouts`
735
+ registry (daemon SQLite, idempotent boot migration) + four PATH-ONLY
736
+ lifecycle specs. These names are not advertised to Hermes until a shipped
737
+ provider handler can supply the task-root context. Ordinary UTF-8 files in
738
+ the task root; symlink/path
739
+ escape/device rejection; CRLF/BOM churn detected (explicit flag required);
740
+ final file bytes commit through the Cloud carrier CAS
741
+ (`POST /api/im/pkf/commit`); unlink never triggers a remote delete.
742
+
743
+ - SQLite V3 + snapshot-fenced full reconcile (product209/16 MA-2, Task 12):
744
+ `memory.db` migrates 2 → 3 in ONE transaction — `memory_replica_state`
745
+ (per-workspace cursor / accessVersion / replicaSubjectHash /
746
+ contentHighWatermark / reconciling|ready|suspended|stale / lease) plus
747
+ `memory_pages.sourceKind` and `replicaActorIdsJson`; the old
748
+ `memory_inbox_cursor` is migrated read-only (accessVersion=0, suspended —
749
+ never an authorization input), an in-flight `reconciling` flips to
750
+ `suspended` on restart, and a newer-schema db fails closed with a typed
751
+ `MemorySchemaIncompatibleError` (never a downgrade write). The runtime
752
+ consumes the Task 11 manifest/content ports: first manifest page pins
753
+ epoch/subject/high-watermark, content comes ONLY via
754
+ `POST /memory/sync/content` (snapshotToken + sourceRevisionId +
755
+ transport/content hashes), encrypted-pkf verifies transportHash → decrypts
756
+ → verifies plaintext contentHash, legacy-html lands via the compat text
757
+ path (never the local PKF write/outbox path), replicated rows carry the
758
+ sorted exact actor set, and the ACL-shrink diff (complete set vs local
759
+ controlled rows) deletes page/version/content/link/FTS — heads, deletes,
760
+ tombstones and cursor/epoch/hash/lease/ready commit atomically, followed
761
+ by a catch-up pass for content created mid-reconcile. Agent recall fails
762
+ closed (typed `MemoryReplicaNotReadyError`) unless the replica is ready
763
+ AND the live registered authority snapshot still matches the pinned
764
+ epoch/subject hash AND neither the snapshot nor the local lease expired;
765
+ `memory.invalidate` suspends the replica first. The boundary ACL predicate
766
+ now checks `replicaActorIdsJson` membership (cap `sub` ∈ exact set,
767
+ empty set denies all, tampered JSON fails closed) BEFORE coarse
768
+ visibility/principal rules. Legacy daemon sync receiving 426
769
+ `MEMORY_RUNTIME_UPGRADE_REQUIRED` stops typed (no fallback) and surfaces
770
+ `SyncResult.upgradeRequired { minRuntimeVersion,
771
+ requiredRuntimeCapabilities }`; strict workspaces are routed to the V3
772
+ port automatically. `MIN_STRICT_RUNTIME_VERSION = '2.2.12'` (first build
773
+ with SQLite V3, same source as the Cloud rollout constant).
774
+
775
+ - Memory authority snapshot + cap v2 (product209/16 MA-1B, Task 10): the
776
+ daemon registers the Cloud-delivered `memoryAuthority` snapshot bundle
777
+ (optional field on the RuntimeConfigBundle, 60m lease) and mints cap v2
778
+ ONLY from a valid snapshot — server-derived actor claims (principal /
779
+ taskIds / councilIds / verbs), 15m TTL clamped to
780
+ `snapshot.validUntil`. No valid snapshot (never fetched / expired /
781
+ tampered / epoch regression) → NO cap is injected and agent Memory RPC
782
+ fails closed. v1 tokens still verify for one compat release cycle (the v1
783
+ minter is retained only as the compat encoder; production mint sites
784
+ migrated to `mintCapV2`). The boundary ACL predicate now adjudicates
785
+ actor/principal/task claims: a deputy cap reads its bound member's
786
+ `human:*` pages (strict principal-id equality, never as `sub`, never
787
+ widened to other agents/members). `agent.host.declare` now carries
788
+ `runtimeCapabilities` (the three §8.2 v1 capabilities) and the daemon
789
+ handles the `memory.authority.invalidate` owned-channel frame (drop
790
+ snapshot + forced bootstrap refresh).
791
+
792
+ - Memory RPC write/outbox atomicity (product209/16 MA-0S, M-OUTBOX-001):
793
+ `POST /local/memory/write` now commits the page aggregate
794
+ (page/version/content/FTS) and its outbox events (page upsert + placement
795
+ link) in ONE SQLite transaction, reusing the post-turn
796
+ extracted-page-applicator pattern. An outbox enqueue failure rolls the whole
797
+ aggregate back and the RPC returns 500 instead of the old success that
798
+ stranded the page local-only. Success responses now also carry
799
+ `localVersion` and `outboxEventId`; the idempotency key stays derived from
800
+ the logical write identity (page id + parentVersion + contentHash), so
801
+ retries with a stable caller-supplied page id keep the same key.
802
+
803
+ - Native bounded query tools `pkf_outline` / `pkf_search` / `pkf_read`
804
+ (product209/15 PKF-H2): in-process over the bundled core — 32 KiB output
805
+ budget, signed paged cursors, 16/64 KiB read bounds, no includeSource
806
+ escape, no regex query surface. Offline; same receipts as Cloud.
807
+
808
+ - Native `pkf_validate` tool (product209/15 PKF-D3): in-process PKF validation
809
+ over the bundled pkf-core — no shell-out, no daemon RPC, offline by
810
+ construction. Registered across the Hermes / Claude Code / Codex tool
811
+ surfaces; same golden-fixture results as Cloud.
812
+
813
+ - PKF core staging (product209/15 PKF-D2): the runtime now stages a
814
+ hash-verified `@prismer/pkf-core` build into its local node_modules during
815
+ `prebuild` (`sdk/build/stage-pkf-core.cjs`), so offline parsing/validation
816
+ runs from the bundled core with no cloud network and no monorepo root source
817
+ (`test/pkf-core-staged.test.ts`). The staged copy never lands in the
818
+ published tarball; the standalone pack gate verifies both tarballs unpack
819
+ and run without the repo root.
820
+
821
+ - The bundled Remotion catalog now ships one self-contained canonical
822
+ `remotion` skill instead of twelve duplicate top-level skills plus a set of
823
+ short topic references. Runtime fallback resolves historical slugs through
824
+ canonical metadata aliases, and installed-skill delivery collapses multiple
825
+ legacy edges to one prompt section while preserving the selected historical
826
+ skill ID for sync acknowledgements.
827
+ - Hermes session creation and every chat turn now lock the configured `model`
828
+ (`require_model_lock: true`) while deliberately omitting `provider`. The
829
+ dedicated per-profile gateway resolves its authoritative named provider
830
+ before session creation; model-only locks let pre-lock sessions self-heal
831
+ after an OTA without Hermes v2026.8.3's named-provider identity mismatch.
832
+ - Runtime OTA bundles now carry the in-tree Cloud CLI/SDK and its bundled AIP
833
+ package. The frozen sandbox image no longer supplies these JavaScript product
834
+ packages; the signed boot bundle is their single delivery path. The resident
835
+ Rust manager now starts health before bundle resolution, retries when a fresh
836
+ Pod has no bundle, prepends the bundle CLI path, and is the sole owner of
837
+ Runtime child respawn.
838
+ - Signed Runtime manifests now record exact Runtime, Cloud SDK, and AIP package
839
+ provenance and reject native `.node` payloads. Packing also requires the
840
+ signing private key to match either the explicit rotated daemon public key or
841
+ the bootstrapper's built-in public key, preventing bundles that self-verify
842
+ during packing but cannot verify inside a Pod.
843
+ - Sandbox Pod probes now separate manager liveness (`:7890/healthz`) from
844
+ Runtime readiness (`:7890/readyz`). A fresh Pod waiting for a signed bundle
845
+ remains manager-alive but NotReady; Runtime child failure no longer makes
846
+ kubelet kill the manager that owns recovery.
847
+ - Bundle pointers and boot markers are atomically written as `0640 user:user`,
848
+ so both the Runtime owner and the group-member resident manager can read
849
+ rollback state even under a strict `0077` launcher umask.
850
+ - Sandbox Manager S2 (WP-E r4): Parameterize `execute_disk_cleanup` slow-ms switch
851
+ as a function parameter (previously read from process-global env). Eliminates
852
+ non-deterministic test races under parallel `cargo test` — unit tests now pass
853
+ slow_ms directly. Production path reads env once in `execute_action`. No
854
+ protocol/behavior change.
855
+ - Sandbox Manager S2 (WP-E, product209/09 S2): OTA settle logic (strike/rollback/
856
+ blacklist) migrated from TS (`ota-check.ts settlePreviousBoot`) into the Rust
857
+ manager (`ota.rs`). File formats unchanged — boot-attempt.json, pointer files,
858
+ blacklist.json match TS `bundle-store.ts` byte-for-byte. Manager settles
859
+ unconfirmed boot markers BEFORE OTA resolve; `SANDBOX_MANAGER_PRESENT` env
860
+ disables TS-side settle to prevent double-settle. Config backup point:
861
+ `config-bootstrap.ts applyBundle` now writes backup to
862
+ `~/.prismer/config-backup/` (atomic temporary+rename) before applying new
863
+ Hermes config. Rescue actions (cmd channel + inotify polling): `rollback_config`
864
+ (restore from backup → validate → restart), `ota_rollback` (current ← previous),
865
+ `restart_daemon`, `diag_bundle` (tar.gz of config+logs+resources),
866
+ `disk_cleanup`. Command idempotency via command-id result cache. Failure
867
+ classification: FATAL (config syntax, OTA failure, DB corruption) → Fail-slow
868
+ (diag pack + stay alive); TRANSIENT → Fail-fast (immediate restart, max 3
869
+ consecutive → Fail-slow). Design: docs/product209/09-resident-sandbox-manager.md
870
+ §3.3, §3.5, §3.6, §4, §6.
871
+ - Observability (WP-D, product209/07 §3.7.4): daemon /healthz `config` segment
872
+ (`configVersion` / `lastApplyAt` / `lastApplyError` / `pending`) derived from
873
+ bootstrapStates via spread discipline — absent when no ConfigDelivery state,
874
+ keeping CLI/K8s healthz shape unchanged. `runtime.incident` WS emission on
875
+ bootstrap errors (401/403 stop, 404/500/network backoff) for cloud-side
876
+ audit-row consumption. `LocalServerState.config` interface added.
877
+ - Sandbox Manager S1 (09): Resident sandbox-manager — Rust static binary
878
+ (`sdk/prismer/src/daemon/sandbox-manager/`) that supervises the daemon as a
879
+ child process under tini. Entrypoint init logic (config.toml first-write,
880
+ fake key fallback, static binding validation, OTA resolve) migrated from
881
+ `daemon-entrypoint.sh` into the Rust binary. Healthz HTTP endpoint on port
882
+ 7890 (K8s probes target the manager, not daemon). Daemon heartbeat file
883
+ (`~/.prismer/daemon-heartbeat`) written every 15s by the new
884
+ `DaemonHeartbeat` class (`daemon/daemon-heartbeat.ts`) for manager-side
885
+ liveness monitoring. Design: docs/product209/09-resident-sandbox-manager.md.
886
+ - ConfigDelivery P1: RuntimeConfigBundle types (`RuntimeConfigBundle`,
887
+ `HermesProviderConfig`, `HermesBundleConfig`, `BootstrapRequest`,
888
+ `BootstrapResponse`, `ConfigApplyState`) and daemon-side bootstrap module
889
+ (`fetchBootstrapBundle`, `applyBundle`, `isValidBundle`,
890
+ `createBootstrapState`, `resetBootstrapStopped`, `computeBootstrapErrorAction`,
891
+ `toApplyState`) for fetching workspace-level runtime config from
892
+ `GET /api/im/runtime/bootstrap` (B1 endpoint). Design:
893
+ docs/product209/07-config-delivery-runtime-bootstrap.md §3.3–3.4.
894
+ - Adoption path in runner.ts now calls `triggerBootstrap()` (cloud B1 fetch)
895
+ instead of direct `reprovisionHermesProfiles()` when `FF_CONFIG_DELIVERY` is
896
+ ON (default). The cloud is the single source of truth for provider + model
897
+ + key assembly. §3.6: explicit OFF falls back to 2.2.8 env-inference path.
898
+ - Coding Agent Line (08 P2/P3): CodingSessionControls UI component for
899
+ workspace coding agent session lifecycle — adapter selection (claude-code /
900
+ codex / opencode), forced-mode display, new-session / continue-session
901
+ actions. Reuses `POST /api/conversations/direct` (zero new API). Session
902
+ lifecycle tests for code-agent-driver resume semantics (same-key cache hit,
903
+ restore-after-restart resumeSession, different-conversation creates new
904
+ session), TRAP 1 single-flight guard (duplicate taskId replay), and
905
+ autonomous launch on resume. Design:
906
+ docs/product209/08-coding-agent-line.md §4–5.
907
+ - Backoff schedule for bootstrap fetch: 5s → 10s → 30s → 60s cap
908
+ (`BOOTSTRAP_BACKOFF_MS`). Error semantics: 404/500 → backoff, 401/403 → stop
909
+ retries; 401 stopped state reset by key adoption event (§3.4).
910
+ - Memory cap v1 fail-closed (spec16 §8.1 MA-0S, M-CAP-001): the daemon memory
911
+ RPC no longer has an enforce-off branch — `PRISMER_MEMORY_CAP_ENFORCE` is
912
+ deleted and every `/local/memory/*` call must carry a valid
913
+ `x-prismer-memory-cap` (missing → 401 `memory_cap_required`; expired /
914
+ tampered / forged → 401 `memory_cap_invalid`; a cross-workspace request is
915
+ a cap-layer ws mismatch → 401 `memory_cap_invalid` per §13.4 — the old 403
916
+ `memory_ws_scope_violation` is retired). The `prismer memory` CLI and the
917
+ shared adapter tool client auto-carry the cap from `$PRISMER_MEMORY_CAP`.
918
+ The acting identity is always the verified cap subject — body/query actor
919
+ fields are never trusted — and a signed wildcard-scope cap under a
920
+ non-system subject (or a cap whose own ws claim disagrees with its scope)
921
+ is rejected at verify. Deny logs carry only a sha256 prefix of the
922
+ presented token plus a reasonCode. Daemon-internal maintenance (outbox
923
+ flush, WS invalidate, extraction) keeps writing the store in-process
924
+ (system channel), never through the agent RPC gate.
925
+
926
+ ### memory211/03 §7 B1 — memory_browse recency signal(`updatedAt` + `hubsByRecent[]`)
927
+
928
+ - **feat(daemon):** `memory_browse` 结果 hub 行新增 `updatedAt`(epoch ms——hub 行自身
929
+ 与其 child-of 后代的最新更新时间 spread;hub 行在叶节点写入时不会被 re-touch,故按
930
+ spread 算)与 `hubsByRecent[]`(同集 hub 按 `updatedAt` DESC 排序的 recency 信号)。
931
+ Additive:`hubs[]` 保持既有结构序,`index`/`nearest` 不变;无新工具、无 schema 变更
932
+ (`/local/memory/place-context` 同 helper,两处不可漂移)。
933
+
934
+ ### memory211/03 §7.2 B4 — R5 recall-methodology turn metrics
935
+
936
+ - **feat(daemon):** dispatch 度量随既有 `turn.navigation_used` / `turn.shortcuts_taken`
937
+ 追加三枚 `turn.*`:`first_round_hybrid`(第一轮即 batch search ≥2 且同轮 browse)、
938
+ `first_round_direct_read`(第一轮即 load 且无前导 browse——direct-recall shortcut)、
939
+ `tool_rounds`(批内按 `R5_ROUND_GAP_MS` 计工具轮数)。分析式归并 daemon-side 的
940
+ `tool-sequence.ts` ring;与既有两枚同纪律:turn 无 memory 调用 ⇒ 无行(不伪造 0)。
941
+
942
+ ## 2.2.10 (2026-08-07)
943
+
944
+ - Fix Hermes "No LLM provider configured" on sandbox agents: the provider
945
+ bootstrap wrote `model.provider: custom:prismer`, but hermes' custom-provider
946
+ resolution matches `custom_providers[].name` verbatim — `custom:prismer`
947
+ never matched the `prismer` entry, the gateway fell through with
948
+ provider='custom' and no key, and every turn failed at AIAgent init.
949
+ Exposed on 2.2.9: the dual-write made the gateway read the profile config
950
+ (HERMES_HOME override) for the first time, surfacing the format mismatch.
951
+ Write the bare provider name (`prismer`); verified in-pod that
952
+ `_resolve_runtime_agent_kwargs` then resolves api_key + base_url correctly.
953
+
954
+ ## 2.2.9 (2026-08-07)
955
+
956
+ - Fix Hermes sessions EMPTY reply after a process-level interrupt (sandbox
957
+ agent observed live on 2026-08-07): the S10 pollution-rotation state was
958
+ process-memory only, so a daemon restart (OTA kill1 / re-adoption) wiped
959
+ "this session is polluted" and the next turn reused the polluted hermes
960
+ transcript → `empty_reply`. Three changes:
961
+ - `session-health` interrupted/empty-streak state now persists to
962
+ `~/.prismer/hermes-session-health.json` (env `HERMES_SESSION_HEALTH_FILE`
963
+ to override, `setSessionHealthFile(null)` to disable) and reloads on boot —
964
+ a daemon restart keeps the rotation signal.
965
+ - The dispatcher catch marks the session interrupted on CONNECTION-level
966
+ breaks too (gateway killed by adoption re-provision, network drop), not
967
+ only `task_cancelled` / `upstream stall` — sessions-sse never throws on
968
+ application errors, so a thrown error is always an interrupted turn.
969
+ - EMPTY rotation threshold defaults to 1 (`HERMES_SESSION_EMPTY_ROTATE_THRESHOLD`
970
+ to raise) — one empty reply rotates the session instead of burning a
971
+ second user-visible failed turn.
972
+ Design: docs/product209/07 §2 (plan 2 closure).
973
+
974
+ ## 2.2.8 (2026-08-06)
975
+
976
+ - Hermes provider bootstrap written to the ROOT `~/.hermes/config.yaml` +
977
+ `~/.hermes/.env`, not the per-profile dir — Hermes' provider resolution
978
+ (runtime_provider / AIAgent) only reads the root config, so every sandbox
979
+ agent failed with "No LLM provider configured" (2026-08-06). Profile dir kept
980
+ for the gateway `-p` flag.
981
+ - Re-provision Hermes profiles after the WS handshake adopts the real
982
+ per-workspace API key: ACS sandboxes boot with the entrypoint placeholder key
983
+ and any gateway spawned before adoption holds it; on adoption re-run
984
+ `prepareProfile` (rewrites the root config with the adopted key) and kill the
985
+ gateways so the next dispatch respawns with the real env/config. No-op for
986
+ k8s/local (real key from boot, byte-identical rewrite).
987
+
988
+ ## 2.2.7 (2026-08-05)
989
+
990
+ - Fix ACS handshake key adoption reaching the HTTP layer: `CloudClient` froze
991
+ the boot-time placeholder key in its opts at construction, so after the
992
+ handshake adopted the real per-workspace key every HTTP call still sent the
993
+ placeholder and 401'd ("API key not found or revoked"). Profile sync then
994
+ failed, declare stayed 0/0 and the setup parked at `agents_bound`. New
995
+ `CloudClient.setApiKey()` + the adoption block now re-points the live client
996
+ and `PRISMER_API_KEY` env. No-op for desktop/local daemons (real key from
997
+ boot).
998
+
999
+ ## 2.2.6 (2026-08-05)
1000
+
1001
+ - Adopt the cloud-delivered daemonId on the ACS binding handshake
1002
+ (`authenticated` ack extra.daemonId), exactly like the existing apiKey
1003
+ adoption: the sandbox boots with a derived `container:<hostname>` id that
1004
+ never matches the `im_containers` row's canonical UUID, so `declare` would
1005
+ otherwise fail the setup's daemonHasSignal lookup and `daemon_connected`
1006
+ would park forever. Only the ACS binding path sends daemonId, so
1007
+ desktop/local daemons are unaffected.
1008
+
1009
+ ## 2.2.5 (2026-08-02)
1010
+
1011
+ - Established `@prismer/runtime` as the Runtime host under `sdk/prismer`, with
1012
+ Cloud SDK dependencies removed from its control-plane boundary.
1013
+ - Replaced external coding-agent Marketplace plugin lifecycle control with
1014
+ Runtime-owned Claude adapter hooks and a durable provider-independent
1015
+ post-turn job ledger.
1016
+ - Added remote-first skill catalog resolution with verified cache and bundled
1017
+ fallback, including resolver state and post-turn counters in `/healthz`.
1018
+ - Retired Go/Rust SDK builds and four-segment hotfix releases; active packages
1019
+ now use the shared `X.Y.Z` release version and npm/PyPI release matrix.
1020
+
1021
+ ### Added — D1/D2 裁决落地:两个新终态 code + `daemon.declare.retry` 解 park
1022
+
1023
+ cloud 侧本轮实施了 product206/13 的两条用户裁决(D1「显式归属,一次性选择」/ D2「凭证指纹
1024
+ 现在收窄」),daemon 侧要认它们的拒绝,并且要能被**解 park**。
1025
+
1026
+ - **`declare-guard.ts` 分类表新增两个终态 code**(判定逻辑与阶梯一行未动,只是表多了两行):
1027
+ - `WORKSPACE_DAEMON_MISMATCH`(D1)—— 这个 workspace 的**本机槽位**显式归另一台设备,
1028
+ 而归属**刻意不自动漂移**。归为终态正是裁决的要点:把它当可重试等于把「等对方掉线就
1029
+ 抢过来」这条隐式规则从后门放回来。
1030
+ - `DAEMON_CREDENTIAL_MISMATCH`(D2)—— daemonId 属于本账号,但这把 API key 不是认领时那把。
1031
+ daemon 只有一把 key,重试永远拿同一个答案。
1032
+ - 两条都带用户能照做的 hint(`prismer status` / `/healthz.declareBlocked` 逐字透出)。
1033
+ cloud 同时发 `retryable: false`,所以**即使是不认识这两个 code 的老 daemon** 也会按终态
1034
+ 处理(wire hint 压过本地表,R5 既有契约)。
1035
+ - **新增入站帧 `daemon.declare.retry`(cloud → daemon)** —— 用户在云端做完补救动作
1036
+ (批量重认领 / 改派 workspace 槽位)后,cloud 主动叫 daemon 再试一次。**它是这两条出路
1037
+ 端到端能用的那一半**:R5 的终态 park 是有界探测(+5min/+10min 之后彻底停发),没有这个
1038
+ 推送,一个 20 分钟后才被修好的 daemon 只能靠重启进程恢复 —— 而「重启 daemon」正是 R5
1039
+ 出入1 拒绝交付的答案。实现上复用 `onExplicitDeclare()` 这个既有的解 park 缝
1040
+ (`POST /v1/workspace` 走的同一条),**不新增状态机**。帧按 `daemonIds` 过滤(rooms 按
1041
+ 人的 IMUser id 寻址,同账号所有 daemon 都会收到),不点名自己就忽略。纯加法:不认识这个
1042
+ type 的旧 daemon 落进 `handleIncoming` 的静默 default。
1043
+
1044
+ ### Added — R5:declare 被 cloud 拒绝时有出路(分类 + 退避 + 用户可见),local-first 不变
1045
+
1046
+ `agent.host.declare` 被拒时,daemon 过去**把拒绝丢在地上**:`runner.ts` 的 `ws.on('message')`
1047
+ 只对三个 `AUTH_*` code 有动作,其余 error 帧写完 stderr 就落进 `handleIncoming()` 的
1048
+ switch 被 default 吞掉;同时 30s 心跳**无条件**重发同一份注定失败的 declare。用户看到的是
1049
+ 「设备连上了、但一个 agent 都不托管」,没有任何面能说出原因。cloud 侧的 R4(daemonId 认领
1050
+ 鉴权)落地后这条从体验缺陷变成**必现路径**:认领冲突就会走到这里。
1051
+
1052
+ - **新增 `daemon/declare-guard.ts`** —— 分类表 + 退避阶梯 + 阻塞态,一个模块决定这三件事:
1053
+ - **终态**(`DAEMON_ID_CLAIMED` / `DAEMON_FORGOTTEN` / `NO_USER`):重试永远不会成功,
1054
+ 只有人能解。**不再无限重发** —— 拒后只留 2 次有界探测(10×/20× tick,默认 5min/10min)
1055
+ 然后彻底停发。留探测而不是归零,是因为 forgotten / 认领释放这类终态常常几分钟内被人修好,
1056
+ daemon 应该自己发现,不该要求用户重启。
1057
+ - **可重试**(`WORKSPACE_ACTIVE_DEVICE_BUSY` / `DAEMON_IDENTITY_UNAVAILABLE` /
1058
+ `NO_IMUSER_LINKED` / `SHADOW_JOIN_FAILED` / `INTERNAL`):继续重试但**退避**
1059
+ 2×/4×/8×/10× tick(默认 60s→120s→240s→300s 封顶),不再恒 30s 打。
1060
+ - **认证类**(`AUTH_FAILED` / `AUTH_REQUIRED` / `auth_invalid`):**行为不变**,仍是整机停机。
1061
+ 凭证坏了退避没有意义 —— 这条显式确认,不再隐含在「反正都被 default 吞掉」里。
1062
+ - **非 declare 通道的 error**(`WITHDRAW_DAEMON_MISMATCH` / `UNKNOWN_EVENT`):完全不碰
1063
+ declare 状态机(wire 没有可信的 per-frame 关联 id,这张排除表就是关联)。
1064
+ - **未知 code 一律降级为可重试**:新 cloud 发一个老 daemon 没见过的 code,绝不能把设备
1065
+ 变成永久沉默。
1066
+ - **wire hint 压过本地表**:cloud 可在 error 帧带可选的 `retryable: false | {afterMs}`
1067
+ (`ServerEvents.error` 第 4 参,本包外配套)。双向可选、各自兜底,**不引入
1068
+ protocolVersion 协商**。
1069
+ - **`/healthz` 新增 `declareBlocked`**(`LocalServerState`)—— `{code, message, retryable,
1070
+ since, attempts, nextRetryAt, hint}`。这是「**连不上**」(`wsConnected:false`)与
1071
+ 「**连上了但被拒**」(`wsConnected:true` + `declareBlocked`)唯一可区分的地方。字段用
1072
+ spread 发出:没被拒的 daemon 的 healthz 形状与 R5 之前逐字节一致。
1073
+ - **`prismer status` 打印拒绝原因**(新增导出的纯函数 `formatDeclareBlocked`)——「设备未被
1074
+ cloud 接受: <code>」+ cloud 原话 + 怎么办 + 下次尝试时间(park 时明说「不再自动尝试」)。
1075
+ 同一改动里 `readDaemonStatus()` 改为认 `PRISMER_DAEMON_PORT`(原来硬编码 3210,导致
1076
+ desktop / agent-rt pod / 集成 rig 上 `prismer status` 一律误报「daemon 没在跑」)。
1077
+ - **不变量(每条都是 j48 的一道门)**:① local-first —— 被拒**不停机**,本地 run 照跑照写产物;
1078
+ ② 离线 ≠ 被拒 —— socket 断只走 `ws-client` 自己的重连退避,**绝不**置 `declareBlocked`;
1079
+ ③ 正常重连不吃退避 —— 新 `authenticated` 重置阶梯(重连是高频路径);④ 显式用户意图
1080
+ (装 agent / 切 workspace)解 park,这也是「我修好了,再试一次」的入口。
1081
+ - **可调**:`PRISMER_DECLARE_INTERVAL_MS`(默认 30_000,钳 1s–300s)—— 心跳 declare 周期,
1082
+ 整条阶梯以它为单位。生产默认不变,只给运维 / 集成测试压缩墙钟用。
1083
+ - 测试:`test/declare-guard.test.ts` 16 条(分类表 + 阶梯算术 + 四条不变量,注入时钟)。
1084
+ 贯通门 `scripts/test203/journeys/j48-declare-rejection-backoff.ts`:**真 daemon 进程**
1085
+ (从源码起)+ 逐帧记录的 stub cloud,6 条断言全取副作用(wire 上的 declare 帧数与间隔 /
1086
+ `/healthz` / `prismer status` 输出 / 磁盘产物)。负控 `--tamper` 用**源码级自动注入**
1087
+ (把 R5 从一份 src 拷贝里拆掉再起 daemon):实测终态 3 帧 → 30 帧、退避间隔 6/8/16s → 恒 2s、
1088
+ `declareBlocked` 消失、CLI 无输出,四处如期翻红;local-first 与离线两条在 tamper 下仍绿
1089
+ (证明它们不是靠 R5 假绿)。
1090
+
1091
+ ### Fixed — 桌面本地 provider 选择落不了库:`proxyProvider` 新增 `local:<profileId>` 命名空间
1092
+
1093
+ `AgentProfile.config.proxyProvider` 一个命名空间装两类值:云端 provider chain(云端全知)
1094
+ 与**桌面本地 provider profile**(只存在于用户机器的 `~/.prismer/config.toml`
1095
+ `[[providers]]` + keychain,**云端结构性不可知**)。cloud 侧的写入校验只放行它认得的 id,
1096
+ 于是桌面用户从三个入口(`ProxyProviderSelect` / `ModelPicker` / composer 模型切换器)选中
1097
+ 本地 provider 时,每一次写入都 400 `invalid_proxy_provider` —— 这条漂移全仓无任何 doc 或
1098
+ 代码记录过,实测复现(`PATCH /api/im/agent_profiles/<id>` config.proxyProvider='qwen' → 400)。
1099
+
1100
+ 修法是**加命名空间**而不是放松校验:`local:<profileId>` 自描述,cloud 零知识放行;裸的未知
1101
+ 链 id 照旧被拒(release202/12 C2 的防拼错能力一点没丢)。
1102
+
1103
+ - **`resolveLocalProvider()` 认两种形态**(`adapters/shared/local-provider.ts`)——
1104
+ `local:<id>` strip 前缀后按 `profile.id` 匹配;**裸 id 照旧匹配**(前缀出现之前写下的
1105
+ profile、手写 config.toml、既有测试全部不受影响)。新增导出 `LOCAL_PROVIDER_PREFIX` /
1106
+ `isLocalProviderSelector()` / `localProviderProfileId()`(cloud 侧的镜像副本在
1107
+ `src/lib/llm/local-provider-ref.ts`,**前缀字符串必须同步**)。
1108
+ `newapi` / `default` 的保留短路只对**裸** selector 生效 —— 显式 `local:newapi` 指的是用户
1109
+ 正好这么命名的本地 profile,前缀说得毫不含糊。
1110
+ - **未解析的 `local:` 不再被当成云端链走**(hermes `resolvePrismerProviderBaseUrl` +
1111
+ codex `resolveCodexPrismerProvider`)。profile 不在这台机器上 / key 未配 / 类型与该
1112
+ adapter 的 wire 不兼容时,旧逻辑会拼出 `/api/v1/proxy/local%3Aqwen` —— 每一发都 404。
1113
+ 现在落回聚合器 `/api/v1`,与「没选」同一个降级面。**真的云端链 id 仍走
1114
+ `/api/v1/proxy/<chain>`**(这条闸严格限定在 `local:` 命名空间内)。
1115
+ - 测试:`test/local-provider.test.ts` 新增 `local:` 命名空间 6 条 + codex 路由 2 条;
1116
+ 新增 `test/hermes-local-provider-selector.test.ts` 5 条 —— oracle 取 hermes 真正跑的字节
1117
+ (`<HERMES_HOME>/profiles/<p>/config.yaml` 的 `custom_providers[].base_url` + `api_mode`),
1118
+ 不是返回值。负控:拿掉 strip → 7 条红(实测)。
1119
+ 贯通门 `scripts/test203/journeys/j44-local-provider-selector.ts`(真 endpoint + DB oracle)
1120
+ 正向 PASS;`--tamper`(发裸 id = 修复前的前端)如期红,复现的正是那条 400。
1121
+
1122
+ ⚠️ 本条需要 cloud 端配套(不在本包):`src/im/api/agent-profiles.ts::validateProxyProvider`
1123
+ 放行 `local:` 前缀,三个前端选择器发带前缀的值。缺任一层用户仍撞 400。
1124
+
1125
+ ### Added — F1-b:运行中的 daemon 可以改 workspace(`POST /v1/workspace` + `Runner.setDeclaredWorkspace()`)
1126
+
1127
+ 用户在 daemon **已经在跑**的时候新建 / 切换 / 删除 workspace,daemon 与桌面此前**零感知**:
1128
+ `config.toml` 没有 workspace 字段,`PRISMER_WORKSPACE_ID` 在 spawn 时就冻死,
1129
+ `GET /workspaces/sync` 有 client 方法但**零调用方**(死代码)。daemon 会一直服务它启动时
1130
+ 的那个 workspace,直到整个 app 重启。
1131
+
1132
+ - **`Runner.setDeclaredWorkspace(workspaceId)`**(`daemon/runner.ts`)—— 新增
1133
+ `workspaceOverride` 字段,优先级**高于** spawn 冻结的 `PRISMER_WORKSPACE_ID`(原
1134
+ `sendDeclare()` 的取值链 `env || this.workspaceId` 里,env 永远赢,所以只改
1135
+ `this.workspaceId` 是够不到的)。改完立即 `sendDeclare()` 重新声明;socket 断开时
1136
+ 只记录意图,下一次 `authenticated` 时带上。**幂等**:同 id ⇒ `changed:false` 且不发帧。
1137
+ - **`POST /v1/workspace { workspaceId }`**(`daemon/local-server.ts`)—— 桌面主进程
1138
+ 用它把用户当前的 workspace 推给在跑的 daemon。未接 `onSetWorkspace` 的 embedder 答
1139
+ **501**(而不是假装成功)。空串 / 非字符串 / 坏 JSON 一律 400 且不触达 runner。
1140
+
1141
+ **选 redeclare 而不是「切换即重启 daemon」**:重启会杀掉在飞 run;而 `agent.host.declare`
1142
+ 在 cloud 侧本来就是 refresh-on-redeclare("the latest declare wins",shadow 全拆重建、
1143
+ binding 走 `handleHostDeclare` 对同一 daemon 返回 `refreshed`)。**实测**二次 declare 既
1144
+ 不报 409 也不报 `DAEMON_FORGOTTEN`,cloud 正常回 `host.acked`。
1145
+
1146
+ ⚠️ **本条需要配套的 cloud 端修复才生效**(`src/im/ws/handler.ts`,不在本包):cloud 的
1147
+ workspace 解析链把 `im_containers` 行排在 `payload.workspaceId` 之前,而 local daemon 的
1148
+ 那行正是 handler 自己上次 declare 时写的 —— 于是 daemon 被**永久钉死**在第一次 declare 的
1149
+ workspace 上。实测二次 declare 带新 workspaceId,**同一条 socket 上**和**整条连接重连之后**
1150
+ cloud 都照样 ack 回旧的,静默无错。(这也是为什么「重启 daemon」那条退路同样修不好。)
1151
+
1152
+ - 测试:`test/local-server-set-workspace.test.ts`(5 条,真 HTTP,非 mock 路由)绿。
1153
+ 真栈门:`scripts/test203/journeys/j43-daemon-workspace-redeclare.ts` —— 真 WS declare +
1154
+ `im_containers` DB oracle。正向 PASS;负控 `--tamper`(撤掉 redeclare)必红,实测红。
1155
+
1156
+ ### Fixed — desktop205 R1-e:资产投递链的三条残留(`pendingByTask` 泄漏 / auto-scan 去重 / `bindSourceTask` 判别)
1157
+
1158
+ - **`bindSourceTask` 不再靠 id 形状猜**(`daemon/asset/deliver.ts`)。旧判据
1159
+ `taskId.length <= 30 && !/^(session:|run[:_])/` 是**已经在错**而不只是脆弱:
1160
+ `IMTask.id` 与 `IMTaskRun.id` 都是 `@default(cuid()) @db.VarChar(30)`,形状不可分;
1161
+ cloud 对聊天 dispatch 下发的 `payload.taskId` 就是 `IMTaskRun.id`,经
1162
+ `PRISMER_RUN_ID`(spawn 适配器)或从 `<execution_context><run_id>` 抄出的 `--run-id`
1163
+ (hermes)原样走到这里时,形状闸答"看板卡"并 stamp `sourceTaskId` = 一个 `im_tasks` 里
1164
+ 不存在的 id(装得下 VarChar(30),所以不 500,只是静默错绑 + 落进 `/tasks/<runId>`)。
1165
+ 改为按判别式取真值:auto-resolve ⇒ run · 本机在飞 dispatch 的 `payload.kind`(新增
1166
+ `ArtifactsWatcher.activeTaskKind()`,由 dispatch.ts 用 `dispatchKind` 立键)⇒ 权威 ·
1167
+ `mode:'task-attach'` ⇒ 调用方显式声明 · 都问不到才回退形状闸**并打警告行**。
1168
+ - **合成会话行不再往 `pendingByTask` 立无人抽干的桶**(`daemon/asset/deliver.ts`)。
1169
+ auto-resolve 落在 `task_id` 为 NULL 的行(`HermesSessionMapper` 的 `session:<id>` 合成行)
1170
+ 时没有可搭载的 dispatch,`flushPending` 的两处生产调用都按 cloud dispatch id 取键,
1171
+ 写进去的桶永远没人读、随 daemon 生命周期单调增长。现在不 record,`mode:'attach'` 的响应
1172
+ 改带 `ridesReply: false` + 说明(资产仍上传并锚在 `/runs/<runId>`)。
1173
+ - **auto-scan 上传方补上与 `recordDeliveredAsset` 同口径的 assetId 去重**
1174
+ (`daemon/artifacts-watcher.ts`)。cloud `POST /assets` 按
1175
+ `(workspaceId, contentHash[, sourceTaskId])` 去重并返回既有行,于是同一份文件走
1176
+ 「写进 artifactsDir + 显式 `cloud deliver --mode attach`」两条路时同一个 assetId 会被裸
1177
+ `push` 两次,在 `reply.assetIds` 里出现两次。
1178
+ - 测试:`test/desktop205-run-artifact-folder.test.ts` 新增 R1-e 7 条(每条正控配反向负控;
1179
+ fake cloud 复刻真 dedup 规则)。逐条注入故障验红:A `expected 1 to be +0`、
1180
+ B `expected [ 'ast_1', 'ast_1' ] to deeply equal [ 'ast_1' ]`、
1181
+ C `expected 'cmq5r8t1v0002wxyz' to be null`。
1182
+
1183
+ ### Tests — `prismer banner` 的欢迎词行现在被 T0 钉住(apc/04 §5 样例 2)
1184
+
1185
+ `prismer banner`(非 `--compact`)在 banner 之前输出一行固定欢迎词
1186
+ `Welcome to Prismer Cloud`(`src/cli/commands/banner.ts:13`,2026-07-26 由并行线落盘)。
1187
+ 这一行是**桌面本地 feed OTA 环的可观测 oracle**,但此前全仓没有任何测试断言它——
1188
+ 「测试选层绿」对这条改动是空的,apc/04 §5 强制的负控(欢迎词打错字 ⇒ T0 断言红)
1189
+ **根本红不起来**。
1190
+
1191
+ 新增 `test/banner-welcome.test.ts`(真 `buildBannerCommand()` + `parseAsync`,
1192
+ 不 mock 命令本体):非 compact 必须写出该行,`--compact` 必须不写。
1193
+
1194
+ - commit `b3821c22`(首轮 2 条断言)+ `a3478605`(同一 task 的第二轮,锚定「欢迎词必须是 stdout
1195
+ 第一行」并补 `--compact` 非空断言,共 6 条)
1196
+ (task `cms3dwas3034qxze2p6t857ch`,进开发审批 `cms3dwva3034xxze2md8n54yj`,
1197
+ 发版审批 `cms3e9y7b03dlxze2c9fg2fva`)
1198
+ - 绿:`apc test --tier=T0 --json` exit 0 —— T0(root) 56/56 · T0(runtime) 191/191 · T0(desktop) 13/13
1199
+ - 负控红:同一命令在欢迎词打错字后 exit 1 —— T0(runtime) 190/191,
1200
+ `failedNames = ["test/banner-welcome.test.ts (vitest)"]`;红触发 H2 acceptance hook
1201
+ 自动 redispatch(`im_tasks.metadata.acceptanceRedispatchCount = 1`)
1202
+ - OTA 触达(本机 daemon channel local feed):bundle `2.2.4-ota.1785166882`
1203
+
1204
+ ### BREAKING — built-in role template `--from-template ceo` renamed to `--from-template team-manager`
1205
+
1206
+ `sdk/prismer-cloud/runtime/src/templates/roles/ceo.json`'s top-level `templateName` field changed
1207
+ from `"ceo"` to `"team-manager"` (CEO→Team Manager internal naming pass, no compat alias kept —
1208
+ scripts hardcoding `prismer profile create --from-template ceo` will now fail with "Unknown
1209
+ template"; re-run with `--from-template team-manager`). This only renames the CLI-facing template
1210
+ lookup key surfaced by `prismer profile templates` / `getRoleTemplate()` / `listRoleTemplates()`.
1211
+ The role's internal routing identifier `roleTemplate.slug` stays `"ceo"` — unrelated code paths
1212
+ (`profile-defaults.ts`, `orchestrator-lock.ts`, `workspace-orchestrator.service.ts`, …) that branch
1213
+ on `slug === 'ceo'` are unaffected.
1214
+
1215
+ ### Added — `gh-claim-readback`:第一个"他证"型判据(apc/17 §3 相 1 · W1.1 / W1.2)
1216
+
1217
+ `structured-criteria.ts` 既有 6 个 checker 的 ground truth **全部是被判进程自己能写的东西**
1218
+ (它跑的那个文件系统、它能改写的 git 对象库、它自己建的 task 行)。apc/12 §0.10 记的实证:
1219
+ 一棵从没参与本仓库任何发版的独立代码树跑出的 `ota-promote` 产物,与本仓库产物**逐字节相同** ——
1220
+ 本地产物不携带运行身份。
1221
+
1222
+ 新 checker 读的是 **GitHub 服务端签发、且绑死在一个仓库上的 id**:PR number / Actions `run_id` /
1223
+ check-run `conclusion` / branch protection。声明词表 `GH-REPO:` / `PR:` / `RUN:` / `SHA:` /
1224
+ `CHECK: <name> = <conclusion>` / `PROTECTION:`。
1225
+
1226
+ - **跨绑定比对**:回读对象的 `base.repo.full_name` / `repository.full_name` 必须等于声明的
1227
+ `GH-REPO` —— 一个真实但属于**另一个仓库**的 id 是 §0.10 的 free-ride 形态,即使传输层肯服务
1228
+ 它也死在这一比。`repo` arg(skill.json 钉死)另挡"换成自己的 fork"
1229
+ - **fail-closed 三态**(§7 边界 3):无 `APC_GH_TOKEN` / 不可达 / 非 2xx **一律红**,不降级为
1230
+ `env_blocked`。branch protection 的 403 是只读边界在如实作答,仍然是红
1231
+ - **`headShaMustEqualLocal`(W1.2)**:服务端 head sha 必须同时等于声明的 `SHA:` 与本地
1232
+ `git rev-parse HEAD` —— §0.10 列为"待建"的 run-scoped 运行标识的第一个真实现(三方必须同名,
1233
+ agent 只控其二)
1234
+ - **判据侧不 spawn `gh` 二进制**(§3 相 1 ⚠️):`gh` 对"没有 run"答 exit 0,而 doc12 §0.9 的
1235
+ 同一形状已复发 5 次。走 REST + 强制字段比对,从源头不引入那个退出码语义家族。`apiBase` 只是
1236
+ skill.json arg、**故意不给 env 覆盖**:判据回读的端点不该由被判 agent 所处的环境来指定
1237
+ - 测试:**传输不 mock** —— GitHub 响应形态的 fixture server 跑在**独立进程**里
1238
+ (`syncHttpGetJson` 是同步阻塞的,同进程 server 必死锁),checker 走真 HTTP。四条负控
1239
+ (编造 id / 跨仓库真 id / 无 token / 非 2xx)+ 变异证明:摘掉判定后这 11 条负控全部转绿
1240
+
1241
+ ### Security — coding agent spawn env: 凭据 denylist 反转为 allowlist(apc/17 §8.1 W2.1′)
1242
+
1243
+ `filterCredentialBaseEnv` 之前问的是「这个名字是不是**已知的坏**」。拿本仓库 `.env.local` 的
1244
+ 57 个真实变量名实测:**拦 21 / 透传 36**,透传里包括一把**活的** SMTP 口令(`SMTP_PASS` —
1245
+ `_PASS` 匹配不上 `_PASSWORD`)、`REDIS_URL`(URL 形态携带 `user:pass@`)、`SMS_ACCOUNT`、
1246
+ `SMTP_USER`、`K8S_CLUSTER_URL`、`AUTH_URL`。补前缀只是同一形态的又一次枚举。
1247
+
1248
+ - **反转**:新 `isAllowlistedSpawnEnvKey()` 是唯一的门 —— 只有**显式列出**的键能从 daemon 自身
1249
+ `process.env` 继承进 coding agent。原来的名字形态规则降级为**第二道门**,防前缀族
1250
+ (`NODE_AUTH_TOKEN` 这类)夹带。判别式:一个谁都没枚举过的
1251
+ `FUTURE_VENDOR_CREDENTIAL` 现在**默认被拦**(denylist 下必漏)
1252
+ - 大小写归一化到 UPPER:一次同时吃下 Windows 的 `Path`/`ProgramFiles`/`SystemRoot` 与小写
1253
+ `http_proxy`/`no_proxy`,不需要重复条目
1254
+ - **`claude-code-cli` / `codex-cli`(runner.ts 的 D21 fallback adapter)接进同一个漏斗** ——
1255
+ 这两条之前用裸 `{...process.env}` spawn,driver 那条修好了它们还开着,等于没修
1256
+ - overlay 语义不变:`launchEnv` / `taskEnv` / `runtimeSettings.env` / `config.envVars` 在过滤
1257
+ **之后**合入,永远不被剥 —— 那是 code agent 唯一的鉴权通路(网关注入)
1258
+ - 逃生门不变:`PRISMER_CC_NO_CRED_FILTER=1`,且只从 `baseEnv` 读(overlay 关不掉)
1259
+ - ⚠️ **未纳入**:hermes gateway(`{...process.env}` 逐字继承,**零过滤**)、
1260
+ `daemon/shell-executor.ts`、`runner.ts::runShellCommand` —— 三条都不走本漏斗,见报告
1261
+
1262
+ ### Fixed — task 产物根本不过 outbox(desktop205 O14)
1263
+
1264
+ W8 把断云契约修好了,但 `docs/desktop205/03-acceptance.md` §3.3 的「产物 = 落本地 + 进 outbox /
1265
+ 复联 = 补传成功」**只对 drop-folder 成立**。task 产物走的是完全另一条路:
1266
+ `ArtifactsWatcher.deliverFile` / `upload()` 里的裸 `uploader.uploadAsset(...)` —— 不入队,失败
1267
+ 直接抛给调用方,文件留在 task workdir,**复联永不补传**(auto-scan 那条的重试还是纯内存的,
1268
+ 进程一死就没了)。`asset/origin/agent-gen.ts` 这个适配器早就在,但从没接线。
1269
+
1270
+ - **接线**:task 产物在**瞬时**上传失败(`isTransientUploadError`,W8 已有的分类器)时进
1271
+ `OriginOutbox`,由**同一个** `UploadRunner` tick 补传 —— 与 drop-folder 同一个队列、同一份
1272
+ 持久化重试保证、同一个 tray/healthz 积压计数(W13)
1273
+ - **语义选择:在线路径不变,只在「云不可达」时转异步**。不是"一律入队立即返回"——
1274
+ `send` / `message-attach` 两个 mode 结构上需要用 assetId 再打一次 cloud,一律入队会让这两个
1275
+ 功能彻底不可用;而永久拒绝(4xx / policy / MIME)继续抛回调用方,agent 必须知道自己的文件被
1276
+ 拒收,把它排进一个注定 dead-letter 的队列只会把可行动的错误藏起来
1277
+ - **返回**:`attach` / `task-attach`(交付方式就是上传本身)⇒ **202** `{ok:true, queued:true,
1278
+ outboxId}`;`send` / `message-attach` ⇒ 字节照样入队(产物不丢)但**报 502**,因为消息动作
1279
+ 确实没发生,说成功是 agent 会照着行动的谎。`ArtifactsWatcher.deliverFile` 返回类型改为
1280
+ `{status:'uploaded'|'queued'}` 判别联合(唯一调用方是 `asset/deliver.ts`)
1281
+ - ⚠️ **`AgentGenAdapter.fetch` 原来读 `detail.bytes`(内存 Buffer),结构上无法过 outbox**
1282
+ —— outbox 存的是 `payloadJson = JSON.stringify(detail)`,Buffer round-trip 回来是
1283
+ `{type:'Buffer',data:[…]}` 而不是 Buffer(outbox 文件头也明写"不存字节")。task 产物因此
1284
+ 按 **path** 入队、drain 时从磁盘重读(与 drop-folder 同构);`observedAt` 取 **mtime**,
1285
+ 同一份未修改文件重复交付收敛成一行
1286
+ - `SourceHints` 补 `assetKind` / `sourceTaskId` / `adapter` 三个透传位,否则补传出来的资产会
1287
+ 是 `kind=file` 且**不绑看板卡** —— 那是"补传成功"的假象
1288
+ - `test/desktop205-task-artifact-outbox.test.ts`(真 http server + 真 socket + 真 SQLite +
1289
+ 真文件系统,零 mock;断云 = 真关端口)。负控四条,全部实测转红:不复联就断言补传 ⇒ 红 ·
1290
+ 永久 4xx 仍必须 dead-letter(把分类器改成永不判死 ⇒ 红)· 摘掉入队那一步 ⇒ 正控 6/8 转红 ·
1291
+ 同步路径遇 4xx 不得入队。另加 daemon 重启后仍补传(证明持久化不是内存重试)
1292
+
1293
+ ### Fixed — 「归属换人」被当成「服务器确认它没了」(desktop205 O15 残留)
1294
+
1295
+ 收紧到「404 且是我方信封」之后仍有一条通道能通过确认:cloud 的
1296
+ `GET /agent_profiles/:id` 的 where 带 `workspace: { ownerImUserId }`,所以 **profile 还在、
1297
+ 只是 workspace 换了主人**(转移 / 换账号)时也回我方信封的 404 ⇒ 本地 profile + agent 行照删。
1298
+ 「不是你的」是权限变化,不是资源消失。
1299
+
1300
+ - **cloud 侧**(`src/im/api/agent-profiles.ts` GET `/:id`):miss 分支再查一次「不带 owner 约束」
1301
+ 的同 id,回 `{ok:false, error:{code:'forbidden'|'not_found', message:'Profile not found'}}`。
1302
+ 判别位放 `error.code`,**HTTP 状态与 message 文本两个分支完全一致**(沿用 `workdirs.ts` 的既有
1303
+ 范式;403 会把「这个 id 存在」抬到最易探测的那层)。额外查询只在 miss 分支
1304
+ - **daemon 侧**:`confirmProfileGoneOnCloud` 的判据由「信封对」升级为
1305
+ **`error.code === 'not_found'`** —— 正向凭据。`forbidden` / 无 code 的老信封 / HTML / `{}` /
1306
+ 其它状态码 / 网络失败一律不删
1307
+ - **版本漂移是刻意的保守方向**:老 cloud 回裸字符串(无 code)⇒ 不确认 ⇒ 保留行。代价有界
1308
+ (脏 profile 留到 cloud 升级;`host.acked` 的 tombstone 路径 `computeProfilesToDelete`
1309
+ **本来就没有 owner 过滤**,仍会清真正删掉的 profile),反方向(误删活 agent 的行)不可恢复
1310
+ - 负控实测:把判据改回「只要信封对」⇒ 归属换人那条**当场删光两张表的行**
1311
+ (`profiles:0, agents:0`),2 条转红;cloud 侧 `src/im/tests/acp-agent-profile-ownership-404.test.ts`
1312
+ 在改路由前 4/7 红(两种 404 字节完全同形)
1313
+
1314
+ ### Fixed — 删本地 profile 行用弱判据(desktop205 O15)
1315
+
1316
+ `syncProfileFromCloud` 只要 `CloudError.status === 404` 就删掉本地 profile + agent 行。真断云
1317
+ 给的是 `status:0`,所以当时是安全的 —— 但「404」≠「服务器确认这个 profile 没了」:captive
1318
+ portal、反代误路由、滚动发布期的 404 都会命中,而这是**破坏性操作**。
1319
+
1320
+ - `CloudError.code` 帮不上忙:`CloudClient.request` 在 body 不是我们 `{error:{code}}` 对象时会
1321
+ **合成** `code:'not_found'`,nginx 的 HTML 404 与真 404 在 CloudError 上完全同形
1322
+ - 改为要求一次**正向确认** `confirmProfileGoneOnCloud()`:用 `fetchRaw` 重读同一路由(保留了
1323
+ `request` 丢掉的原始 body),必须是 404 **且** body 是我们自己的 JSON 错误信封
1324
+ (`{ok:false,…}`,cloud 实回 `{ok:false, error:'Profile not found'}`)。HTML / 非 JSON /
1325
+ `{}` / 其它状态码 / 网络失败一律**不删**。只在 404 分支多打一次请求,正常路径不加
1326
+ - `test/desktop205-profile-delete-warrant.test.ts`(真 http server 分别回我方信封 / nginx HTML /
1327
+ 关端口,真 CloudClient,真 local.db;oracle 是两张表的行数)。把判据改回裸 404 ⇒ HTML 404 与
1328
+ `{}` 404 **当场删光本地行**,实测转红
1329
+
1330
+ ### Fixed — 断云会永久毁掉产物投递(desktop205 W8,两个真 bug)
1331
+
1332
+ `docs/desktop205/03-acceptance.md` §3.3 的断云契约要求「产物 = 落本地 + 进 outbox」且
1333
+ 「复联 = outbox 补传成功」。实测(`test/desktop205-offline-outbox.test.ts`,真 http server +
1334
+ 真 socket + 真 SQLite + 真文件系统,零 mock)两条都不成立,成因是 `UploadRunner` 里两个叠加的
1335
+ 缺陷:
1336
+
1337
+ - **一次 `drainOnce` 烧光全部 attempts**。`recordFailure` 把非终态行直接放回 `'pending'`,
1338
+ 而 `drainOnce` 的 `while(true)` 会在同一轮里**立刻重新 claim 这同一行** ⇒ `maxAttempts=3`
1339
+ 的三次重试全部发生在同一次 drain、彼此之间没有任何时间间隔。配合 runner.ts 每 1 秒一次的
1340
+ drop-folder tick,**一次 1 秒的网络抖动就足以把一个产物判死**。
1341
+ 修:一次 drain 对同一行最多消耗一次尝试(重复 claim 即释放并跳出)。
1342
+ - **ECONNREFUSED 被算进 dead-letter 预算**。cloud 不可达属于"稍后重试",不是"这些字节
1343
+ 永远不会被接受";旧行为把断云变成**永久数据丢失**(文件被移进 `upload-failed/`,复联后
1344
+ 再也不会补传)。修:新增 `isTransientUploadError()`(socket 级错误码 / `fetch failed` /
1345
+ 超时 / 408·429·5xx),瞬时失败**永不** dead-letter;4xx 等永久拒绝仍照旧判死。
1346
+
1347
+ 负控(防"把 dead-letter 整个拆了"骗绿):永久性 HTTP 400 仍必须在 maxAttempts 次后
1348
+ dead-letter 并把文件移进 `upload-failed/`;以及"不复联就断言复联结果必须红"。
1349
+
1350
+ ### Added — daemon 侧上报 tray 可观测性数据(desktop205 W13)
1351
+
1352
+ 2026-07-26 裁决把桌面 tray 定位成**普通用户唯一的可观测面**(daemon runtime 是服务器形态,
1353
+ `prismer` CLI 对桌面 daemon 结构性失明)。daemon 补上它此前从不上报的两项:
1354
+
1355
+ - **`/healthz.assetOutbox`** `{pending, uploaded, deadLetter, sampledAt}` —— 产物待传/失败积压。
1356
+ 那次清理撞见 2 在飞 + 4 dead-letter,用户完全无从知晓。
1357
+ ⚠️ **计数由 producer 侧采样**:healthz 是纯内存零 I/O 快照而 tray 定时轮询它,
1358
+ handler 里做 `COUNT(*)` 会把每次轮询变贵。采样点是 drop-folder tick(本就每秒开这个 db),
1359
+ 复用既有的 `pendingCount/uploadedCount/deadLetterCount`,新增的只是组合器
1360
+ `snapshotOriginOutboxCounts`
1361
+ - **`/tasks/running.tasks`** `[{taskId, agentName?, kind?, scopeLabel?, startedAt}]` —— 在跑的是谁、
1362
+ 归属哪个 workspace、跑了多久。`scopeLabel` 是 cloud 预渲染的 `identityContext.scope`
1363
+ **原样透传**(不解析)。**不含任务名**:wire 上根本没有 title,只有 `prompt`,而 prompt 是
1364
+ 用户内容,不该画进菜单栏
1365
+ - 两者都走既有 **spread 范式**:runner 没接对应子系统 ⇒ 字段整个缺席,
1366
+ **CLI / K8s 的 healthz 与 `/tasks/running` 形状逐字节不变**;且「没数据」与「数据是 0」
1367
+ 在 wire 上就是可区分的(消费端不得 `?? 0`)
1368
+ - `test/tray-outbox-healthz.test.ts`:真 SQLite outbox → 真 HTTP /healthz 断言积压数;
1369
+ 负控三条(数据源置反 / 停止采样 / state 不带字段),并断言**反复轮询 healthz 不会改变读数**
1370
+ (证明零 I/O 没被破坏)
1371
+
1372
+ ### Fixed — held-out deny 规则不再落共享 settings.json(desktop205 W4)
1373
+
1374
+ `ensureApcHeldOutDeny` 把 deny 规则 read-merge-write 进 **per-daemon** 的
1375
+ `~/.prismer/claude-config/.claude/settings.json`,合并是 `new Set([...existing, ...rules])`
1376
+ **只增不删** ⇒ 一次 APC run 置位后,同机此后所有 coding agent(含无关的用户 agent)
1377
+ 永久继承 held-out deny,且规则藏在 hermetic 目录里用户无从得知。
1378
+
1379
+ - `ensureApcHeldOutDeny`(有文件副作用)**删除**,换成纯函数
1380
+ `apcHeldOutDenySettings()` / `apcHeldOutSettingsArg()`,零文件系统副作用
1381
+ - 规则改由 **per-run** 的 Agent SDK `Options.settings` 承载(= CC 的 `--settings` flag,
1382
+ CC 自己落到临时文件再读)。共享 settings.json 全程不被写
1383
+ - 置位点从 `buildClaudeSpawnEnv`(env builder 不该有文件副作用)挪到 `buildOptions`
1384
+ - ⚠️ 选 `Options.settings` 而非 `extraArgs.settings`:两者编译成**同一个** `--settings`
1385
+ flag,而 SDK 让 `Options.settings` 覆盖 extraArgs 项(`sdk.mjs`:
1386
+ `if (this.options.settings) k9.settings = this.options.settings`)。`buildFastModeOptions`
1387
+ 已经在 fast-mode 机型上设 `Options.settings`,走 extraArgs 会在那些 run 上**静默丢掉**边界
1388
+ - ~~已知残留:老机器上**已被污染**的 `settings.json` 不会自己消失,需一次性清理~~
1389
+ → 已由下面的 W4 迁移收掉
1390
+
1391
+ ### Fixed — held-out deny 的**开关**也变成 per-dispatch(desktop205 W5 / F7)
1392
+
1393
+ W4 只消除了「污染永久化」,开关本身仍是 daemon 全局:`buildOptions` 调
1394
+ `apcHeldOutDenySettings()` **不传参**,读的是 daemon 的 `process.env`。⇒ 想开这个边界,
1395
+ 只能给整台机器上所有 coding agent 一起开 —— 正是 W4 刚刚拆掉的爆炸半径又从正门走了回来。
1396
+
1397
+ - `buildOptions` 改为 `apcHeldOutDenySettings(sdkEnv)`:判据取**这次 spawn 真正拿到的
1398
+ env**。`buildClaudeSpawnEnv` 已经把 dispatch 的 per-task env(`extra.claude.env` →
1399
+ `taskEnv`,以及承载 cloud `metadata.skillConfigEnv` / `roleParamsEnv` 的
1400
+ `launchContext.env`)叠在 daemon env 之上,所以 per-task `APC_HELDOUT_DENY=1`
1401
+ 只窄化这一次 run,同 daemon 并发的其他 coding agent 不受影响
1402
+ - `test/apc-heldout-per-task.test.ts`:oracle 是**注入 queryFactory 捕获的真 SDK
1403
+ `Options`**(真 `ClaudeAgentSession` 跑真 `buildOptions`,探针不复刻合并逻辑)。
1404
+ 负控 = 同一 client(同 daemon、`process.env` 全程不置位)两个 session,只有带标记的那个
1405
+ 拿到规则;另一条负控 = per-task `'0'` 压过 daemon 全局 `'1'`
1406
+ - ⚠️ **仍未接线**:cloud / daemon 两侧都**不存在**「这次 run 是 APC coding-scope」这个判据
1407
+ (`workdir.sourceRef` 从不与 repo 身份或 held-out 列表比对),所以通道就绪、置位方待定。
1408
+ ⚠️ 定位是 **hint 不是安全边界**——`Bash` 结构上就在 CC 的 file-permission check 之外
1409
+
1410
+ ### Fixed — 一次性清理被 pre-W4 代码污染的 settings.json(desktop205 W4 迁移)
1411
+
1412
+ - 新增 `pruneApcHeldOutDenyFromSettings(configDir)`:从 `permissions.deny` 里移除**恰好等于**
1413
+ 旧写入方生成过的规则(含已废弃的 `Write(...)` / `MultiEdit(...)` / `NotebookEdit(...)`
1414
+ 三种拼法——那正是老机器上的大头),用**精确字符串相等**而非前缀/模式匹配,用户自建规则
1415
+ 逐字保留;deny 变空则删该键,文件里其他内容一律不动。非抛出:文件缺失 / 不可解析 /
1416
+ 不可写都是返回值,不是异常
1417
+ - 落点是 `ensureClaudeConfigIsolation`:它本就拥有 hermetic 目录、在任何 CC spawn 前必跑、
1418
+ 已是 per-daemon-process 缓存(= 一次性迁移想要的节奏),且只在隔离开启时触发——污染只可能
1419
+ 在那个前提下产生。失败被吞,清理不阻断 dispatch
1420
+ - `test/apc-heldout-deny-migration.test.ts`:oracle 是 settings.json 的**字节**。
1421
+ 最吃重的负控是「用户自建规则必须存活」,含形似但不在列表里的 `Edit(scripts/test203)`
1422
+ (held-out 路径的前缀)、`Edit(scripts/test203/journeys/j11-…)`(held-out 目录**内部**)等;
1423
+ 把实现换成子串匹配即变红
1424
+ - 已知边界:若将来从 `APC_HELD_OUT_PATHS` **删除**某路径,磁盘上它的旧规则将不再可清理,
1425
+ 需要单独补一条
1426
+
1427
+ ### Fixed — deny 规则表按 CC 2.1.220 实际语法换代(desktop205 W3)
1428
+
1429
+ `APC_DENY_TOOLS` 从 `["Write","Edit","MultiEdit","NotebookEdit"]` 收敛为 `["Edit"]`。
1430
+ CC 2.1.220 实跑证实:`Edit(path)` 是**唯一**参与 file permission check 的路径匹配器,
1431
+ 且它覆盖全部 file-editing 工具;`Write(p)` / `NotebookEdit(p)` 不参与,`MultiEdit`
1432
+ 已不是已知工具。**语义不减弱**,去掉的是每个 held-out 路径 6 条死规则(每次 spawn 54 行
1433
+ stderr 告警)。
1434
+
1435
+ - 新增 `test/apc-heldout-cc-syntax.test.ts`(desktop205 G1):拿**实际发出的** `--settings`
1436
+ payload 起一次真 `claude`(临时 HOME、无凭证、无网络——CC 在鉴权前就校验规则),
1437
+ 断言 permission-rule 告警 **0 条**;负控塞一条 `MultiEdit(path)` 必须出告警。
1438
+ CC 二进制不可得时写 evidence 文件并 **loud skip**,不静默通过
1439
+ - 旧 drift-guard 只比对**路径列表**、不校验规则语法,所以这次 CC 改语法我们完全不知道;
1440
+ G1 把「CC 升级悄悄废掉我们的规则」变成可检出
1441
+
1442
+ ### Fixed — APC skill 源随包分发(desktop205 D8)
1443
+
1444
+ `sdk/apc/skills/` 此前**只存在于 repo working tree**。`resolveApcSkillsRoot()` 从运行
1445
+ 模块向上walk,因此在任何非 repo 宿主(OTA bundle / npm 装的 daemon / 桌面
1446
+ `resources/runtime`)都返回 null ⇒ `installPlatformApcSkills()` 返回 null ⇒ **什么都不装**。
1447
+ 这是藏在平台门背后的第二道静默门,与 2026-07-19「bundle 漏 plugins/ ⇒ 记忆层全黑」
1448
+ **同一失败模式**(打包清单与真实依赖各自演化),修法沿用那次的范式。
1449
+
1450
+ - prebuild hook 现镜像 **两个**源进包根:`built-in-skills/` + 新增 `apc/skills/`
1451
+ - `package.json` `files` 增加 `apc`(npm tarball 已实测含全部 35 个文件)
1452
+ - 两个 OTA 打包器(K8s `build-daemon-runtime-bundle.ts` / 桌面 `build-daemon-bundle.cjs`)
1453
+ stage `apc/` 并在各自 REQUIRED_ENTRIES 里校验
1454
+ - `BUNDLE_REQUIRED_ENTRIES` 与桌面 `REQUIRED_ENTRIES_BY_KIND.daemon` **同步**加
1455
+ `apc/skills`——缺 skill 的 bundle 判为不可用,而不是静默降级
1456
+ - 桌面 `assemble-runtime.sh`(built-in 兜底层,被 `resolveDaemonBundlePath()`
1457
+ **不经校验**直接返回)补 stage `apc/` **与 `plugins/`**。后者是本次挖出的既有缺口:
1458
+ 2026-07-19 那次断言「floor 总是 memory-capable」只对 K8s 镜像成立,桌面这层手工
1459
+ 组装的 floor 从来没有 `plugins/`
1460
+ - 新增 `test/apc-skill-distribution.test.ts`:仓外临时目录里 esbuild 打真模块 → 断言
1461
+ `resolveApcSkillsRoot()` 真返回存在路径(正控);删掉 `apc/skills` 后断言解析为 null
1462
+ **且** `isValidBundleDir()` 判红(负控);四处清单再分叉即红(drift 门)
1463
+
1464
+ ### Fixed — launchd boot shim 是第三份 REQUIRED_ENTRIES 且早就漂了(desktop205 O11)
1465
+
1466
+ `apps/desktop/electron/daemon-boot.cjs` 的注释自称与 `bundle-manager.ts`
1467
+ "same contract … keep them in sync",实际只要 `dist/cli.js` + `package.json`——
1468
+ 2026-07-19 加的 `plugins/` 和 2026-07-26 加的 `apc/skills` 都没跟上。
1469
+ ⇒ **launchd 会启动一个 host 进程判为无效的 bundle**,而「缺 plugins 的 bundle 被启动」
1470
+ 正是那次记忆层全黑事故的形状。
1471
+
1472
+ - shim 的 **bundle** 判据收紧到与另两份逐字节一致(4 项)
1473
+ - **兜底层判据刻意更弱**(只要 `dist/cli.js` + `package.json`):floor 是用户与
1474
+ 「这台机器没有 daemon」之间的最后一道,因为缺 `plugins/` 就拒绝启动它 = 把「降级的
1475
+ daemon」变成「没有 daemon」,还会被 KeepAlive 无限重放。这与 host 侧完全一致——
1476
+ `resolveDaemonBundlePath()` 校验 staged/current **bundle**,然后**不校验**地返回
1477
+ builtin。保持 floor 有能力是打包步骤的职责(`assemble-runtime.sh`,已有断言)
1478
+ - 不从共享模块 require 清单:该文件**只 import node builtin** 是它的立身之本
1479
+ (生成物 = 又一个能在「绝不许起不来」的路径上丢失的东西)。改用 drift 断言机械化
1480
+ - `apc-skill-distribution.test.ts` 的 drift 门从四处扩到**五处**,并把「floor 判据必须是
1481
+ bundle 判据的真子集」也钉住,让这条不对称保持是**决定**而不是退化成漂移
1482
+ - `apps/desktop/electron/__tests__/boot-shim.test.ts`:oracle 仍是 marker 文件
1483
+ (**哪个 cli.js 真的跑了**)。新增负控——缺 `plugins/` 的 bundle 与缺 `apc/skills` 的
1484
+ bundle 都必须被拒且回落到 floor;配套反向负控:完整 bundle 仍能启动(证明门不是无条件拒绝)、
1485
+ 缺 `plugins/` 的 **floor** 仍能启动、缺 `dist/cli.js` 的 floor 仍 exit 1
1486
+
1487
+ ### Added — skill 验收判据引擎:新增 `type:"structured"` 与 6 个 checker(补记)
1488
+
1489
+ > **补记说明**:这批改动分 4 个 commit 落地(`4f17c311` / `689ea719` / `e82db478` /
1490
+ > `c3d91481`)**均未更新本文件**,违反 CLAUDE.md「改 `sdk/*` 任何包必须更对应
1491
+ > CHANGELOG」。由 apc/12 的 code-review 验收跑在审 `c3d91481` 时作为 **convention
1492
+ > 段 major finding** 抓出——这正是该 skill 该抓的东西,故在此补齐而非静默略过。
1493
+
1494
+ `bundle` 的 `matchAcceptanceCriteria` 新增 `type:"structured"` 判据类型(`substring`
1495
+ / `regex` 两条既有路径**逐字节不变**;未知 checker fail-closed)。动机:原判据全部
1496
+ 读 agent 的**报告文本**,因此原则上无法区分「真跑」与「编造」——实证一份零检索、
1497
+ 路径行号瞎写的纯编造报告可拿满分。
1498
+
1499
+ **层 2(复核报告关于「文件」的声称)**
1500
+
1501
+ - `cited-evidence` — 每个 `path:line` 读回磁盘(存在/行范围/非空行/锚定命中/行数下限)
1502
+ - `dimension-coverage` — 每维独占 keyed 行,答案须是可复核引用或 `N/A — 理由`;
1503
+ 可选 `requiredVerdicts` 钉某维 verdict;可选 `anchorToKey` 要求引用落在**声明该维的那一行**
1504
+ - `doc-sync-obligations` — 从 delta 自行重算义务集与 gap 集再比对
1505
+
1506
+ **层 3(复核关于「运行时副作用」的声称)**
1507
+
1508
+ - `json-claim` — 重解析报告内 fenced JSON:钉 schema/形态、散文数值 ⇄ 产物字段、
1509
+ 派生判定须由退出码推出、每层算术、`baselineRecompute`、`freshArtifacts`(产物 mtime
1510
+ 与自带 timestamp 同期)、`filesExist`、`fileEquals`
1511
+ - `declared-id-readback` — `TASK:/ASSET:/CRITERION:/META:` 声明的 id **回读真 API**
1512
+ (含存储 JS 类型,`--set` 的数字化在此被逮)
1513
+ - `git-claim-readback` — 只读 git plumbing:commit 文件集恰好等于声明、branch tip、
1514
+ push 落点须为本地 bare 替身、被拒 prod tag 不在其上、冲突仓 `MERGE_HEAD` 仍在
1515
+
1516
+ I/O 走同步子进程(HTTP 经 `execFileSync(node -e fetch)`,token 由 env 传不进 argv;
1517
+ git 全只读),**`matchAcceptanceCriteria` 签名未变**,两个 CLI 的 `normalizeCriterion`
1518
+ 同步保留 `checker`/`args`(否则结构化条目会降级成 substring 而恒红)。
1519
+
1520
+ ### Fixed — `dimension-coverage` 的 `anchorToKey` 配错不再静默 fail-open
1521
+
1522
+ `anchorToKey` 此前经 `argRecord` 取值,对任何非 plain-object 返回 `undefined` ⇒
1523
+ `true` / `[{…}]` / `"minAnchored:1"` / 键名手滑,**都会静默退回加严前的行为并报满绿**。
1524
+ 而该 arg 存在的唯一理由就是堵「label 承诺大于实现」那个洞 ⇒ 静默跳过使这道守卫沦为
1525
+ 装饰。现改为显式 `checker misconfigured` 判红,与本文件既有惯例一致(`dimensions` /
1526
+ `requiredVerdicts` / `deltaFiles` / `json-claim` args / `allowed.*` 早已 fail-closed)。
1527
+ **缺省不置位时行为仍逐字节不变**,不追溯打红已按旧语义验收的判据。
1528
+
1529
+ ### Fixed — `CITATION_RE` 缺 `%` 导致引用真实文件被判编造
1530
+
1531
+ 路径字符类与 lookbehind 排除集均不含 `%`,而仓库有 19 个真实的 `%5F` 路径文件
1532
+ (admin debug endpoint)⇒ 被解析成 `5Fadmin/...` 判 FAKE。对 `cited-evidence`
1533
+ (`bad.length > 0 ⇒ fail`)是**无条件红**——**这类「如实判红」比「编造判绿」更隐蔽**,
1534
+ 因为它看起来像判据很严。已补 `%` 并加回归;存在性检查未放宽。
1535
+
1536
+ ### Fixed — APC skill 只投平台 workspace,源改工作树(判据修正)
1537
+
1538
+ 上一版把「谁能拿 APC skill」判据挂在 **cloud 的逐 agent 安装记录**上。用户随后给出平台
1539
+ workspace 的正式定义,两条都推翻了它:
1540
+
1541
+ 1. **APC 范围的 skill 只在唯一的平台 workspace 安装**,只用来做 prismercloud 工程本身,
1542
+ **不需要任何发布动作**。⇒ cloud catalog 不该出现在内容路径上(它当时只有 17 个里的 7 个,
1543
+ 且比工作树旧几天)。
1544
+ 2. **平台 workspace = 平台管理员账号绑定的 workspace**。⇒ 判据是 workspace 的 owner,不是
1545
+ agent 的安装行。旧判据选错了对象:真机上被投中的 agent 属于一个**非平台** workspace。
1546
+
1547
+ **新判据**(`isPlatformWorkspace`,daemon 侧):
1548
+
1549
+ - `GET /api/im/workspaces/:id` → `ownerImUserId`;`GET /api/im/users/lookup?identifier=<email>`
1550
+ → 平台管理员的 imUserId;相等即平台 workspace。两个都是既有 endpoint,**cloud 侧零改动**。
1551
+ - 管理员白名单读 `ADMIN_EMAILS`(与 cloud `admin-rbac.ts` 同一个 env、同一套 lowercase 归一)。
1552
+ - **不问 cloud「我是不是 admin」**:`isSandboxAdmin()` 在 `NODE_ENV !== 'production'` 对任何人
1553
+ 短路返回 true,拿它当高危投递的门在 dev 等于没门(apc/14 A3 的教训)。这里是
1554
+ `isAdminEmailStrict` 的 daemon 侧镜像。
1555
+ - **fail-closed 覆盖每一种 unknown**:白名单未配置/为空、profile 没有 workspaceId、workspace
1556
+ 读不到(非成员 404 / 网络错 / 报文异常)—— 一律判 false,且 false 不只是「不装」,而是把
1557
+ entitled class **reconcile 成空**(非平台机器上残留的 APC 目录与新装同样是泄漏)。
1558
+ - verdict 有缓存:正判 30min、负判 60s(`/users/lookup` 是 30/min 限流;正判长 TTL 让已证实的
1559
+ 平台机器在云端短暂不可达时不掉工具链)。
1560
+
1561
+ **源改工作树**:`resolveApcSkillsRoot()` 从模块位置向上找 `sdk/apc/skills/`(dist 3 跳、src 7
1562
+ 跳),`installPlatformApcSkills()` 直接 cpSync。永远是最新版、无需发布、cloud 退出内容路径只
1563
+ 留授权路径。npm 安装态找不到该目录 → null → 不投(只有平台 checkout 才有源)。
1564
+
1565
+ **保留未动**:`persistence → hermes only` 的门逐字节未动;`.prismer-managed` /
1566
+ `.prismer-entitled` 两个互不相交 managed class 与各自 reconciler 的归属护栏;
1567
+ `resolveCodingCwdSkillsDir()` 的 claude-code-only 目标解析。
1568
+
1569
+ 测试 `test/coding-skill-entitlement.test.ts` 18 → **33 例**,四处判据变异全部真红(平台门恒真
1570
+ → 6 红;白名单空时静默兜底 → 2 红;去掉 persistence 拒绝 → 2 红;去掉外来目录护栏 → 2 红)。
1571
+ 真机(活 daemon 未停未重启,走真 endpoint):平台 workspace 的 coding agent 拿到 **17/17**
1572
+ 且含工作树独有的「输出契约」节;非平台 workspace 的 agent **0 个**且旧的 7 个被 evict,12 个
1573
+ `.prismer-managed` 与用户的 `sdk-release` 完好;`ADMIN_EMAILS` 未配置时对**真平台 workspace**
1574
+ 也是 0 且目录不创建。
1575
+
1576
+ ### Fixed — structured acceptance checkers: three contract↔checker misalignments that reddened truthful reports (apc/12)
1577
+
1578
+ 端到端真跑(真 daemon → claude-code → 真回帖)证实结构化 checker 的机制是对的(编造报告
1579
+ `18 citations / 0 verified on disk` 判红,同一份报告下三条旧 regex 判据仍全 PASS),但同时暴露
1580
+ **三条会把 100% 如实的报告判红**的缺陷。「无论对错都会红」与「无论对错都会绿」是同一类 bug。
1581
+
1582
+ - **`CHANGE-POINT` 正则拒绝整行反引号包裹**:前导字符类 `^[\s>*\-|]*` 不含反引号,而
1583
+ `impact-trace` sample prompt 自己给的范例就是整行包裹的 —— 照抄范例 → `no CHANGE-POINT
1584
+ declaration found`。前导类补反引号。
1585
+ - **`rg -c` 的 `path:COUNT` 被当行号复核**:契约明令跑 `rg -c` 对账,其输出正是 `path:数字`。
1586
+ 新增 `parseClaimCitations()`:整行只有一个裸 `path:数字` 的行(即 `rg -c` 输出)不采为
1587
+ citation;`parseCitations()` 保持字面语义不变(调用方会传单行片段给它,容差不能落在那里)。
1588
+ 契约侧同步要求计数写成 `count=N`、不得内联进句子——内联即恢复为声明并照常复核。
1589
+ - **缩写路径判 FAKE 且文案误导**:`verifyCitation` 的失败文案改为点名契约规则
1590
+ (REPO-ROOT-RELATIVE / `rg -c` 计数不是行号),契约侧显式写明必须 repo-root-relative。
1591
+
1592
+ ### Added — `dimension-coverage` 支持钉死 verdict(`args.requiredVerdicts`)
1593
+
1594
+ `{ "<dimension>": "covered"|"gap"|"n/a" }` 要求该维度**自己那一行**以指定 verdict 开头
1595
+ (中文 `已覆盖`/`缺口`/`不适用` 同义)。没有该 arg 时行为逐字节不变;有该 arg 但行上无
1596
+ verdict token、或 verdict 不符、或 `requiredVerdicts` 指向 `dimensions` 之外的键 → fail-closed。
1597
+ 这是 `design-review` 五维审计的承重 oracle:没有它,「五维全标 covered + 正文某处出现 gap 一词」
1598
+ 是一次照样打绿的橡皮图章。
1599
+
1600
+ 测试 `test/bundle-structured-criteria.test.ts` 25 → 36 例(新增 11,每条正控配负控,含
1601
+ 「照契约范例逐字产出的报告必须绿」一组)。
1602
+
1603
+ ### Added — APC held-out write DENY at dispatch (apc/14 D6,双重约束的 dispatch 侧)
1604
+
1605
+ docs/apc/02 §2 R4 的"双重约束"要求 held-out(`baseline.json` / journeys / contract gates /
1606
+ 视觉基线)**物理上打不开写**。review 侧的 diff 门(`scripts/test203/run.ts --review-diff`,
1607
+ 不依赖 agent 诚实)已成;本条补上 dispatch 侧的 deny。
1608
+
1609
+ 隔离 CC 的 hermetic `settings.json`(`~/.prismer/claude-config/.claude/settings.json`,
1610
+ 即 CC 的 `user` settings source)种入 `permissions.deny` 规则——`Write/Edit/MultiEdit/NotebookEdit`
1611
+ 命中 held-out 路径即被 CC 二进制**硬拒**(deny 是硬边界,即使 `bypassPermissions` 模式
1612
+ `canUseTool` 谓词根本不被调用也生效,故落点选 settings.json 而非谓词)。
1613
+
1614
+ - **门**:`APC_HELDOUT_DENY=1`(dispatch 侧显式开,默认 OFF——普通 coding agent 写权限不受影响)。
1615
+ 且只在 config 隔离开启(默认 ON)时生效,绝不写用户真实 `~/.claude`。
1616
+ - **单一真相源**:`config-isolation.ts::APC_HELD_OUT_PATHS` **镜像**
1617
+ `scripts/test203/heldout-guard.ts::HELD_OUT_PATHS`(runtime 包 `rootDir=./src` 无法跨包
1618
+ import,故镜像 + drift-guard 测试读源文件断言两份集合相等——漂移即红)。
1619
+ - **不误伤**:只拒 held-out 写;普通源文件(含 `scripts/test203/run.ts` 本身)、读操作不受限。
1620
+ - 落点 `config-isolation.ts`(`isApcHeldOutDenyEnabled` / `apcHeldOutDenyRules` /
1621
+ `ensureApcHeldOutDeny`,read-merge-write 幂等,不丢无关 settings),种入点在
1622
+ `agent.ts::buildClaudeSpawnEnv`。契约测试 `test/apc-heldout-deny.test.ts`(8 例,每正控配负控,
1623
+ 三处变异各自转红)。端到端(真 CC 进程试写 baseline 被拒)本轮未跑(无活 daemon)。
1624
+
1625
+ ### Fixed — daemon churn 放大成请求风暴(APC Root B:sync/ack 无 backoff + host.acked 全量再同步)
1626
+
1627
+ gap A(下条)用有界 undici dispatcher 封住了请求风暴的**爆炸半径**,但**没治源头**。源头
1628
+ 是一个 churn 循环(`ownership rejected → adopt → re-declare`)让 `host.acked` 在短窗内**连发多次**,
1629
+ 而每次 `host.acked` 都会 (a) 触发一遍**全 workspace** 的 cloud→local memory 再同步,(b) skill-sync
1630
+ 路径**重发每个 skill 的 ack**——**攻击之间无 backoff、快速触发不合并**。打到慢 cloud 上就是自我放大
1631
+ (实测 `skill sync ack failed` ×85 / `Request aborted/timeout` ×107)。
1632
+
1633
+ **修**(新模块 `src/daemon/churn-guard.ts`,两个纯 + 可注入时钟/RNG 的原语):
1634
+
1635
+ - **`ExponentialBackoff`** — 连续失败 → 下次尝试延迟 `base·factor^n`(封顶 + ±抖动),成功即重置。
1636
+ 用于 skill-sync **ack 门**(`skill-sync.ts::ackSkillSync`):按 `(agent,slug)` 记账,一个刚失败的
1637
+ ack 在其指数退避窗口内**跳过重发**(下个自然触发在窗口过后重试,不丢 ack、只是不再猛砸)。base 2s /
1638
+ cap 60s。env kill-switch `PRISMER_DAEMON_ACK_BACKOFF=off`。
1639
+ - **`CoalescingRunner`** — 把短窗内的 N 次 `trigger()` **合并成一次** trailing run,失败后按 backoff
1640
+ 拉开重试间距。`memory/runner-wiring.ts::syncMemoryFromCloud` **改为 debounce**:churn 期一串
1641
+ `host.acked` 只落**一次**全 workspace fan-out(workspace 并集);整体不可达时按指数退避重试而非
1642
+ 每 tick 再发火。默认 1s 窗口(非延迟敏感,见 cloud-dispatcher.ts 注释),env
1643
+ `PRISMER_DAEMON_MEMORY_SYNC_DEBOUNCE_MS=0` 可关。
1644
+
1645
+ 只改 retry/trigger 的**节奏**,不掩盖竞态、不放松任何断言。单测断真行为(间隔指数增长、N 次触发只跑 1 次、
1646
+ 失败按 backoff 拉开),每条修复配一条"原本会抓到它"的变异用例(拆 backoff 成恒定间隔 / 拆 debounce /
1647
+ 拆 ack 门 → 分别红 4/4/1 条,已实测)。**诚实边界**:本轮只证 backoff/debounce 的**逻辑**有牙;真活
1648
+ daemon 下"连接不再涨"的前后对照未跑(有界 dispatcher 已封半径,主会话可随后验)。孤儿 council daemon 的
1649
+ **根治 = 孤儿 conversation soft-delete cascade**,属 `project_orphan_conversations_survive_workspace_soft_delete`
1650
+ 深域,不在本次。
1651
+
1652
+ ### Fixed — daemon cloud fetch 连接池无上限,churn 会撑爆慢 cloud(APC gap C)
1653
+
1654
+ daemon 的 cloud HTTP 客户端(`auth.ts CloudClient`)用 Node 内置 `fetch`,即 undici
1655
+ **默认** global Agent——对单一 origin 的连接数**无上限**。任何请求风暴(如 adopt/ownership
1656
+ churn 在每次 `host.acked` 重放 memory-sync + transport-probe + skill-sync-ack、且无 backoff)
1657
+ 打到一个**慢** cloud(本机 `npm run dev`,HTTP/1.1)时,undici 每个并发请求开一条**新** socket
1658
+ 而非复用 ⇒ 连接爆涨(实测单个 daemon 攥住 **345 条**空闲 ESTABLISHED socket 到 :3000),
1659
+ 把单线程 dev server 的 event loop 饿死(连静态路由都从 0.005s 涨到 24s),并形成恶性循环
1660
+ (慢 → 30s 超时 → 重试 → 更多 socket → 更慢)。删掉该 daemon,server 连接 711→5、延迟 24s→5ms,
1661
+ 把病因隔离到 daemon 的无界连接池。
1662
+
1663
+ **修**:daemon 前台启动时(`daemon.ts::runForeground`,任何 cloud 请求/SSE 之前)安装一个
1664
+ **有界** undici global dispatcher(新模块 `src/daemon/cloud-dispatcher.ts`):`connections` 上限
1665
+ (默认 24,`PRISMER_DAEMON_MAX_CLOUD_CONNECTIONS` 可调)+ keep-alive 复用。再大的风暴也只能开
1666
+ ≤N 条 socket,超额请求在 undici 内排队而不是拿新 socket 砸服务器——结构性封住爆炸半径。
1667
+ 实测 200 并发请求打慢服务器:修前 200 socket,修后 24 socket。WS(走 `ws` 包)与 asset 下载
1668
+ (自带 per-request dispatcher)不受影响。**注**:这只封上限,不治真正的驱动(churn 循环 +
1669
+ skill-sync-ack 无 backoff),后者是另一个更深的 bug。
1670
+
1671
+ ### Fixed — daemon git RPC / workdir jail:三个对抗评审坐实的真缺陷
1672
+
1673
+ 评审用真 git 复现,全部已修 + 补回归用例(`test/git-rpc.test.ts` 26 例,
1674
+ `test/workdir-materialize.test.ts` 25 例,全部带负控)。
1675
+
1676
+ 1. **非冲突的 git 失败被误判成 `conflict`**(`git-rpc.ts`)。旧实现对 git 的**错误文本**
1677
+ 做子串匹配(`/conflict|automatic merge failed|unmerged files/i`),而 git 会把调用方
1678
+ 给的 ref 名回显进错误里 ⇒ `branch fix/conflict-handling`(分支已存在)和
1679
+ `merge feat/conflict-x`(ref 不存在)都被判成 `conflict` 且 `files=[]`,一路透到 CLI 打印
1680
+ "Merge conflict — NOT auto-resolved. Escalate to a human. Conflicted files (0)"——
1681
+ 把拼写错误渲染成需要人类介入的冲突。**现在判据取副作用**:git 自己的未合并索引
1682
+ (`git diff --diff-filter=U`)或在场的 `MERGE_HEAD`,与错误文本无关。真冲突的
1683
+ `files` 因此恒非空。
1684
+ 2. **jail root 可由 payload 抬高**(`git-rpc.ts::runGitExecRequest`)。`workspaceId` 是
1685
+ 拼进 `workspacesDir` 的**路径段**,而它只校验了 `typeof === 'string'`;`path.join`
1686
+ 会归一化 `..` ⇒ `workspaceId:'ws_A/..'` 把 jail root 抬到 workspaces 目录,
1687
+ 实测在**另一个 workspace 的 repo 里建出了分支**。现在用 `isSafeSegment` 校验段。
1688
+ (cloud 侧 member 门此前挡住了 HTTP 可达性,所以这是纵深防御层的洞——而
1689
+ daemon jail 存在的全部意义正是"cloud 错了也不塌"。)
1690
+ 3. **jail 是词法的,symlink 可越狱**(`git-rpc.ts` + `workdir-materialize.ts`)。包含判定
1691
+ 走 `path.resolve`,不解 symlink;而能在 workdir 里种 symlink 的正是这道 jail 针对的
1692
+ coding agent 本人。实测 jail 内 `ln -s <外部 repo> link` + `cwd:'link'` ⇒ 外部 repo
1693
+ 真被写了分支。现在包含判定走 **realpath**(新模块 `src/daemon/path-jail.ts`),
1694
+ 且返回解析后的真实路径(不再从 symlink 穿过去执行)。
1695
+
1696
+ **新增内部模块** `src/daemon/path-jail.ts`:`isSafeSegment` / `realpathBestEffort` /
1697
+ `resolveWithinJail`。安全谓词只此一份——重复实现必然漂移。
1698
+
1699
+ **顺带修的既有债(与上面 3 条分开记账)**:`runner.ts` 的
1700
+ `agent.fs.list` / `agent.fs.read` / `agent.fs.write` / `agent.workdir.materialize`
1701
+ 用同样的方式拼 jail root,同样没校验段,现在四处都加了同一个守卫;
1702
+ `resolveWorkdirCwd` 的词法 jail 同步改成 realpath 判定。
1703
+
1704
+ **行为变化**:`resolveGitCwd` / `resolveWorkdirCwd` 返回的 cwd 现在是 realpath。生产
1705
+ 路径(`~/.prismer/workspaces/...`)无 symlink,解析前后一致;仅测试夹具里的
1706
+ `os.tmpdir()`(macOS `/var` → `/private/var`)会看出差别。
1707
+
1708
+ ### Added — daemon git RPC 接线 + 硬化(APC P0-5 / apc/05 §1 A2)
1709
+
1710
+ `src/daemon/git-rpc.ts` 此前**已存在但零调用方**(只在 `index.ts` export)。本次接线成
1711
+ `agent.git.exec` reverse-RPC(`runner.ts` 新增 case,回 `agent.git.reply`,镜像
1712
+ `agent.workdir.materialize` 的形态),并补四项硬化:
1713
+
1714
+ 1. **cwd jail** —— 新增 `resolveGitCwd(root, cwd)`(镜像 `resolveWorkdirCwd`)。
1715
+ `GitRpcRequest.root` 现为**必填**(fail-closed);root 由 daemon 自己从
1716
+ `workspaces/<workspaceId>` 算,不接受 payload 指定。
1717
+ 2. **remote allowlist** —— push 的 remote 必须是白名单里的**纯名字**(默认 `['origin']`,
1718
+ `PRISMER_GIT_REMOTE_ALLOWLIST` 可覆盖)。URL 形态与 option 形态
1719
+ (`--receive-pack=…`,可在对端执行命令)在进 argv 前就被拒。
1720
+ 3. **commit / merge 回 sha** —— `GitRpcResult.sha`,供 task↔commit provenance。
1721
+ 4. **冲突返回文件清单** —— `GitRpcError.files`(`git diff --name-only --diff-filter=U`),
1722
+ worktree 保持现场(不 `merge --abort`、不 reset)。
1723
+
1724
+ 其它:错误 detail 现同时读 stderr+stdout(git 把 CONFLICT 写 stdout,旧的 `||` 链在
1725
+ stderr 非空时会丢掉冲突文本);git 子进程加 120s 超时(旧实现对不可达 remote 会永久挂)。
1726
+
1727
+ **破坏性(内部 API)**:`gitRpc()` 的 `root` 必填。此前零调用方,无外部影响。
1728
+
1729
+ ### Fixed — drain_respawn no longer hangs on stop()(product205 OTA)
1730
+
1731
+ `armDrainRespawnWatcher` 不再 `await this.stop()`——`stop()` 里的
1732
+ `await servicePool.shutdown()` / `await localServer.stop()` 可能无限卡住
1733
+ (adapter 子进程不退出 / HTTP 连接不关闭),导致 `process.exit(0)` 永远
1734
+ 到不了,daemon 不退出,kubelet 不重启,OTA 中断,用户必须手动重建 pod。
1735
+
1736
+ 修法:drain_respawn 只做同步清理(关 WS + abort in-flight + stop SSE)
1737
+ 然后直接 `process.exit(0)`。K8s/desktop supervisor 重启进程,OS 回收
1738
+ 所有资源。
1739
+
1740
+ ### Fixed — OTA bundle 纯净化(product205 OTA 空腔修复)
1741
+
1742
+ OTA bundle 构建 6 个空腔全部修复:
1743
+ 1. `127.0.0.1` artifactUrl 不可达 → `host.docker.internal`
1744
+ 2. `redis-cli` 不存在 → 用 `ioredis` 清 apply blob
1745
+ 3. 版本黑名单 → 发新版本绕过
1746
+ 4. macOS native 二进制(`fsevents.node` / `rollup.darwin-arm64.node`)→ 删除
1747
+ 5. `built-in-skills/` 缺失(`readSkillText` throw)→ 补进 bundle
1748
+ 6. `better-sqlite3` 目录残留导致 `linkBuiltinNativeDeps` 跳过 → 整个目录删除
1749
+
1750
+ ### Added — action governance runtime P1(product205 M6)
1751
+
1752
+ daemon-side runtime enforcement seam reopens(docs/product205/03 §3.4):
1753
+
1754
+ - `HERMES_YOLO_MODE` 从硬编码 `'true'` 改为 `runtimeApprovalMode` 驱动(team=gated
1755
+ → YOLO off → dangerous-command 桥激活;personal=auto)
1756
+ - `--dangerously-skip-permissions`(claude-code)改为 `runtimeApprovalMode` 条件化
1757
+ - Hermes `approval.request` SSE 事件捕获 §3.5 bundle(`buildApprovalBundleFromHermesPayload`
1758
+ + `classifyActionClass`),透传到 `SessionsSseResult.approvalBundle`
1759
+ - bundle 传播:sessions-sse → sessions-dispatcher → runs-dispatcher → AdapterResult.metadata
1760
+ - `dispatch.ts` `finalError` 附带 `approvalBundle`(runtime-hook 源)随 `task.dispatch.reply`
1761
+ 上报 cloud(code=awaiting_human_approval)
1762
+ - `turn.ask_human`:`task.dispatch.reply` error code=awaiting_clarification + clarifyBundle
1763
+
1764
+ ### Added — persona seatScope(product205)
1765
+
1766
+ `CreateAgentForWorkspaceInput` gains `seatScope`('workspace' | 'council')。
1767
+ Council convene 创建 persona agent 时标 `seatScope='council'`——不占 workspace
1768
+ 席位、不出现在 agent roster。需配合 cloud 侧迁移 523。
1769
+
1770
+ ### Added — general-assistant role template(product205 M4)
1771
+
1772
+ 平台默认 deputy role 模板(通用助理),member first mile accept-provision 用。
1773
+
1774
+
1775
+ ### Fixed — persona role: cloud MCP allowlist(product205 M0 · AC-C6)
1776
+
1777
+ persona role 模板此前 `mcpServers: []`——persona-agent 在 cloud MCP 面无限制
1778
+ (可调 `prismer.task.create`/spend),与动作治理能力黑名单冲突(strip 后暴露为
1779
+ load-bearing)。补 discussion-only `toolsAllowlist`(memory read/recall ·
1780
+ agent/message send · asset 只读;task/skill/publish/spend 面按 allowlist 语义省略即拒)。
1781
+ oracle:`council-m2` 正测 `config.mcpAllowlist` 转绿 + 参数化负控(persona 拒
1782
+ `task.create`、orchestrator 同工具放行对照)。
1783
+
1784
+
1785
+ ### Added — role/council-scoped memory writes(product204 E4)
1786
+
1787
+ 打通 role/council 可见性从 tool → daemon store → outbox envelope → cloud 的全链
1788
+ 路(cloud 侧 `POST /api/im/memory/pages` 早已 honor `body.visibility`,缺口全在
1789
+ daemon/tool 侧):
1790
+
1791
+ - `MemoryVisibility` 联合新增 `{ kind:'role'; slug }` / `{ kind:'council'; id }`;
1792
+ 所有 switch/consumer(store 写入+hydrate、cloud-sync `parseCloudVisibility`、
1793
+ boundary `acl-predicate`、rpc `visibilityToString`)穷尽处理新 kind。
1794
+ - `rpc.ts`:`visibilityToString` emit `role:<slug>` / `council:<id>`(cloud
1795
+ memory-acl 期望的确切串形态);新增 `parseVisibilityString` 把 memory_write
1796
+ 工具传来的 `role:<slug>`/`council:<id>`/`agent:<id>`/`workspace` 串解析回联合,
1797
+ 缺失/未知安全降级为 `workspace`(D3 保守默认)。
1798
+ - `memory-tools.ts`:`MemoryWriteToolInput` + `MEMORY_WRITE_INPUT_SCHEMA` 增可选
1799
+ `visibility?: string`(`required` 仍为 `['path','content']`)。
1800
+ - boundary ACL:role/council 作为粗粒度共享 scope 在 daemon 边界按 workspace-可见
1801
+ 处理,真正的成员治理留 cloud 超集。
1802
+ - 新增回归 `test/memory-visibility-scope.test.ts`:断言 outbox envelope 串
1803
+ `council:<id>`/`role:<slug>` 精确落地 + 负控(缺失→workspace、未知→workspace)。
1804
+
1805
+ ### Fixed — 代码评审整改(feature/release203 批次)
1806
+
1807
+ - **hermes gateway 每轮杀重启风暴**:`writeHermesGatewayStamp` 写盘失败(profile
1808
+ dir 不可写)时,`hermesGatewayConfigDrifted` 把本进程刚 spawn、只是没记下的
1809
+ gateway 当成无见证孤儿 → 每条消息 kill+respawn、清空会话上下文。spawn 时同时在
1810
+ 内存里见证(键用 stamp 绝对路径,生产复用/测试隔离两不误),写盘失败也不再 churn。
1811
+ 补回归负控 `test/hermes-gateway-stamp.test.ts`(抽掉内存兜底即翻红)。
1812
+ - **`cloud role test` 把瞬时网络错误当「skill 不存在」**:只有 404 才是真缺失;
1813
+ status 0 / 5xx / timeout 归为 `unreachable`(exit 2 + 「transient, retry; do NOT
1814
+ rebuild」),不再误导 agent 去重建已存在的 skill。
1815
+
1816
+ ### Added — `prismer skill|role` 发布后治理七动词(product204/21 M-P W3)
1817
+
1818
+ 发布之前有 `publish`,发布之后此前**什么都没有**。补齐镜像动作,skill 与 role
1819
+ 两个命名空间同构(blueprint 无 CLI 命名空间,仅 HTTP + Studio):
1820
+
1821
+ - **`delist <slug> [--reason]`** / **`relist <slug> [--changelog]`** — 退出/回到
1822
+ Marketplace 的**可见性单轴翻转**。存量使用者零影响(skill 已装 agent 继续
1823
+ 同步内容、role 继续投影)。relist 不是新端点,是重跑该资产自己的 publish
1824
+ 端点——publish 的门(license / SS-02 / takedown 锁)因此只有一份实现。
1825
+ - **`deprecate <slug> --reason <r> [--successor <slug>]`** / **`undeprecate`** —
1826
+ 软信号 + 替代品指针;**不拦截**新安装/新 apply,只让消费者看见理由与后继。
1827
+ - **`archive <slug> [--confirm]`** — 破坏性退役。**有活跃消费者时服务端 409**,
1828
+ CLI 把消费者计数打成人话并提示 `--confirm`("3 agents are still using this
1829
+ skill — archiving unbinds them")。public role 直接 archive → 提示先 delist。
1830
+ - **`transfer <slug> --to <imUserId>`** + **`transfer-accept`** / **`transfer-abort`** —
1831
+ 两段式所有权转移;offer 阶段 owner 不变,受让人 accept 才落。
1832
+ - **`published`** — 我的发布清单表格:slug / 上架状态(listed·delisted·deprecated·
1833
+ taken_down·archived)/ **消费者数**(= archive 的爆炸半径)/ installs / 在途转让。
1834
+
1835
+ **错误码人话化**(新 `cli/publish-lifecycle.ts`,与 `@prismer/sdk` 的 `cloud`
1836
+ CLI **共享同一张表**,两包不允许漂移):`not_owner` / `has_active_consumers` /
1837
+ `taken_down` / `changelog_required` / `successor_not_listed` / `transfer_pending` /
1838
+ `not_transfer_target` / `must_delist_first` … 一律译成"原因 + 出路"的整句,
1839
+ 不再抛裸 HTTP 码。
1840
+
1841
+ > **承重语义**(SKILL.md 与文案都必须传达):skill 安装与 role→agent 是**活引用,
1842
+ > 不是快照**——改已发布件的内容会自动传播到全部使用者,故内容改动强制带
1843
+ > `--changelog`;**下架不影响存量使用者,归档才解绑**。
1844
+
1845
+ 同步更新 built-in skill `skill-builder` / `role-builder` 的 SKILL.md
1846
+ (新增「发布后管理」节)与 `docs/api/publish-lifecycle.md`。
1847
+
1848
+ ### Added — `agent.host.declare` 上报 `bundleVersion`(product204/08 §2.3 step 4,M9 收口)
1849
+
1850
+ - declare payload 新增 additive 字段 `bundleVersion`:**实际在跑的 runtime 版本**
1851
+ (boot-time OTA 换入的 bundle 即 bundle 版本;builtin boot 即镜像/npm 版本——
1852
+ 进程 exec 的是 bundle 的 cli.js,自身 package.json 就是真相)。cloud 侧落
1853
+ agent card `metadata.bundleVersion`,runtime-skew 探针 fleet 分布随之从
1854
+ `im_agent_bindings.daemonVersion` 升级为真实运行版本(daemonVersion 保留为
1855
+ legacy fallback)。老 daemon 不带该字段 → cloud 行为不变。
1856
+
1857
+ ### Fixed — role-builder `ingest-role.mjs` 默认公域回归(M9 收口,随 bundle 分发)
1858
+
1859
+ - `built-in-skills/role-builder/scripts/ingest-role.mjs` 默认路径回归为 admin
1860
+ 公域 catalog(违反 product204/16 §2.3「CLI 默认私域」;根因=M5 的改动落在
1861
+ **gitignored 的 runtime 拷贝**、被 runtime 重建用 canonical 旧版覆盖——
1862
+ lost-uncommitted-change,j25 A4 回归抓到)。修复落 **canonical**
1863
+ `sdk/prismer-cloud/built-in-skills/`:默认 `/mine`(private),公域须显式
1864
+ `--publish`(alias `--admin-catalog`),`--mine` 保留为 no-op 兼容。
1865
+
1866
+ ### Fixed — 无 config 环境任何 `prismer` 子命令崩溃(M9 收口)
1867
+
1868
+ - `cli/commands/task.ts` 的 `mkTaskWaitAdapter` 与 `cli/commands/workspace.ts`
1869
+ 的 `mkMemberAdapter` 在 **program 构建期** 急切 `mkCloud()`(→ `loadConfig`),
1870
+ 导致没有 `~/.prismer/config.toml` 的主机上 `prismer --help` / `prismer ota
1871
+ status` / 一切子命令直接崩 "Config not found"。CloudClient 改为**首次调用时
1872
+ 惰性创建**,与 daemon 轮询等 config 的设计对齐——OTA resolver(`prismer ota
1873
+ resolve`)必须在 pre-setup 环境可用(08 §2.3 entrypoint 钩子)。
1874
+
1875
+ ### Added — boot-time runtime-bundle OTA 消费链(product204/08 §2.3,M9-β)
1876
+
1877
+ - **`daemon/ota/`(新模块)**:K8s/CLI 侧 bundle OTA 状态机——
1878
+ 检查(manifest 3s+重试,候选序 `PRISMER_BUNDLE_MANIFEST_URL` env →
1879
+ `/api/runtime/update/manifest` → `/api/desktop/update/manifest`,两种
1880
+ payload 形态均容忍)→ 下载 → **sha256/sha512 + Ed25519 双验**(签 zip 原始
1881
+ 字节;公钥 `DAEMON_BUNDLE_PUBKEY` env 可覆盖、内置常量与桌面同 keypair;
1882
+ 验签失败=显式拒绝+incident 回传,**绝不落盘执行**)→ 系统 `unzip` 解包到
1883
+ `~/.prismer/bundle/<version>/` → 解包后 `package.json` 版本≠manifest 版本
1884
+ 即拒(双版本源 drift 门禁)→ 原生依赖(better-sqlite3)解包时从镜像内置
1885
+ runtime **symlink 借入**(ESM 不查 NODE_PATH——桌面 bundle-manager 已
1886
+ code-verified 的教训,08 §2.3 "NODE_PATH 借用"的语义落点)→ 原子指针切换
1887
+ (`previous` = 回滚位)。
1888
+ - **回滚 + 熔断**:boot-attempt marker 跨容器原地重启存活(emptyDir 语义,
1889
+ **无 PVC**);上次 boot 未确认 → 计 strike + `boot_failed`/`rolled_back`
1890
+ 回传 + 回退 previous;**连续 2 strikes → 本地拉黑 + 删目录(防复活)**,
1891
+ 停留 previous/镜像 builtin(镜像永固兜底,任何失败路径都不 brick)。
1892
+ - **incident 回传**:走既有 `POST /api/desktop/update/report`(migration 499
1893
+ 词表,`component='daemon'`)——`applied`/`boot_ok`(熔断统计分母)+
1894
+ `verify_failed`/`boot_failed`/`rolled_back`/`blacklisted`。⚠️ 该路由现有
1895
+ `DAEMON_ID_RE` 只认桌面形态 id,K8s `container:<pod>` 形态会被
1896
+ logged-then-dropped——server 侧放宽挂 α/收口波。
1897
+ - **`prismer ota (resolve|status)`(新 CLI verb)**:`resolve --exec-path`
1898
+ 是 K8s entrypoint 钩子(stdout 只打 bundle cli.js 路径,builtin=空输出,
1899
+ 日志全走 stderr);`PRISMER_BUNDLE_OTA=0` 本地跳过(server 侧 kill switch
1900
+ = manifest decision=none)。
1901
+ - **boot 确认**:`daemon start` 成功后 10s 稳定窗(`scheduleBundleBootConfirm`)
1902
+ 清 marker + 清 strike 记录 + 回传 `boot_ok`;窗口内崩溃 ⇒ marker 存活 ⇒
1903
+ 下次 boot 自动计 strike 回滚。
1904
+ - 测试:`test/ota-bundle.test.ts`(13 用例:真 keypair/真 zip/真本地 HTTP
1905
+ server 全链 + 篡改负控 + 回滚/拉黑注入 + 回传 payload 断言)。
1906
+
1907
+ ### Removed — `PRISMER_UNIFIED_WS` 逃生口(product204/18 Wave 3,挂 M9 收割)
1908
+
1909
+ - `daemon/runner.ts` 不再读 `PRISMER_UNIFIED_WS`:unified `WS /ws/realtime`
1910
+ 是唯一 realtime 路径,legacy `SseSubscriber`(`/api/im/sync/stream`)不再
1911
+ 被 runner 实例化(模块保留,仅测试引用)。设 `PRISMER_UNIFIED_WS=0` 的部署
1912
+ 自本版起为 no-op。
1913
+
1914
+ ### Added — per-agent skill config env injection(product204/09 §2.3 Phase C)
1915
+
1916
+ - **`adapters/prismer-env.ts::skillConfigEnvFromMetadata`**:读取 cloud 派发时
1917
+ 解析好的 `task.metadata.skillConfigEnv`(KEY→value,UPPER_SNAKE、禁
1918
+ `PRISMER_` 前缀——cloud 入库门禁保证),`applyPrismerScopeEnv` 尾部以
1919
+ **覆盖语义**并入子进程 env(三级解析 agent 覆盖/role 默认 高于 daemon 全局
1920
+ env;缺省 = 不注入 = global env 兜底)。coding 路径(claude-code / codex /
1921
+ provider-proxy-env)零改动即消费。
1922
+ - **hermes adapter(seam b)**:`dispatch()` 把 `skillConfigEnv` 循环
1923
+ `writeEnvValue` 进 per-profile `.env`(gateway 长驻进程不走 spawn env;
1924
+ per-profile 文件 ⇒ 同 daemon 多 agent 隔离是结构性的)。
1925
+ - **`daemon/skill-loader.ts::parseSkillConfig`**:SKILL.md frontmatter `config:`
1926
+ 声明的 daemon 侧宽松镜像解析(诊断用;值永远来自 dispatch metadata,不做本地
1927
+ 解析注入)。`LoadedSkill` 增 `config` 字段。
1928
+
1929
+ ### Added — identityContext named sections(product204/07 Phase C · 分段注册制 D6)
1930
+
1931
+ - **Wire type** `TaskDispatchRequestPayload.identityContext.sections?: IdentitySection[]`
1932
+ (`{ id, owner, order, content }`,additive;旧 daemon 忽略该字段,旧 cloud 不发
1933
+ ——双向 version-skew 安全)。
1934
+ - **`daemon/dispatch.ts::renderIdentityLines`** 扩展:校验(content string +
1935
+ order finite number)→ 按 `order` 升序稳定排序 → trim → 滤空,作为
1936
+ `sections: string[]` 与 identity/user/scope 一起进 `metadata.identityContext`;
1937
+ `hasIdentity` 判定包含 sections。
1938
+ - **三个 adapter 消费面**(coding `code-agent-driver` / `claude-code` legacy CLI /
1939
+ hermes instructions slot):sections 以 `\n\n` 连接在既有 identity 三行**之后**、
1940
+ 同一 native identity slot。hermes 侧只进 per-turn `instructions`,**不写 SOUL.md**
1941
+ (D6 红线:动态 envelope 注入,不落任何持久文件)。
1942
+ - 首个注入段:cloud 侧 `platform-directives`(order 20,仅 active orchestrator
1943
+ dispatch 携带);order 30/40(config-directives / per-turn-voice)为 09/03 预留槽。
1944
+
1945
+ ### Changed — `role create` defaults to the PRIVATE domain(product204/16 §2.2 M5)
1946
+
1947
+ **Behavioral flip, no version bump.** 创作默认私域,进公域是显式动作:
1948
+
1949
+ - **`prismer/cloud role create`** 默认改打 `POST /api/im/role-templates/mine`
1950
+ (owner-scoped private,任何已认证 key 可用,落 Studio `/mine`)。旧默认
1951
+ (admin 公域 catalog 直写)收敛为显式 `--admin-catalog`(非 admin 403)。
1952
+ `--mine` 保留为向后兼容 no-op。
1953
+ - 新 `--publish`:create 后自动 `POST /:slug/publish`(走 RO-11/RO-6 publish
1954
+ gate),一步进公域 Marketplace。
1955
+ - **`role-builder/scripts/ingest-role.mjs`** 同步反转默认(头注释与代码此前
1956
+ 自相矛盾——注释声称默认 `--mine`、代码默认公域;现在代码与文档一致),同样
1957
+ 新增 `--publish` / `--admin-catalog`。role-builder `SKILL.md` 口径同步。
1958
+
1959
+ ### Added — `skill publish` + `role export`(product204/16 §2.2(c) / §2.5)
1960
+
1961
+ - **`skill publish <slugOrId> [--license --changelog]`** — 显式 owner 动作,把
1962
+ skill 从私域(`publishScope='workspace'`,create 默认)翻到公域 Marketplace
1963
+ (`POST /api/im/skills/:id/publish-template { scope:'marketplace' }`)。注意:
1964
+ marketplace 搜索面自本版起只见 `publishScope='marketplace'`——不 publish 的
1965
+ skill 他人搜不到。
1966
+ - **`role export <agentImUserId> [--out <dir>] [--ingest]`** — 把活 agent 的人格
1967
+ 晶化为 role bundle(`GET /api/im/agents/:id/role-bundle`,SOUL 取 RAW
1968
+ operatingPrinciples,不含运行时注入子句,round-trip 不堆叠);`--ingest` 直接
1969
+ `POST /role-templates/mine` 落私有 role。
1970
+
1971
+ ### Added — daemon declares its device identity(desktop204 D204-3)
1972
+
1973
+ `agent.host.declare` 现在恒带 `daemonLabel` + `daemonKind`。此前 daemon 只发
1974
+ `daemonId / daemonVersion / platform`,cloud 只能靠 `daemonId.startsWith('daemon-')`
1975
+ 猜 kind、拿 daemonId 后缀当 label——对任何不遵守该前缀约定的 id 都是错的,且永远
1976
+ surface 不出用户自己起的设备名。
1977
+
1978
+ - 新 `src/daemon/device-identity.ts`:
1979
+ - `resolveDaemonKind()` — `PRISMER_DEVICE_KIND` > `KUBERNETES_SERVICE_HOST`(⇒ `k8s`)> `local`
1980
+ - `resolveDaemonLabel()` — `PRISMER_DAEMON_LABEL` > config.toml `daemon_label` > OS hostname > daemonId(永不为空)
1981
+ - config.toml 新增可选 `daemon_label`(用户自定义设备名)。
1982
+ - `device-dir.ts` 的 `inferDeviceKind()` 收敛到同一个 resolver —— `device.json` 与
1983
+ declare 线上的 kind 不可能再互相打架。
1984
+ - cloud 侧字段 / DB 列早已存在(`im_agent_bindings.boundDaemonKind/Label`),**无需迁移**;
1985
+ cloud 的猜测逻辑保留为旧 daemon 的 fallback。
1986
+
1987
+ ### Added — `agent.host.withdraw` on intentional shutdown(desktop204 D204-4)
1988
+
1989
+ daemon 现在会在**主动关闭**时(SIGTERM/SIGINT ⇐ ⌘Q / `prismer daemon stop` / pod
1990
+ terminate)**drain 完成后、WS 关闭前**发 `agent.host.withdraw`。cloud 侧 handler /
1991
+ service / 错误码此前是**写好的死代码——没人发过这个事件**,于是 ⌘Q 后 cloud 要等满
1992
+ **3 分钟**心跳陈旧才把 agent 标离线,这 3 分钟内派给它的任务全是黑洞。
1993
+
1994
+ - `Runner.stop(opts?: { withdraw?: { reason, timeoutMs? } })` —— 只有**显式传 intent**
1995
+ 才发(崩溃 / auth-failed / version-skew respawn 不发:同一个 daemonId 马上会回来,
1996
+ binding 不该被标成可回收)。
1997
+ - `inflightDrained` 取 stop 时是否还有在跑的 run(有 ⇒ `false`,cloud 知道该 requeue)。
1998
+ - 新 `WsClient.sendAndFlush(msg, timeoutMs)` —— 等帧真正写进 socket 再返回,
1999
+ 避免 `close()` + `process.exit()` 把 withdraw 一起蒸发掉。
2000
+ **best-effort 是硬约束**:socket 不 OPEN / flush 超时 → 立刻 resolve `false`,
2001
+ 绝不抛、绝不阻塞退出(离线 ⌘Q 最多多花 1.5s)。
2002
+
2003
+ ## 2.0.9
2004
+
2005
+ ### Fixed — sandbox daemon 镜像 stale-tag 陷阱(线上 daemon 落后 cloud)
2006
+
2007
+ 镜像 tag 从 `/VERSION` 派生(`daemon-v$(cat VERSION)`),pod 按 **tag** 引用镜像
2008
+ (`getDaemonImage()` 返回 `canonical`,`digest` 仅供诊断),且 test/prod 是
2009
+ `imagePullPolicy: IfNotPresent`。
2010
+
2011
+ 于是当一次发版没有 bump `/VERSION` 时:CI 的 `build_sandbox_image` 确实重新构建
2012
+ 并 push 了新镜像,但**tag 名字不变** → kubelet 看到节点已缓存该 tag,永远复用旧
2013
+ 层。cloud 前进、daemon 原地不动 —— test 环境因此用 07-07 的 daemon 跑
2014
+ release203/28 的 cloud(会话压缩 + systemHidden + memory 抽取/召回均不匹配)。
2015
+
2016
+ 本次 bump `/VERSION` → `2.0.9` 使 tag 变为 `daemon-v2.0.9`,强制 kubelet 拉取新
2017
+ 镜像。约束已写入 `infra/sandbox-image/image-pin.yaml` 顶部注释:**任何触及
2018
+ `sdk/prismer-cloud/runtime/` 的发版必须 bump `/VERSION`。**
2019
+
2020
+ ## Unreleased
2021
+
2022
+ ### Fixed — release203/10: skill 下发闭环三环修复(workspace203 体系化 e2e 全绿)
2023
+
2024
+ skill 从"安装"到"运行中 agent 真能用"的完整链路修复。根因:hermes gateway 的
2025
+ `HermesSkillLoader` 在 spawn 时从 `profile.config.skillsDir` 固定 skillsRoot;
2026
+ 若启动时未注入 device dir,则读 profile dir,而 skill-sync 写 device dir → 错位。
2027
+
2028
+ - **目录错位** (`daemon/skill-sync.ts` + `daemon/runner.ts`): 新增纯函数
2029
+ `withPerAgentSkillsDir(profile, paths, daemonId)`;`syncProfileFromCloud` 的
2030
+ prewarm 用它注入 device skillsDir,使 gateway 从启动即读 skill-sync 写入的
2031
+ `devices/<did>/agents/<aid>/skills/`。此前 prewarm 用未注入的 profile 启动 →
2032
+ 读 profile dir → 已安装 skill 对 agent 不可见。
2033
+ - **活进程缓存不刷新** (`daemon/dispatch.ts` + `daemon/runner.ts`): dispatch 的
2034
+ skill-sync `synced>0` 时经新增的 `DispatchDeps.dropService` drop 缓存的
2035
+ hermes 服务,`ensureService` 以含新 skill 的 device dir 重启。运行中 agent
2036
+ 安装 skill 后**当次 dispatch** 即可用(此前需服务重启才可见)。revision-diff
2037
+ 保证稳态 `synced==0` 不再重启,无 churn。
2038
+ - **首次冷启动根治** (`daemon/runner.ts`): `syncProfileFromCloud` 在 prewarm
2039
+ 前,当确实注入了 device skillsDir,先 `syncInstalledSkillsForDispatch` 铺
2040
+ skill 到 device dir,再 `servicePool.drop` 旧服务,使 gateway 以 warmProfile
2041
+ (device-dir loader)重建、其 skill loader 首次扫描即读完整 skill 集。
2042
+ blueprint 实例化 / bind 的 agent **首次 invoke** 即命中,免冷启动窗口。
2043
+
2044
+ 端到端验证:`docs/workspace203/20-systematic-test-execution.md` —— workspace203
2045
+ 体系化 flow(前置1-4 + workspace1-2)S0-S7 真机全绿,全程 DeepSeek flash。
2046
+
2047
+ ### Changed — memory203/20 W-B: extraction prompt v2 (richness-balanced) + `web_load` prismer:// + oversized advisory passthrough
2048
+
2049
+ - **Extraction prompt v2** (`daemon/memory/extract.ts`, ruling 0705-3/4
2050
+ "record, never limit"): removed the "emit minimal valid PKF" self-framing
2051
+ and the `BUDGET: … prefer 1-3 DENSE pages / concise` compression language
2052
+ (doc 19 §3.4 ①② — the structural scarcity that flattened pages to prose).
2053
+ New posture: richness-balanced PKF — frontmatter script with a **REQUIRED
2054
+ one-sentence `description`** (feeds the cloud description column, doc 20
2055
+ §1.1), `<h2 id>` sections, full typed-link vocabulary incl.
2056
+ `supports`/`contradicts`, the FULL `<prismer-data>` view set
2057
+ (`table|bar|line|scatter|area|heatmap` — "never flatten a source table into
2058
+ prose"), and asset POINTERS (short description +
2059
+ `<a rel="derived-from" href="prismer://asset/<id>">`, never a body copy —
2060
+ doc 20 §1.4 asset 零镜像). The ONLY token rules are anti-waste: ANTI-REPEAT
2061
+ (same-topic page in the recall context ⇒ placement MUST be extend/attach,
2062
+ never a near-duplicate new page) + circular-recall discipline (pages already
2063
+ in the provided context are not new sources to re-query). Prompt exported
2064
+ (`EXTRACTION_SYSTEM_PROMPT`) so the guidance lane (W-C skills) teaches
2065
+ consistently.
2066
+ - **`MEMORY_EXTRACT_MAX_TOKENS` default 2048 → 8192** (record-not-limit):
2067
+ usage is recorded, not constrained — gateway `usage.input_tokens`/
2068
+ `output_tokens` now flow onto `ExtractTurnResult.promptTokens`/
2069
+ `completionTokens` and into the `llm_response` stage line
2070
+ (`pages=N, chars=M[, tokens=in:X/out:Y][, truncated]`, exported
2071
+ `formatLlmResponseDetail` in `hook-server.ts`). Truncation salvage stays as
2072
+ the pure fallback. Paired: `MEMORY_EXTRACT_TIMEOUT_MS` default 60s → 120s
2073
+ (measured: 4096 tokens took 29–44s; the timeout must outlast the 8192
2074
+ ceiling or the abort-drops-memory bug returns).
2075
+ - **Attached-asset ids reach extraction** (additive contract): the provider
2076
+ shell's `sync_turn` now parses `<attached_assets><asset id="…"/></…>` out of
2077
+ the turn text and stamps `extra.attached_asset_ids` on the `post_llm_call`
2078
+ body; hook-server threads it into `ExtractInput.attachedAssetIds`, and
2079
+ `extract.ts` falls back to parsing the XML itself
2080
+ (`parseAttachedAssetIdsFromTurn`) for older shells. The user prompt lists
2081
+ the real `prismer://asset/<id>` pointer URIs so pages reference assets
2082
+ instead of mirroring them.
2083
+ - **`web_load` accepts `prismer://` URIs** (doc 20 §2.2 / 19 B9): scheme
2084
+ allowlist widened from http(s)-only to http(s) | `prismer://` in BOTH the
2085
+ provider shell (`plugins/memory/prismer/__init__.py`) and the daemon route
2086
+ (`daemon/web/rpc.ts`); the daemon still forwards to the cloud Load API
2087
+ unchanged (it natively resolves `prismer://<owner>/asset/<sha>` and
2088
+ `.../file/...`). Tool description retargeted: load web pages OR workspace
2089
+ assets/files — use it instead of re-reading raw sources memory already
2090
+ points at. Garbage schemes (file://, ftp://, …) still 400 `invalid_url`.
2091
+ Spec sync in `adapters/web-tools.ts`.
2092
+ - **`oversized` candidates kind** (doc 20 §1.2 hub-size ADVISORY,
2093
+ record-not-limit): added to the daemon `HEALTH_KINDS` whitelist
2094
+ (`daemon/memory/rpc.ts` → forwards `GET /api/im/memory/health/oversized`),
2095
+ the Python `memory_curate` `kind` enum, and the shared
2096
+ `MemoryCurateInput.kind` spec (`adapters/memory-tools.ts`, which also picks
2097
+ up the previously-drifted `conflicts` value). Advisory only — the cloud
2098
+ lane measures and suggests splits; nothing is machine-enforced.
2099
+
2100
+ ### Changed — memory203/20 W-C: guidance rewrite (four-truths) — memory + memory-dream skills, MEMORY_CORE_DIRECTIVE
2101
+
2102
+ - **Teach only what W-A/W-B landed** (the initiative's lesson: never teach what
2103
+ the platform can't do). All guidance now consistent with
2104
+ `EXTRACTION_SYSTEM_PROMPT` v2 (`daemon/memory/extract.ts`): same rel
2105
+ vocabulary (`supports`/`contradicts`/`related`/`derived-from`/`references`/
2106
+ `cites` + structural `child-of`), same `<prismer-data>` view set
2107
+ (`csv|json` × `table|bar|line|scatter|area|heatmap`), same asset-POINTER
2108
+ form (short description + `<a rel="derived-from" href="prismer://asset/<id>">`,
2109
+ never a body copy), same anti-waste rules (no duplicate extraction, no
2110
+ circular recall — the ONLY token rules, 0705 record-not-limit rulings).
2111
+ - **`built-in-skills/memory/SKILL.md`** (canonical + runtime mirror): dual-
2112
+ section ownership section replaces the self-contradictory "hub needs a body"
2113
+ vs "never hand-edit INDEX/hub" doctrine (doc 19 D11) — `#overview` prose is
2114
+ agent-owned and rebuild-proof (W-A `rewriteTocSection`, test-verified),
2115
+ `#toc` is machine-owned; frontmatter `description` REQUIRED (one-sentence
2116
+ self-summary feeding TOC entries/chips) with the dual-track note (body prose
2117
+ carries inter-entity logic); de-hedged append flow — recall/browse shows an
2118
+ existing page on the topic ⇒ `memory_write op="append-section"` /
2119
+ `"rewrite-section"` is a MUST (the "may not be available / fallback" hedging
2120
+ is gone — the ops are live in daemon rpc + Python tool schema); richness
2121
+ palette with three worked PKF examples (prismer-data page, asset-pointer
2122
+ page incl. `<img src="prismer://asset/…">` media, supports/contradicts pair
2123
+ with `#section` anchors) — media/section-granularity teachings lifted from
2124
+ the deprecated memory-curation skill (doc 19 D12 迁移, that skill stays
2125
+ deprecated); size discipline (INDEX/hub lean as taught discipline, enforced
2126
+ by nothing; `kind="oversized"` split suggestions; ordinary pages unlimited,
2127
+ richness encouraged); consolidated anti-patterns (re-reading raw files
2128
+ memory already distills, circular recall, duplicate extraction, copying
2129
+ asset bodies, hand-editing `#toc`).
2130
+ - **`built-in-skills/memory-dream/SKILL.md`** (v2 → v3, canonical + runtime
2131
+ mirror): convergence loop gains STEP 1c (review `kind="oversized"` hub-split
2132
+ advisories — suggestions, never enforcement) and STEP 6 (MAINTAIN the
2133
+ `#overview` prose on INDEX/hubs via `op="rewrite-section"` — the
2134
+ orchestrator's editorial duty; the rebuild only regenerates `#toc` with
2135
+ per-entry descriptions); candidates envelope documents the `oversized`
2136
+ surface; "rebuild_index is the ONLY INDEX write" reworded to the dual-
2137
+ section truth; section-op hedging removed from the enact/fold steps.
2138
+ - **`MEMORY_CORE_DIRECTIVE`** (`daemon/dispatch.ts`): "NEVER write or
2139
+ hand-edit INDEX.pkf (machine-generated)" replaced with the dual-section
2140
+ truth ("`#toc` machine-owned — never hand-edit; `#overview` prose IS yours
2141
+ to write and maintain"); adds the richness line (description-required +
2142
+ rich blocks + asset pointers + work-from-memory-not-raw-files + anti-waste);
2143
+ extend decisions name the live section ops; orchestrator loop gains
2144
+ overview maintenance + the conflicts/oversized candidates kinds.
2145
+
2146
+ ### Added — release203 web-capability fix: `workspace_web_search`/`web_load` tools + terminal toolset restore
2147
+
2148
+ - **`workspace_web_search` / `web_load` provider tools** (`plugins/memory/prismer/__init__.py`):
2149
+ workspace-CONTEXT tools (not memory ops) registered through the provider
2150
+ shell's `get_tool_schemas()` seam (7 tools total now). Handlers POST the
2151
+ new daemon routes; descriptions steer agents away from scripting HTTP via
2152
+ `execute_code`+subprocess (the observed fallback: 197 execute_code vs 0
2153
+ web calls in 14 days — Hermes' native web toolset is schema-dropped in pods
2154
+ because no upstream search backend is configured, and per the user ruling
2155
+ we ARE the search backend: no third-party keys/packages enter the pod).
2156
+ `web_load` validates http(s)-only shell-side before any daemon call.
2157
+ Name note: the search tool CANNOT be `web_search` — Hermes v0.17 rejects
2158
+ provider tools shadowing reserved CORE tool names even when the core tool
2159
+ is check_fn-dropped ("Core tools always win", live-hit 2026-07-03); the
2160
+ event-stream mapper (`tool-call-mapper.ts`) maps `workspace_web_search`
2161
+ onto the first-class `web_search` search row (and `web_load` onto the
2162
+ `fetch` row) so the timeline surface is unchanged.
2163
+ - **Daemon `/local/web/search` + `/local/web/load` routes** (`daemon/web/rpc.ts`,
2164
+ new `attachWeb` hook in `local-server.ts`, wired in `runner.ts`): FORWARD to
2165
+ the cloud Load API `POST /api/context/load` (`{input: query|url|urls}` —
2166
+ search + cache + compress + deposit, Exa server-side) with the daemon's own
2167
+ `Authorization: Bearer <sk-prismer>` (same credential lane as extract.ts;
2168
+ the Load API's `apiGuard` accepts sk-prismer Bearer directly, no JWT
2169
+ exchange). Forward timeout 120s default (live-measured: cold 3-result query
2170
+ = 59s), env-tunable `PRISMER_WEB_LOAD_TIMEOUT_MS`; the daemon also sends
2171
+ `x-forwarded-proto` matching its cloud baseUrl scheme (the Load API
2172
+ self-fetches `/api/search` at `x-forwarded-proto || https`, which 500s
2173
+ against a plain-HTTP dev/pod cloud). Search passes `search.topK = limit`
2174
+ alongside `return.topK` — the route's default searched set is 15 and it
2175
+ compresses EVERY uncached hit before ranking (59–118s cold for a 3-result
2176
+ ask). Every text field in the passthrough bounded to ~8k chars with an
2177
+ explicit truncation marker; `[web-tool]`-prefixed structured logs (trace
2178
+ id, query/url count, status, ms).
2179
+ - **Explicit `platform_toolsets.api_server` pin** (`adapters/persistence/hermes/index.ts`
2180
+ `configurePrismerProvider`, exported `HERMES_API_SERVER_PLATFORM_TOOLSETS`):
2181
+ bypasses the hermes v0.17.0 subset-inference bug that silently dropped the
2182
+ `terminal` toolset. The pinned list is the `hermes-api-server` composite
2183
+ reverse-mapped to configurable toolset keys minus default-off (verified
2184
+ against the hermes-agent reference `hermes_cli/tools_config.py`); explicit
2185
+ membership flips hermes to deterministic direct resolution. Union-merged
2186
+ with operator entries, idempotent; per-role `agent.disabled_toolsets`
2187
+ (toolsetScope deny) still applies LAST hermes-side, so governance wins.
2188
+ - **`WEB_TOOL_DIRECTIVE`** (`daemon/dispatch.ts`): hermes-only teaching line on
2189
+ the same `composedSystemPrompt` seam as `MEMORY_CORE_DIRECTIVE` ("Web
2190
+ research → `web_search`/`web_load` (workspace-billed, cached). CLI →
2191
+ `terminal`. Do NOT script HTTP or subprocess for these.").
2192
+ - **Frozen spec** `adapters/web-tools.ts` (sibling of `memory-tools.ts`):
2193
+ `WebSearchInput/Output`, `WebLoadInput/Output` + naming rationale
2194
+ (`web_search` matches the event-stream mapper's `SEARCH_TOOLS` so calls
2195
+ render as first-class search rows).
2196
+ - Tests: `test/web-rpc.test.ts` (forward+auth header+bounding+non-http
2197
+ reject+degrade), `test/web-provider-shell.test.ts` (real python3 exec of the
2198
+ shell: 7-tool registration + shell-side invalid_url negatives),
2199
+ `test/hermes-platform-toolsets.test.ts` (pin content, operator merge,
2200
+ idempotency, deny coexistence).
2201
+
2202
+ ### Added — memory203/18 §11.4 residual #3: conflict-lifecycle review surface (`kind=conflicts`)
2203
+
2204
+ - **`GET /local/memory/health?kind=conflicts`** (`daemon/memory/rpc.ts`
2205
+ `HEALTH_KINDS`): passthrough to the new cloud
2206
+ `GET /api/im/memory/health/conflicts` — live remote-conflict pages, each
2207
+ carrying `metadata.latestTwoVersionSummaries` (`{version, changeSummary,
2208
+ authoredBy, createdAt}` from the version DAG) + `metadata.currentVersion`,
2209
+ so the orchestrator can judge which LWW side owns the head without loading
2210
+ full contents. `kind=all` now returns four surfaces
2211
+ (`{orphans, duplicates, stale, conflicts}`).
2212
+ - **`memory_curate` python tool** (`plugins/memory/prismer/__init__.py`):
2213
+ `candidates` `kind` enum gains `"conflicts"`. Cloud-side, conflict is now a
2214
+ STATE, not a brand — a curation touch or clean (non-conflicting) rewrite of
2215
+ a conflict-flagged page restores its `sourceKind` to `daemon-sync`, so
2216
+ reviewed pages drop off this surface; the `memory-dream` skill's convergence
2217
+ loop gained a STEP 1b teaching the review flow.
2218
+
2219
+ ### Fixed — memory203/18 W5 (final-round5 P0): two-phase stall watchdog + silent-empty-reply guard
2220
+
2221
+ - **Two-phase upstream-stall watchdog** (`adapters/persistence/hermes/sessions-sse.ts`):
2222
+ the W4 single 120s threshold killed HEALTHY long-context turns — after
2223
+ round-3 bulk ingestion the recall probes carried ~120k promptTokens and the
2224
+ upstream legitimately took >120s to the FIRST token (3/3 probes
2225
+ watchdog-aborted; the 15s dispatch heartbeats already protected those turns
2226
+ from the 300s reaper). Now the guard runs two phases: before the first
2227
+ MODEL-activity event it uses `PRISMER_UPSTREAM_FIRST_EVENT_MS` (default
2228
+ 270 000 ms — under the 300s reaper, generous for long-context first tokens);
2229
+ after it, the original inter-event `PRISMER_UPSTREAM_STALL_MS` (default
2230
+ 120 000 ms). Both env-tunable, resolved per call. ⚠️ hermes enqueues
2231
+ `run.started`/`message.started` BEFORE the LLM call (api_server.py
2232
+ `_run_and_signal`), so those acks re-arm but do NOT end the first-event
2233
+ phase — gating on the literally-first frame would have made the fix inert.
2234
+ Abort messages name the phase (`no first event for Ns` vs `no events for
2235
+ Ns`); `dispatch.ts` `isLimiterClassError`/`retryReasonToken` match both
2236
+ wordings (requeue lane + `reason=stall` unchanged). Clarify-disarm and
2237
+ keepalive-comment semantics unchanged. Covers both hermes lanes (shared
2238
+ `consumeSessionsSse`).
2239
+ - **Silent-empty-reply guard + transcript-tail recovery**
2240
+ (`adapters/persistence/hermes/sessions-dispatcher.ts`): after a stall-abort,
2241
+ hermes keeps generating server-side (`_run_agent` runs `run_conversation` in
2242
+ a thread executor the disconnect-cancel cannot preempt) and flushes the turn
2243
+ into the session store on completion; the daemon retry re-sent the same
2244
+ prompt into the SAME session and the retried stream could complete with
2245
+ EMPTY content — the adapter forwarded ok=true output:'' → cloud marked the
2246
+ run completed and the DM message-post gate (`output || attachments`)
2247
+ silently skipped the post (run=completed, error=NULL, DM empty, credits
2248
+ burned; the killed attempt's answer later leaked into the next turn's
2249
+ reply). Now an empty-output completion (approval/clarify suspensions
2250
+ excluded) is never a silent success: the dispatcher first tries to RECOVER
2251
+ the reply from `GET /api/sessions/{id}/messages` (last non-empty assistant
2252
+ row after the first user row carrying this dispatch's id — the id is
2253
+ stamped in the `<execution_context>` XML; 2 bounded polls), surfacing it
2254
+ with `metadata.hermes.replyRecovered='session_transcript_tail'`; otherwise
2255
+ it fails LOUDLY with `empty_reply`, which `daemon/dispatch.ts` treats as
2256
+ terminal (no 3× re-burn of a ~120k-token prompt into the polluted session)
2257
+ so the cloud posts a visible "Agent failed" system event.
2258
+ - **Best-effort upstream cancel on stall-abort** (same file): fires
2259
+ `POST /v1/runs/{runId}/stop` fire-and-forget before the retry. Known
2260
+ ineffective on current hermes — the sessions chat/stream handler never
2261
+ registers its run in `_active_run_agents` (only the /v1/runs path does), so
2262
+ this 404s today; kept as the correct semantic signal for the day hermes
2263
+ registers sessions runs. A clean per-turn abort therefore CANNOT be done
2264
+ from the adapter yet — the guard above is the real mitigation.
2265
+
2266
+ ### Changed — memory203/18 W4 runtime tail: upstream-stall watchdog + CLI gateway-detection fix
2267
+
2268
+ - **In-flight upstream-stall watchdog** (`adapters/persistence/hermes/sessions-sse.ts`
2269
+ `createStallGuard` + `resolveUpstreamStallMs`): the W2-gate's dominant residual
2270
+ failure mode was a SILENT upstream LLM hang — no error, no SSE events — so the
2271
+ adapter emitted no progress, the 300s daemon reaper killed the run
2272
+ (`daemon_task_timeout`), the cloud requeued, the same stall repeated until the
2273
+ requeue cap (4) exhausted → 3/18 permanent burst failures + starved recall
2274
+ probes. The W3 retry-loop progress frames couldn't cover this: the call never
2275
+ RETURNED to enter the retry loop. Now, when an in-flight sessions SSE stream
2276
+ produces NO event frames for `PRISMER_UPSTREAM_STALL_MS` (default 120 000 ms,
2277
+ env-tunable, read per-call), the guard cancels the stream and throws
2278
+ `upstream stall: no events for Ns (…)` — a plain (non-AbortError) error that
2279
+ `categorizeDispatchError` maps to a retryable `adapter_dispatch_failed`.
2280
+ Covers BOTH hermes lanes (sessions-dispatcher + runs-dispatcher share
2281
+ `consumeSessionsSse`). The reaper window is untouched (last-resort backstop);
2282
+ `clarify.request` disarms the guard while a human answers (re-armed by the
2283
+ next event frame).
2284
+ - **Stall-class rides the limiter/requeue lane** (`daemon/dispatch.ts`):
2285
+ `isLimiterClassError` now matches `upstream stall: no events for` — the retry
2286
+ loop uses the jittered limiter backoff with `retrying(attempt=N, reason=stall)`
2287
+ progress frames (new `retryReasonToken` token), and on exhaustion the failure
2288
+ lands `dispatch_precondition_unavailable` so the cloud's transient-requeue
2289
+ channel re-delivers instead of a permanent red failure. Generic `HTTP 504`
2290
+ wording remains terminal (unchanged).
2291
+ - **`prismer memory list` gateway-detection fix** (`cli/commands/memory.ts`):
2292
+ `list` never fell back to `$PRISMER_WORKSPACE_ID` (unlike search/read/recall/
2293
+ write), so a bare `prismer memory list` in an agent pod skipped the modern
2294
+ `/local/memory/list` route entirely, probed only the legacy 404 paths, and
2295
+ reported `memory_gateway_unavailable` against a live daemon RPC (W2-gate
2296
+ item 3 FAIL, R7 CLI projection). Now `list` honours the env workspace;
2297
+ explicit `--workspace-id` still wins. Verified inside the live agent-rt pod
2298
+ (patched CLI bundle, direct node invocation): `memory list --page-type hub`
2299
+ returns the workspace hubs with exit 0.
2300
+ Known residual: `memory delete` / `memory sync` still only probe legacy
2301
+ routes (no modern `/local/memory/*` equivalents wired) — unchanged.
2302
+
2303
+ ### Changed — memory203/18 W3 lane: failure self-heal (R3.1–R3.3) + event-stream fidelity (R5.2)
2304
+
2305
+ - **R3.1 — limiter-exhaustion now self-heals instead of terminal-failing**
2306
+ (`daemon/dispatch.ts`): when the local retry loop (3 attempts) exhausts and the
2307
+ LAST error is limiter-class (HTTP 429 queue-full / RPM "Rate limit exceeded",
2308
+ 504 "slot not available before deadline"), the synthesised failure carries
2309
+ `dispatch_precondition_unavailable` (original message preserved) instead of
2310
+ `daemon_local_retry_exhausted` — the cloud's EXISTING transient-requeue channel
2311
+ (`requeueTransientRun`, cap 4) re-delivers the run once the queue drains.
2312
+ Non-limiter exhaustion (e.g. HTTP 500 ×3) still lands
2313
+ `daemon_local_retry_exhausted` terminal. Cloud side (same change set,
2314
+ `src/im/ws/handler.ts` + `task.service.ts::isTransientRequeueErrorCode`): the
2315
+ requeue branch also accepts reaper `daemon_task_timeout` for CHAT runs
2316
+ (`run.source !== 'task'`), mirroring the kanban-task retry policy that chat
2317
+ runs never had — same bounded counter.
2318
+ - **R3.2 — limiter-aware backoff** (`daemon/dispatch.ts`): limiter-class retry
2319
+ waits honor an explicit Retry-After hint in the message (`Retry-After: n` /
2320
+ `Retry in ns`, clamped 60s) and otherwise use jittered windows [3–6s, 8–15s]
2321
+ instead of the fixed [1s, 3s] that burnt all 3 attempts in ~4s. Attempts stay
2322
+ at 3 (the requeue channel owns the long tail). `PRISMER_DISPATCH_RETRY_BACKOFF_MS`
2323
+ (comma list) overrides all classes for ops/testing.
2324
+ - **R3.3 — progress during retry** (`daemon/dispatch.ts`): before each backoff
2325
+ the dispatcher emits `task.dispatch.progress` with
2326
+ `retrying(attempt=N, reason=<status>)` (+ structured `detail.kind='retry'`),
2327
+ resetting the runner reaper's inactivity window so the 300s reaper no longer
2328
+ kills runs that are mid-retry, and giving the UI something to show.
2329
+ - **R5.2 — event-stream CLI extraction** (`adapters/persistence/hermes/tool-call-mapper.ts`):
2330
+ `execute_code` wrapping a CLI in python (`subprocess.run(['prismer',…])`,
2331
+ `subprocess.*` string form, `os.system(…)`, `!cmd` lines) now maps
2332
+ `detail.command` to the reconstructed CLI (multiple calls join with ` && `,
2333
+ capped 400 chars) with `detail.commandSource='argv'`; the FULL python source
2334
+ rides additively on `detail.script` (capped/spilled by the step recorder like
2335
+ `output`). Dynamic argv (variables/f-string elements) is never guessed — no
2336
+ match keeps the previous whole-source behaviour. The workspace timeline's
2337
+ collapsed row now renders `` $ prismer memory search … `` instead of
2338
+ `import subprocess (+9 行)` (`src/app/workspace/components/agent-message/adapter.ts`).
2339
+ - Additive type fields: `ToolCallDetail.shell.{script,commandSource}`
2340
+ (`adapters/coding/shared/agent-sdk-types.ts` + UI mirror in
2341
+ `agent-message/types.ts`); step recorder caps `shell.script` like `shell.output`.
2342
+
2343
+ ### Changed — memory203/18 W2 guidance lane: teach the primitives that now exist (§2.3/R6.5/R4.1/R4.2)
2344
+
2345
+ - **`built-in-skills/memory/SKILL.md` rewritten tool-centric** (native `memory_*` tools primary;
2346
+ short `prismer memory` CLI appendix for code agents): browse-first write flow
2347
+ (`memory_browse` → extend / attach via `parent_hub_path` / new-hub-then-leaf → verify via
2348
+ `memory_search`), prefix-free path convention ("use paths exactly as returned by
2349
+ browse/search"), structural edges over prose (`child-of` direction taught explicitly: edge
2350
+ points FROM child TO hub — the W1-gate found 8 hand-written inverted links), INDEX.pkf is
2351
+ machine-generated and never hand-written, never whole-page-rewrite another agent's page.
2352
+ Section ops (`op="append-section"` / `"rewrite-section"`) are taught with an explicit
2353
+ landing note — they ship in the same daemon image (R6.4, parallel lane).
2354
+ - **`built-in-skills/memory-dream/SKILL.md` rewritten around the tight convergence loop**:
2355
+ `candidates` → `memory_browse` → cluster (your LLM) → `promote_to_hub` **WITH `childPaths[]`**
2356
+ (no hollow promotes, no hand-written re-anchor links — the old "no move-anchor op" §Re-anchor
2357
+ workaround is deleted) → `rebuild_index` once per batch → `candidates` again to verify →
2358
+ REPORT the structural delta in the reply. (W1-gate: CEO ran promote without childPaths because
2359
+ the param was undocumented, and hand-wrote 8 inverted child-of hrefs.)
2360
+ - **`built-in-skills/memory-curation/SKILL.md`** (deprecated/reference): placement guidance
2361
+ aligned to browse-first + `parent_hub_path`; removed "anti-orphan is discipline only, the
2362
+ platform won't enforce it" apologetics and the `memory/`-prefixed path convention.
2363
+ - **`MEMORY_CORE_DIRECTIVE` updated** (`daemon/dispatch.ts`): names all five tools (adds
2364
+ `memory_browse`), hardened RECALL rule ("when answering anything about workspace knowledge you
2365
+ did not just read, run `memory_search` FIRST; attached files are raw sources — check memory for
2366
+ distilled knowledge before re-reading them"), browse-first write flow, and the orchestrator
2367
+ convergence loop with `childPaths` + verify + report duty.
2368
+ - **R4.2 — builtin `memory` skill now installs into every hermes profile**
2369
+ (`adapters/persistence/hermes/index.ts`): `installPrismerImSkill` generalized to
2370
+ `installBuiltInHermesSkill(profileName, slug)` (per-slug content cache); both install sites
2371
+ (ensureService + per-dispatch) land `prismer-im-collab` + `memory`, and `memory-dream` for
2372
+ profiles with `taskAuthority: 'orchestrator'` (first consumer of that config bit).
2373
+ - **R4.1 — orchestrator prompt no longer routes file questions straight to raw sources**
2374
+ (`src/lib/role-runtime-policy/prompt-contract.ts`, cloud side): "For uploaded files, first
2375
+ `memory_search` for existing distilled knowledge; use `assets`/`ingest` when memory misses."
2376
+ - **R4.3 — `<execution_context>` dirs annotated** (`daemon/conversation-context.ts`): a
2377
+ `<dirs_note>` leaf marks artifacts/scratch dirs as per-run working space, pointing prior-fact
2378
+ questions at `memory_search` (the W1-gate read-file-over-recall bypass).
2379
+
2380
+ ### Added — memory203/18 W2 daemon lane: path/URI normalization + section write + placement guard + 0-yield root fix
2381
+
2382
+ - **P0 path/URI namespace normalization** (`daemon/memory/store.ts` → `rpc.ts handleWrite`,
2383
+ `hook-server.ts writeExtractedPage`, `cloud-sync.ts fetchPageFromCloudByPath`): ONE shared
2384
+ normalizer — `normalizeMemoryPath` (strips leading `/` + `memory/` segments) +
2385
+ `memoryPathToUri(workspaceId, path)` — used by every link/URI composition site, so a page path
2386
+ (or `parentHubPath`) that already starts with `memory/` no longer produces
2387
+ `prismer://…/memory/memory/…` (the W1-gate: 3/3 agent child-of `link.upsert` events silently
2388
+ dropped cloud-side, 0 rows). `memory.link.upsert` envelopes additionally carry plain
2389
+ `sourcePath` / `targetPath` (normalized; OPTIONAL + additive on `envelope.ts`, no schemaVersion
2390
+ bump) — the cloud lane prefers these over URI parsing, and trace path-mode matches on them.
2391
+ Local store lookups are variant-tolerant via `store.loadByAnyPath`
2392
+ (`memoryPathLookupVariants`: as-given, ±`memory/`, ±`.pkf`); a repeat write under the OTHER
2393
+ path convention extends the EXISTING page (existing row's path wins) instead of forking a
2394
+ near-duplicate.
2395
+ - **R1.4 write-time 回源 (write hygiene)** (`rpc.ts handleWrite` op=replace, `cloud-sync.ts`,
2396
+ `store.ts`, `types.ts`): when the target page is absent locally or still `local-only`, the
2397
+ daemon first pulls the cloud head via the SAME single-page回源 `handleLoad` uses
2398
+ (`fetchPageFromCloudByPath`); `materialisePage` now passes the cloud `version` and
2399
+ `store.write` adopts `max(localExisting+1, version)` — so the write's outbox `parentVersion`
2400
+ continues the cloud head (cloud v4 + empty subset → parentVersion=4, not 0; dissolves the
2401
+ W1-gate's cross-agent full-page-rewrite `remote-conflict base v4 vs head v1` class). Fail-open:
2402
+ offline → proceed exactly as before (a local-first write never blocks on the cloud).
2403
+ - **R6.4 section-level write** (`rpc.ts handleWrite`/`handleSectionWrite`, `__init__.py`,
2404
+ `adapters/memory-tools.ts`, `cli/commands/memory.ts --op/--section`): `memory_write` gains
2405
+ `op` (`replace`|`append-section`|`rewrite-section`, default replace) + `section` (heading id,
2406
+ required for section ops). Section ops FORWARD to the cloud section verbs
2407
+ (`POST /api/im/memory/pages/:id/sections/append|rewrite`, actor-override header like curate),
2408
+ then refresh the local subset from the cloud response (adopting the cloud version,
2409
+ syncStatus→`acked`) — and deliberately emit **NO** `memory.page.upsert` outbox event (the
2410
+ cloud write is authoritative; an upsert on top would double-apply the edit). No cloud wired →
2411
+ explicit 503 `section_write_requires_cloud` (never a silent no-op); unreachable cloud → 5xx
2412
+ passthrough with the local page untouched.
2413
+ - **R6.3 placement guard (ramp, gated LAST per §9.3)** (`rpc.ts handleWrite`): a NEW un-anchored
2414
+ leaf (no existing page at path, not `pageType=hub`, no `parentHubPath`, no `rel="child-of"`
2415
+ content link) is gated by `PRISMER_MEMORY_PLACEMENT_ENFORCE` = `off` | `warn` (DEFAULT) |
2416
+ `enforce`. warn → `[memory-trace] stage=placement_warn` + `placementWarn` counter, write
2417
+ proceeds; enforce → 422 `{code:'placement_required', hubs:[{path,title,snippet}], message}`
2418
+ (hub candidates from the SAME `assemblePlaceContext` browse assembly). The Python tool's
2419
+ memory_write POST is now status-preserving so the structured rejection reaches the model
2420
+ verbatim (was: swallowed into `daemon_unreachable`).
2421
+ - **Extraction 0-yield root fix + terminal states** (`extract.ts`, `hook-server.ts`): live-
2422
+ diagnosed 2026-07-03 on the W1-gate pod (manual post_llm_call + direct gateway replay of the
2423
+ real gate turn): kimi's multi-page JSON stochastically exceeds `max_tokens=2048`
2424
+ (`stop_reason=max_tokens`, observed 996/1924/2048-cut across 3 identical trials), and the
2425
+ truncated payload JSON.parse-failed into a SILENT `[]` — the gate's
2426
+ `llm_called 3/3 → extracted 0/3`. Fix: (a) string-aware salvage parser recovers every
2427
+ COMPLETE page object from a truncated `{"pages":[…` array (the REAL truncated gate-shape
2428
+ payload now yields 3 pages instead of 0); (b) `stop_reason` surfaced —
2429
+ `stage=llm_truncated(max_tokens)` + `truncated` flag on the result; (c) terminal stages
2430
+ `stage=llm_response(pages=N, chars=M[, truncated])` and `stage=skipped(reason=no_pages)` +
2431
+ `extractedEmpty` counter close the llm_called-then-silence gap; (d) a BUDGET line in the
2432
+ extraction prompt biases toward fewer, denser pages. `/healthz` `memory.counters` is now
2433
+ `{received, skipped, extracted, writeFailed, extractedEmpty, placementWarn}`.
2434
+ - **R9.2 extraction deferred retry (doc 18 §9.4)** (`hook-server.ts`, `extract.ts errorStatus`):
2435
+ under burst the in-pod extraction call is starved by the workspace concurrency limiter
2436
+ (429 / 504 slot-deadline / status=0 cloud_unreachable) and the turn's knowledge — from exactly
2437
+ the busiest ingestion window — was dropped forever. Limiter-class failures now land on a
2438
+ bounded in-memory deferred queue (max 32, drop-oldest + `stage=deferred_dropped`); a lazy,
2439
+ jittered ~60s pump (`MEMORY_EXTRACT_RETRY_INTERVAL_MS`, unref'd, self-stopping) drains ONE
2440
+ entry per tick (serial — deliberately gentle on the limiter), max 3 total attempts, then
2441
+ `stage=extraction_abandoned` + counter. A successful retry runs the normal written/synced path
2442
+ with the ORIGINAL traceId (the trace shows the gap AND the recovery); each retry rebuilds a
2443
+ FRESH place-context. Non-limiter failures keep the writeFailed path (never queued). Counters:
2444
+ `memory.counters` gains `{deferred, deferredRetried, deferredAbandoned}` (deferred counts
2445
+ entries, not tries). Stage logs: `stage=deferred(reason=limiter, attempt=N)` /
2446
+ `deferred_retry(attempt=N)`. KNOWN BOUND (deliberate): the queue is in-memory only — a pod
2447
+ restart loses pending entries (no persistence by design; this module has no daemon shutdown
2448
+ hook to log the residual size).
2449
+ - Tests: `test/memory-write-w2.test.ts` (20, negative controls for every mode),
2450
+ `test/memory-extract-salvage.test.ts` (4, incl. the real-payload shape),
2451
+ `test/memory-extract-deferred.test.ts` (9: defer on 429/504/0, non-limiter + 0-page negative
2452
+ controls, traceId-preserving recovery, 3-attempt abandonment, hard-failure-on-retry, 32-cap
2453
+ drop-oldest), `test/memory-stage-counters.test.ts` (+1 extractedEmpty terminal state,
2454
+ counters shape ×9).
2455
+
2456
+ ### Added — memory203/18 W0+W1 daemon lane: write-time placement + browse + trace observability
2457
+
2458
+ - **R1.1 `memory_write` structural placement** (`plugins/memory/prismer/__init__.py`,
2459
+ `daemon/memory/rpc.ts handleWrite`): optional `parent_hub_path` + `relation`
2460
+ (`child-of`|`related`, default child-of) on the tool schema, forwarded as
2461
+ `parentHubPath`/`relation` to `POST /local/memory/write`. When present the daemon enqueues a
2462
+ `memory.link.upsert` GRAPH event (page→hub) mirroring the auto-extract leg — the edge that
2463
+ actually nests the page cloud-side. A locally-missing hub still emits (warn-not-reject; the
2464
+ fail-closed guardrail is R6.3/W2, gated on browse being live). Response additively echoes
2465
+ `link: { targetPath, relation }`.
2466
+ - **R1.3 `memory_curate` promote_to_hub `childPaths[]`** (`__init__.py`, `rpc.ts handleCurate`,
2467
+ `adapters/memory-tools.ts`): pass-through to the cloud POST body so the promoted hub can attach
2468
+ children in the same transaction (cloud lane consumes; older clouds ignore the extra field).
2469
+ - **R6.1 hub snippet wiring** (`daemon/memory/hook-server.ts`): the extraction recall context now
2470
+ carries WHAT each hub is about — `description`, else first ~200 chars of content, else `''`.
2471
+ (The old call site mapped hubs without a snippet, so the extract model was blind to hub topics
2472
+ and rationally fell back to `placement=new` → orphan leaves.)
2473
+ - **R6.2 `memory_browse` / `GET /local/memory/place-context`** (`rpc.ts`, `__init__.py`,
2474
+ `adapters/memory-tools.ts`): write-time structure view `{ index, hubs[], nearest[] }` (each
2475
+ `{path,title,pageType,snippet}`), assembled by the SHARED `assemblePlaceContext` helper the
2476
+ extraction leg uses — browse and extraction cannot drift. `q=` adds nearest pages via the same
2477
+ local hybrid search; F5 cap predicate applies (private pages hidden). `memory_load` additively
2478
+ returns the page's outbound `links` (best-effort cloud `GET /pages/:id/links`; omitted offline).
2479
+ FROZEN spec (`adapters/memory-tools.ts`) now lists `memory_write` + `memory_browse` (R6.5 partial).
2480
+ - **R8.1 traceId 贯穿** (`__init__.py` → `hook-server.ts` → `extract.ts` → `envelope.ts` → `rpc.ts`):
2481
+ `sync_turn` stamps `extra.trace_id` (session id + random suffix); the daemon threads it through
2482
+ extraction into the `memory.page.upsert` / `memory.link.upsert` outbox envelopes (optional
2483
+ `traceId` added to `MemoryEventCommon` — additive, no schemaVersion bump). `handleWrite` accepts
2484
+ an optional `traceId` (else mints `wr_<10hex>`).
2485
+ - **R8.2 stage counters + standardized stage logs** (`hook-server.ts`, `extract.ts`,
2486
+ `daemon/local-server.ts`): module-level `{received, skipped, extracted, writeFailed}` counters
2487
+ surfaced on `/healthz` as `memory.counters`; every pipeline stage logs one grep-able line —
2488
+ `[memory-trace] stage=received|skipped(reason=…)|llm_called|llm_failed(status=…)|written(paths=…)|synced traceId=…`.
2489
+ - **R9.1 sync_turn delivery hardening** (`__init__.py`): the bare `except: pass` around the
2490
+ post_llm_call POST is replaced with at-least-once delivery — one retry after ~1s; terminal
2491
+ outcome always lands as ONE structured stderr line (`[memory-trace] sync_turn delivered …` /
2492
+ `… delivery FAILED err=<class>: <msg>`). Still fire-and-forget: never raises into the turn.
2493
+ - Tests: `test/memory-place-context.test.ts` (7), `test/memory-write-placement.test.ts` (8),
2494
+ `test/memory-stage-counters.test.ts` (5) — each new behavior with a negative control.
2495
+
2496
+ ### Fixed — memory extraction dropped under real docs (generation outran the 30s client timeout)
2497
+
2498
+ - **Why:** test-verified (2026-07-02, memory203 systematic doc-ingest) — a full-doc extraction
2499
+ prompt with `max_tokens: 4096` on `us-kimi-k2.6` takes **29–44s** to generate. The daemon's
2500
+ extraction gateway call was bounded at **30s**, so under real documents the call aborted
2501
+ mid-flight (`status=0 cloud_unreachable "Request aborted/timeout"`), the 2-retry budget was
2502
+ exhausted, and the memory was silently dropped — 6 docs yielded ~1 page, 0 hubs. (The cloud
2503
+ proxy completed the work at ~29s, but the daemon had already given up.) A control run with the
2504
+ workspace concurrency limiter OFF reproduced identically → the limiter was **exonerated**; the
2505
+ cause was purely extraction generation-time vs client-timeout.
2506
+ - **What changed** (`daemon/memory/extract.ts`): `max_tokens` 4096 → **2048** (fits 6 concise
2507
+ pages at ~300 tok each while keeping generation well under the bound) and
2508
+ `GATEWAY_TIMEOUT_MS` 30s → **60s** (margin over the measured worst case). Both are now
2509
+ env-overridable — `MEMORY_EXTRACT_MAX_TOKENS`, `MEMORY_EXTRACT_TIMEOUT_MS` — so ops can tune
2510
+ without a daemon rebuild.
2511
+
2512
+ ### Changed — memory skills/seeds repointed from `cloud memory` → `prismer memory` (post-2822f36f CLI separation)
2513
+
2514
+ - **Why:** commit `2822f36f` dropped the erroneous `cloud` bin from @prismer/runtime (it
2515
+ collided with @prismer/sdk's `cloud` bin → EEXIST on global install). The two CLIs are now
2516
+ cleanly separated: `cloud` = @prismer/sdk (cloud-side HTTP, incl. the memory203/13 decision-D
2517
+ **retired** memory HTTP impl), `prismer` = @prismer/runtime (this canonical daemon CLI). In
2518
+ the agent-rt image both tarballs install, so in-pod `cloud memory` would now resolve to the
2519
+ RETIRED sdk HTTP path — the daemon-backed memory CLI is `prismer memory`.
2520
+ - **What changed:** repointed every agent-facing `cloud memory …` reference to `prismer memory …`
2521
+ across the memory built-in skills, seeds, and adapter/CLI docs:
2522
+ `built-in-skills/memory/SKILL.md`, `built-in-skills/memory-dream/SKILL.md`,
2523
+ `src/im/data/memory-seed/{index.ts,sdk-intro.md}` (memory/recall moved to the `prismer` family;
2524
+ task/send/asset/load/parse/deliver/okr/pay stay `cloud`), the claude-code/codex
2525
+ `adapters/coding/*/memory-tools.ts` doc comments, this package's `cli/commands/memory.ts`
2526
+ header, and the desktop copy `apps/desktop/resources/runtime/built-in-skills/memory/SKILL.md`
2527
+ (whose stale `cloud memory extract/consolidate/compact` were aligned to the canonical set —
2528
+ `extract` → direct `prismer memory write` + automatic background review; `consolidate` →
2529
+ `prismer memory curate`; `compact` dropped, no canonical equivalent). `prismer memory` covers
2530
+ recall/search/read/list/write/delete/curate. No behavior change to the daemon `/local/memory/*`
2531
+ RPC; this is a CLI-name correction so in-pod agents hit the daemon, not the retired sdk HTTP.
2532
+
2533
+ ### Clarified — code-agent memory path is the `prismer memory` CLI, NOT a native tool surface (memory203/09 §① gap G, resolved to §10 decision)
2534
+
2535
+ - **Context:** doc memory203/09 §① flagged 🔴 "CC/codex adapter 工具未接 dispatch
2536
+ (`memory-tools.ts:10`)" — i.e. the per-adapter memory tool schemas were never wired into
2537
+ the running code agent. Investigation shows the gap is **narrower than the 🔴 implies and
2538
+ already resolved by design**: doc memory203/10 §56 decides code agents
2539
+ (claude-code/codex/opencode) have no Hermes-style programmatic provider seam (they run as
2540
+ filesystem CLI subprocesses), so their memory path is the **`prismer memory` CLI + the
2541
+ `memory` built-in skill**, which hits the SAME daemon `/local/memory/*` RPC as the Hermes
2542
+ tool impls. That CLI is fully implemented (`src/cli/commands/memory.ts`:
2543
+ recall/read/write/list/curate, registered in `cli/index.ts`) and the `memory` skill
2544
+ (`scope: common`) is seeded into every coding workdir's `.claude/skills/` via
2545
+ `CODING_COMMON_ALLOWLIST` (`adapters/coding/shared/coding-skill-set.ts`), with the
2546
+ AGENTS.md/CLAUDE.md preamble pointing the agent at `prismer memory`. So recall/read/write/
2547
+ curate are available to code agents out of the box — through the CLI, not a tool schema.
2548
+ - **What changed:** corrected the now-misleading "adapter integration owner work / NOT shipped
2549
+ in phase-0" header comments in `adapters/coding/claude-code/memory-tools.ts` and
2550
+ `adapters/coding/codex/memory-tools.ts` to record the §56 decision (CLI is the path; the
2551
+ native tool surface is intentionally Hermes-only; these schemas are a consumer-less FORMAT
2552
+ FREEZER kept for parity / a possible future programmatic seam — do NOT force-wire them).
2553
+ - **No behavior change** — comment-only; the runtime memory path for code agents already works
2554
+ via the CLI + skill.
2555
+
2556
+ ### Added — Dream CONVERGENCE: orchestrator can now READ candidates + a concrete convergence oneshot (memory203/13 §P4 + §0.5)
2557
+
2558
+ - **Why:** the write path produces well-placed anchored leaves, but at SCALE the wiki
2559
+ degrades into a **FLAT STAR** (`INDEX → many leaves directly`), not a navigable
2560
+ hierarchy. The curation WRITE verbs (promote-to-hub / supersede / rebuild-index)
2561
+ existed, but the orchestrator had **no way to READ what to converge** — orphan leaves,
2562
+ near-duplicate clusters, stale pages all lived cloud-side with no daemon passthrough.
2563
+ The "brain" (orchestrator's own LLM) had hands but no eyes; it could not DECIDE clusters.
2564
+ - **Daemon READ passthrough (`daemon/memory/rpc.ts`):** new `GET /local/memory/health?workspaceId=&kind=&limit=`
2565
+ forwards to the cloud candidate surfaces `GET /api/im/memory/health/{orphans|duplicates|stale}`
2566
+ and returns `{ ok, candidates: { orphans, duplicates, stale } }` (each `{ items, total }`).
2567
+ `kind=all` (default) fetches all three. Same passthrough shape as `handleCurate` — ws cap
2568
+ gated daemon-side, ACL gated cloud-side, **ZERO cloud LLM** (it only scans the page graph).
2569
+ Offline (`cloud` unwired) → 200 degraded empty, honouring the same "降级不中断" contract.
2570
+ Forwards the verified acting agent in `X-Prismer-Memory-Actor` so the workspace ACL
2571
+ projection is correct.
2572
+ - **CLI (`cli/commands/memory.ts`):** new `cloud memory curate candidates [--kind orphans|duplicates|stale|all] [--limit N]`
2573
+ — the READ half. GET to the daemon health route. Read-only (any agent), unlike the three
2574
+ orchestrator-gated write verbs.
2575
+ - **Tool (`adapters/memory-tools.ts`):** `memory_curate` gains a `candidates` op (additive to
2576
+ the existing `promote_to_hub` / `supersede` / `rebuild_index`) with `kind` + `limit` inputs;
2577
+ it branches to the daemon's GET health route and returns the `candidates` payload. Read-only,
2578
+ so it is offered to every agent (the LLM clusters; the cloud does not).
2579
+ - **`built-in-skills/memory-dream/SKILL.md`:** new **"## Convergence flow (oneshot)"** — STEP 1
2580
+ READ candidates → STEP 2 DECIDE clusters (the agent's LLM) → STEP 3 per-cluster `promote-to-hub`
2581
+ a representative + re-anchor members under it (typed `<a rel="parent" prismer://…>` write) →
2582
+ STEP 4 `rebuild-index` so INDEX becomes a TOC of hubs. One worked example (8 scattered
2583
+ `project/helios-*` orphan leaves → `project/helios` hub → re-anchor → rebuild). Fixed the
2584
+ stale `cloud memory curate run-dream` reference (that retired cloud-LLM command no longer
2585
+ exists — replaced by `candidates` + the write verbs).
2586
+ - **Re-anchor note (flagged, not built here):** there is no single "move-anchor" verb. Re-anchor
2587
+ is `promote-to-hub` (mints the hub) + a `cloud memory write` adding the member→hub `rel="parent"`
2588
+ link; `rebuild-index` then drops hub-parented leaves out of the top-level INDEX automatically.
2589
+ This reuses existing cloud endpoints; no new cloud verb requested.
2590
+
2591
+ ### Changed — `memory` skill: add a concrete oneshot WRITE flow so agents actually persist PKF
2592
+
2593
+ - **Why:** a live agent ran a turn but wrote NO memory page — the skill's write guidance was
2594
+ too abstract. The corrected, baked-in flow is **construct-PKF-first, then find where to
2595
+ store it**: (1) CONSTRUCT the full PKF page from the conversation, (2) PLACE by consulting
2596
+ the index (`cloud memory read INDEX.pkf` + `cloud memory recall`) and deciding extend /
2597
+ attach-under-hub / new-semantic-path (never an orphan leaf), (3) WRITE to the decided path.
2598
+ - `built-in-skills/memory/SKILL.md`: new **"## Write flow (oneshot)"** section with a single
2599
+ copy-pasteable end-to-end example (conversation excerpt → constructed `decision` PKF →
2600
+ index-consultation commands + placement reasoning → `cloud memory write`). Reconciled the
2601
+ Operating Rules → Write list so reading `INDEX.pkf` is a WRITE step, not only a Read step.
2602
+ PKF format rules are referenced, not duplicated. The example PKF validates clean against
2603
+ `src/lib/pkf` (frontmatter + `<h2 id>` sections + typed `prismer://workspace` link).
2604
+ - New standalone reference `scripts/cookbook/memory-write-oneshot.md` (the canonical example
2605
+ the skill points to).
2606
+
2607
+ ### Added — DAEMON-SIDE automatic memory extraction (the "自动" leg) + remove cloud-Dream from curate (memory203/13 §0.5 + line 101, Fix Batch B)
2608
+
2609
+ - **Why:** automatic extraction was DEAD. Hermes's native `background_review`
2610
+ spawns with `skip_memory=True`, so our `memory_write` provider tool is never
2611
+ injected into the review fork — whatever it "remembers" lands in Hermes's
2612
+ built-in MEMORY.md, never our cloud PKF wiki. So the "自动" leg is now
2613
+ implemented OURSELVES, in the agent runtime (the daemon in the agent's pod).
2614
+ - **In-pod gateway extraction (`extract.ts` rewritten):** instead of forwarding
2615
+ to the retired cloud `/api/im/memory/extract` (410), `extractFromTurn` now calls
2616
+ the LLM gateway DIRECTLY from the daemon — `${cloud.baseUrl}/api/v1/messages`
2617
+ (Anthropic wire) authed with `Bearer ${cloud.apiKey}`, the SAME agent-credentialed
2618
+ gateway path the code-agent providers use (`PRISMER_BASE_URL` + sk-prismer token,
2619
+ injected for the hosted agent at runner.ts:325-329). The INITIATOR is the agent
2620
+ runtime; the cloud only proxies. **NO cloud LLM call remains.** The prompt carries
2621
+ a recall context built from the LOCAL store (INDEX + hubs + nearest pages) so the
2622
+ model decides placement (extend / attach-under-hub / new leaf) against the real
2623
+ wiki, and returns minimal valid PKF (`<h1>`/`<h2 id>`/`<p>`; cloud materialize
2624
+ validates). Bounded retry (2 attempts) on transient gateway failure; never blocks.
2625
+ - **Post-turn hook re-purposed (`hook-server.ts`):** `handlePostLlmCall` (a no-op
2626
+ after P3') now responds 204 immediately, then fire-and-forget builds the recall
2627
+ context, runs the in-pod extraction, and writes each PKF page via the SAME direct
2628
+ path `handleWrite` uses — `slot.store.write` + outbox `memory.page.upsert` — so
2629
+ pages up-sync and get anti-orphan-anchored on cloud. Scratch/eval sessions skipped.
2630
+ `extract.ts` is re-wired (dead cloud-extract imports removed).
2631
+ - **`run_dream` removed from curate:** the Python provider `memory_curate` no longer
2632
+ advertises `op=run_dream` to every agent, and the daemon `handleCurate` (rpc.ts)
2633
+ no longer forwards `run_dream` to the cloud `/page-dream` LLM. Dream is the
2634
+ orchestrator's memory-dream skill calling the write VERBS, not a cloud-LLM trigger.
2635
+ The three write-verb ops (promote_to_hub / supersede / rebuild_index) are kept.
2636
+
2637
+ ### Changed — retire cloud-side memory extraction; extraction moves to the agent runtime (memory203/13 §0.5, P3')
2638
+
2639
+ - **Governance (用户裁决 2026-06-30):** all memory LLM work runs in the agent's
2640
+ OWN runtime; the cloud does ZERO LLM for memory. The cloud is storage only
2641
+ (materialize page upsert + anti-orphan-anchor to INDEX + recall queries).
2642
+ - `memory_write` is a DIRECT write again. The daemon `POST /local/memory/write`
2643
+ handler reverts to `slot.store.write(...)` + outbox `memory.page.upsert`
2644
+ enqueue (the page then syncs to cloud, materializes, and is anchored to INDEX
2645
+ by the existing cloud path). It no longer wraps the content into a synthetic
2646
+ turn and routes it through any cloud-extract lane — that prior design is废'd.
2647
+ `memory_write` = the agent persisting a PKF page it ALREADY authored in its own
2648
+ runtime (主动 explicit, or Hermes `background_review` 自动).
2649
+ - The daemon `extract-turn` / `extract-compress` routes (which forwarded turns to
2650
+ cloud `/api/im/memory/extract`) are RETIRED — they now short-circuit to an
2651
+ inert no-op and forward NOTHING to any cloud LLM. The `runExtract` /
2652
+ `runExtractInBackground` / `handleExtractTurn` / `handleExtractCompress`
2653
+ helpers (and the `extractFromTurn` import) were removed from `rpc.ts`.
2654
+ - The Python Hermes provider: `memory_write` reverts to "write the PKF page you
2655
+ authored" (direct write; `path`/`content` are the literal page, not extraction
2656
+ hints). `sync_turn` / `on_pre_compress` are now NO-OPs (they no longer forward
2657
+ to the retired cloud-extract routes); automatic auto-extraction is Hermes's
2658
+ native `background_review` in-runtime. The unused `_http_post_async` helper was
2659
+ removed.
2660
+ - (cloud, src/im) `extractMemories` (the cloud LLM extractor) + its
2661
+ `buildWikiRecallContext` structured-recall helper are retired: `memory-extract.ts`
2662
+ is now an inert tombstone that throws, and `POST /api/im/memory/extract` returns
2663
+ 410 Gone. No cloud LLM call remains for memory.
2664
+
2665
+ ### Changed — auto-extraction is now non-blocking + index-TOC injection is structure-preserving (memory203 scale prep)
2666
+
2667
+ - **Non-blocking extract** — `sync_turn` (provider shell) now dispatches the
2668
+ `extract-turn` POST on a daemon thread (`_http_post_async`) and returns
2669
+ instantly, so an agent reading many docs never stalls the turn loop on the
2670
+ cloud LLM round-trip. The daemon `extract-turn` handler ACKs `202 {queued}`
2671
+ immediately and runs `extractFromTurn` in a detached promise (errors logged,
2672
+ never lost). In-process callers (`hook-server`) keep the awaitable
2673
+ `{pages,extracted,error}` contract — fire-and-forget is the HTTP-path default
2674
+ only. New `test/extract-nonblocking.test.ts`.
2675
+ - **Index-TOC scaling** — `index-toc.ts` no longer drops the tail when the
2676
+ injected workspace index exceeds the (hard, Hermes-2200-capped) ≤1,800-char
2677
+ managed-section budget. It now truncates DEPTH-FIRST — every top-level hub
2678
+ heading (the navigational spine) survives; only the deepest leaf detail is
2679
+ trimmed — and appends an observable marker (`… N more sections — load the
2680
+ index page`) so the agent knows to drill in rather than assume it saw
2681
+ everything. Stays a pure function.
2682
+
2683
+ ### Added — multi-device remote-conflict status surfaced to the daemon (memory203 doc 07)
2684
+
2685
+ - The distributed reconcile core (invalidate fan-out + LWW/DAG conflict
2686
+ resolution + idempotency) already worked (7/7 e2e), but a daemon never learned
2687
+ its write LOST a conflict — the `remote-conflict` status lived only in cloud
2688
+ metadata. Now it travels cloud→daemon two ways: (A) inlined in the sync-inbox
2689
+ ACK (`SyncInboxResult.conflicts[]`) so an online loser learns immediately, and
2690
+ (B) on down-sync re-pull, the cloud page summary/detail carries
2691
+ `syncStatus:'remote-conflict'` (computed in `enrichSummaries` from the live
2692
+ curation-conflict version pointer) and `cloud-sync.materialisePage` stamps the
2693
+ local row via the new `MemoryStore.setSyncStatus`.
2694
+ - New `GET /local/memory/conflicts` (peek, not resolve) lists local
2695
+ remote-conflict pages to the host, behind the same F5 boundary predicate.
2696
+ - Tests: cloud `conflict-status.test.ts` 4/4 + daemon `memory-conflict-status.test.ts`
2697
+ 3/3, both with negative controls; existing `memory-e2e-sync.test.ts` stays 7/7.
2698
+ Offline catch-up cursor (C7) remains the pre-existing deferred limitation.
2699
+
2700
+ ### Added — at-rest memory encryption ACTIVATION behind flag (memory203 M-ENC, default OFF)
2701
+
2702
+ - The AES-256-GCM primitives (`crypto-cipher.ts`, `key-manager.ts`), the outbox
2703
+ encrypt path, and the cloud decrypt/FTS-exclude were already present but never
2704
+ fired — no caller marked a page `encrypted=true`. Activated the daemon write
2705
+ side: `MemoryStore` gains an `encryptionPolicy?` seam; `runner-wiring` installs
2706
+ a policy that returns true only when `isEncryptionEnabled()`
2707
+ (`FF_MEMORY_ENCRYPTION_ENABLED==='true'`, dynamic, default OFF) AND storage is
2708
+ not ephemeral AND a durable workspace key exists. `store.write` consults it
2709
+ only when the caller didn't set `encrypted` explicitly (down-sync's explicit
2710
+ per-page flag is never overridden). Fail-closed: flag off / ephemeral / no key
2711
+ → plaintext, never keyless ciphertext.
2712
+ - New `test/memory-encryption-roundtrip.test.ts` (6 tests): encrypt→outbound
2713
+ ciphertext→down-sync decrypt round-trip + 2 negative controls. Cross-device key
2714
+ sharing stays out of scope (doc 06 deferral).
2715
+
2716
+ ### Fixed — auto-extract error surfacing + read-after-write + PKF round-trip (memory203 Wave 1)
2717
+
2718
+ - **`daemon/memory/extract.ts#extractFromTurn`** — no longer returns a bare
2719
+ `ExtractedPage[]` (which collapsed LLM timeout / bad-response into a silent
2720
+ `extracted:0`). Now returns `{ pages, extracted, error }`: `error` is set on a
2721
+ cloud/LLM transport failure OR when the cloud produced candidates that were all
2722
+ rejected by the PKF round-trip gate (`saved=0 && skipped>0`), so a degraded
2723
+ extract is observable instead of masquerading as "nothing to save". Adds a
2724
+ bounded 1-retry on transient (status 0 / 5xx / network) failures with warn logs.
2725
+ - **read-after-write** — after mirroring an extracted page into the local
2726
+ `ScopedMemoryStore`, `confirmMirror()` re-reads it via the same bucket so the
2727
+ next turn's recall is guaranteed to see it; a miss logs loudly (best-effort, the
2728
+ cloud holds the canonical row).
2729
+ - **`rpc.ts` / `hook-server.ts`** — both extract consumers propagate the new
2730
+ `error` field instead of dropping it.
2731
+ - New `test/memory-extract-turn.test.ts` (9 tests incl. negative controls: all-
2732
+ rejected candidates → error surfaced, 5xx → retried, malformed PKF → not
2733
+ persisted) + cloud-side `assertPkfRoundTrips` gate in `memory-extract.ts`.
2734
+
2735
+ ### Changed — memory203 agent-integration convergence onto the native MemoryProvider (doc 10 §3)
2736
+
2737
+ - **`adapters/persistence/hermes/index.ts#configurePrismerProvider`** — when the
2738
+ Prismer MemoryProvider is active (`installMemoryProvider` / `PRISMER_MEMORY_PROVIDER=1`)
2739
+ it now SUPERSEDES the two fragmented paths: the curl `installMemoryHooks` block is
2740
+ skipped (the provider's in-process `sync_turn`/`prefetch` are the native equivalent —
2741
+ running both double-extracts the same turn) and the standalone recall-tools plugin is
2742
+ NOT installed (the provider's `get_tool_schemas` already exposes `memory_search`/
2743
+ `memory_load`/`memory_curate`). Both paths remain intact as the **no-provider
2744
+ fallback** (eval opt-out via `installMemoryHooks: false` preserved). `configurePrismerProvider`
2745
+ + `HermesProfileConfigSchema` are now exported for the config-gen unit test.
2746
+ - **Port fix** — the daemon-port default across the hermes config-gen + the Python
2747
+ provider shell (`plugins/memory/prismer/__init__.py`) moved from the stale `3210`
2748
+ to `7878` (the daemon's real loopback port in agent-rt; `PRISMER_DAEMON_PORT` env
2749
+ still wins). The provider's `_http_get` + the `prismer memory` CLI both read
2750
+ `PRISMER_DAEMON_PORT`, which the config-gen writes into the profile `.env`.
2751
+ - **`test/memory-provider-configgen.test.ts`** — new: asserts provider-on pins
2752
+ `memory.provider: prismer`, writes `PRISMER_DAEMON_PORT` to `.env`, installs neither
2753
+ hooks nor double recall-tools; provider-off fallback still wires the curl hooks (7878).
2754
+ - **Provider-install path fix (`installMemoryProviderShell`)** — the shell was copied
2755
+ to `<profile>/plugins/memory/prismer/`, but Hermes's MEMORY-provider scanner
2756
+ (`plugins/memory/__init__.py` `_iter_provider_dirs`) discovers USER-installed
2757
+ providers ONE level deep at `<HERMES_HOME>/plugins/<name>/` — the extra `memory/`
2758
+ segment exists only for BUNDLED providers. So `find_provider_dir("prismer")` returned
2759
+ `None` and the provider was NEVER loaded: the agent saw no `memory_search` tool and
2760
+ reported "memory not available" despite correct config + a working daemon RPC. Fixed
2761
+ to install at `<profile>/plugins/prismer/`. Verified live in agent-rt: relocation →
2762
+ provider loads (`3 tools`) → agent recalls the seeded Helios corpus (18,400 ev/s)
2763
+ end-to-end. The two install/config-gen tests asserted the broken nested path (green
2764
+ while encoding the bug); both updated to the discoverable layout + a guard that the
2765
+ nested path is absent.
2766
+
2767
+ ### Fixed — daemon FTS recall is AND-first with OR-fallback (not AND-only)
2768
+
2769
+ - **`search.ts#hybrid`** — the local FTS MATCH joined every term with implicit
2770
+ AND, so a natural multi-word recall ("helios throughput target") only hit pages
2771
+ containing ALL terms; a padded query silently returned zero. Now runs AND first
2772
+ (precision), then relaxes to OR when AND yields nothing and the query has ≥2
2773
+ terms (recall gate). BM25 still ranks pages matching more terms higher.
2774
+
2775
+ ### Fixed — cloud→daemon subset projection preserved visibility + canonical id (live MVP3/MVP4)
2776
+
2777
+ - **ACL leak (`cloud-sync.ts#materialisePage`)** — the visibility projection was
2778
+ `page.visibility === 'agent'`, but the cloud stores an owner-PREFIXED string
2779
+ (`agent:<imUserId>`). The exact match never hit, so EVERY agent-private page
2780
+ was stored `{kind:'workspace'}` and recallable by every in-workspace agent (a
2781
+ second agent surfaced another agent's private cost page in the live MVP3 run).
2782
+ New `parseCloudVisibility()` maps `workspace` / `agent:<id>` / fail-closed
2783
+ `private:<id>` (human:/task:/unknown owner kinds), preserving the owner id the
2784
+ boundary predicate (`canCapReadPage`) gates on.
2785
+ - **Curate 404 (`store.ts#write` + `MemoryWriteInput.id`)** — the subset minted a
2786
+ local `page_<uuid>` id instead of the cloud's canonical id, so `memory_curate`
2787
+ promote-to-hub/supersede forwarded an id the cloud didn't know → 404 (the agent
2788
+ then faked success). `materialisePage` now passes `page.id`; the subset mirrors
2789
+ the superset's id. Daemon-authored writes still mint a local id; path conflicts
2790
+ retain the existing id (content/fts/links FK-safe).
2791
+ - **`test/memory-cloud-sync.test.ts`** — extended with agent:/human: pages
2792
+ asserting visibility kind+owner AND the preserved cloud id (the corpus only
2793
+ exercised `visibility:'workspace'`, which is why both bugs hid).
2794
+
2795
+ ### Added — memory203 local-first load fallback + invalidate re-pull + F5 search收口 (doc 07 §3/§4, doc 08 §4a)
2796
+
2797
+ - **`daemon/memory/cloud-sync.ts#fetchPageFromCloudByPath`** — targeted single-page
2798
+ 回源 for the local-first `load` fallback: a subset MISS pulls just that page from
2799
+ the cloud superset (`GET /api/im/memory/resolve?uri=`), materialises it via the
2800
+ existing `materialisePage` (identical decrypt + visibility + write), and the handler
2801
+ re-loads it locally (<5ms FTS). Strictly best-effort + local-first: offline → the
2802
+ fetch returns false → genuine 404, never a 5xx ("断云仍 load 本地").
2803
+ - **`daemon/memory/rpc.ts`** — `handleLoad` is now async; `attachMemoryRpc` gained a
2804
+ `keyManager` option (runner wires `memoryWiring.keyManager`) so回源'd encrypted pages
2805
+ decrypt to local plaintext (fail-closed to sentinel when keyless).
2806
+ - **`daemon/memory/ws-invalidate.ts`** — a non-`soft_delete` cloud invalidate
2807
+ (visibility_changed / promoted / archive) now fires a best-effort
2808
+ `initialSyncFromCloud` to re-pull the fresh SUBSET projection (watermark-bounded,
2809
+ subset fields only — never the full aclJson). `soft_delete` stays mark-only.
2810
+ - **`daemon/memory/rpc.ts#handleSearch`** — F5 search收口 (option a): when a cap is
2811
+ present, search hits are filtered through `canCapReadPage` (bounded `loadById` point
2812
+ lookup per hit, topK ≤ 20) so another agent's private-page snippet never leaks via
2813
+ search — F5 is now uniform across load / list / search.
2814
+ - **Tests** — `memory-load-fallback` (4), `memory-ws-invalidate` (+1 re-pull),
2815
+ `memory-acl-predicate` (+1 search filter). True daemon↔cloud contract проven by
2816
+ `scripts/cookbook/e2e-memory-sync.ts` (real outbox envelope → real `ingestSyncInbox`
2817
+ → materialises `IMMemoryPageSection` + `IMMemorySyncEvent`; flushed:1 deadLettered:0).
2818
+
2819
+ ### Added — P0 memory security spine: per-agent scoped capability tokens (memory203 doc 08, F1–F5)
2820
+
2821
+ Closes the 🔴 daemon memory空腔 (doc 06 §2): `/local/memory/*` RPC + key access had
2822
+ no credential / workspace scope — any same-pod agent could pass another `workspaceId`
2823
+ to read/write/decrypt across workspaces. The fix is mechanism **C3**: the daemon
2824
+ self-signs + self-verifies a per-agent **capability** with a per-boot key.
2825
+
2826
+ - **`daemon/memory/cap.ts`** (new) — `mintCap(sub, ws)` / `mintSystemCap()` /
2827
+ `systemCap()` / `verifyCap()` / `capAllowsWorkspace()` / `isSystemCap()`. Token =
2828
+ `v1.<b64url(payload)>.<HMAC>`; payload = `{aud:'memory', sub, ws, scope:['ws:'+ws],
2829
+ iat, exp}`. The per-boot key is `randomBytes(32)` held ONLY in module memory — never
2830
+ written, serialized, returned, or injected (daemon restart → new key → all prior
2831
+ caps reject). Agent caps are single-workspace; `ws:*` is hard-rejected for agents and
2832
+ reserved for the daemon-internal system cap.
2833
+ - **`daemon/memory/rpc.ts`** — every handler now passes a verified cap and gates the
2834
+ effective workspaceId (query / body / `?uri=`) via `scopeOk`/`effectiveWs`: cross-ws
2835
+ → 403 `memory_ws_scope_violation`. `PRISMER_MEMORY_CAP_ENFORCE` (default OFF one
2836
+ release cycle): no/invalid cap → 401 `memory_cap_invalid` when on, warn+proceed when
2837
+ off — but a present cap is ALWAYS scope-checked. Out-of-process provider routes now
2838
+ stamp `actorImUserId` from `cap.sub` instead of `''`.
2839
+ - **`daemon/memory/key-manager.ts`** — `getKey(workspaceId, cap)` gates key access via
2840
+ `capAllowsWorkspace` and returns null WITHOUT touching the filesystem when the cap is
2841
+ not authorized (fail-closed; the AES key is never the cap). `getKeyOrNull` is now the
2842
+ internal raw loader. Daemon-internal flush/write-down callers
2843
+ (`outbox-worker.ts`, `cloud-sync.ts`) pass `systemCap()`.
2844
+ - **`daemon/memory/acl-predicate.ts`** (new) — `canCapReadPage(cap, page)`: the daemon's
2845
+ BOUNDARY projection of the shared ACL predicate (ws scope + page visibility kind),
2846
+ wired into `load` (cross-agent private → 404, no existence leak) and `list` (filter).
2847
+ The cloud keeps the FULL projection (`memory-acl.ts` aclJson) — two layers, no drift.
2848
+ - **Injection chain (F4)** — `adapters/prismer-env.ts` mints `PRISMER_MEMORY_CAP` from
2849
+ the dispatch metadata (covers claude-code/codex/provider-proxy via
2850
+ `applyPrismerScopeEnv`); the hermes gateway spawn injects it per-boot;
2851
+ `adapters/memory-tools.ts` and the two python shells
2852
+ (`plugins/{memory/prismer,tools/prismer-recall}/__init__.py`) forward it as the
2853
+ `x-prismer-memory-cap` header (env-default, zero extra wiring for in-process tools).
2854
+ - **Tests** — `memory-cap` (15), `memory-cap-rpc` (10), `memory-cap-injection` (5),
2855
+ `memory-acl-predicate` (6), `memory-encryption` (+2 key-gate). 274 memory tests green.
2856
+
2857
+ ### Changed — `cloud task create` delegation is now a tracked board card (release203/21 §4 T1)
2858
+
2859
+ - **`cli/commands/task.ts`** — `cloud task create --agent <id>` now defaults to a
2860
+ **delegated work_item** (`metadata.kind='work_item'` + `dispatchPolicy='on-assign'`)
2861
+ instead of `kind='agent_run'`. The task therefore lands on the Kanban board (留痕/
2862
+ 可观测) **and** auto-dispatches the moment the assignee is set — decoupling "shows
2863
+ on the board" (kind) from "auto-dispatches" (dispatchPolicy). Result write-back +
2864
+ session-anchor binding are unchanged (the cloud settles the board card itself via
2865
+ the IMTask fallback path).
2866
+ - **New `--no-card` flag** preserves the prior pure no-card run channel
2867
+ (`kind='agent_run'`) for internal/orchestrator sub-steps that should NOT surface
2868
+ as a board card.
2869
+
2870
+ ### Added — `agent.fs.read` / `agent.fs.write` repo data-plane RPCs (release203/21 §6 R, C3)
2871
+
2872
+ - **`daemon/fs-read.ts`** (`readReposFile`) + **`daemon/fs-write.ts`** (`writeReposFile`)
2873
+ — new reverse-channel RPCs mirroring the existing `agent.fs.list`: same
2874
+ `resolveProjectReposDir` base + path-jail (no `../` escape out of the workspace
2875
+ root). `agent.fs.read` returns `{ content, encoding:'utf8'|'base64', sizeBytes,
2876
+ mtimeMs, sha256, mime? }`, rejects >1 MiB (`too_large`), base64s binary.
2877
+ `agent.fs.write` is atomic (temp+rename, `mkdir -p` parents), honours optional
2878
+ `ifMatchSha256` (mismatch → `conflict`, no overwrite).
2879
+ - **`daemon/runner.ts`** — wired `agent.fs.read` / `agent.fs.write` cases
2880
+ (`onAgentFsRead` / `onAgentFsWrite`, mirroring `onAgentFsList`); replies on
2881
+ `agent.fs.read.reply` / `agent.fs.write.reply`. Old daemons lacking these cases
2882
+ fall through to `unknown-message` → cloud invoke times out → frontend degrades to
2883
+ read-only (no version negotiation needed).
2884
+ - Cloud side: `POST /api/im/workspaces/:wid/fs/{read,write}` + `RepoCodePanel` /
2885
+ `/repo-code-lab` Monaco harness (see cloud repo). Live-verified: write→read exact
2886
+ roundtrip + `ifMatchSha256` conflict guard.
2887
+
2888
+ ### Added — INDEX dynamic core-inject (memory202 doc 05 §4.2a, flag-gated OFF)
2889
+
2890
+ - **`daemon/memory/index-toc.ts`** — `buildIndexToc(indexMarkdown, budget)`: a
2891
+ PURE, section-aware, bounded TOC builder over the workspace INDEX page (the
2892
+ curated memory MAP). Within budget → the whole map prefixed with a short
2893
+ `# Memory Map` header; over budget → drops to the SKELETON of headings (the map
2894
+ shape) cut at heading boundaries, NEVER mid-section. Reuses the recall path's
2895
+ fence-aware ATX scanner (`section.ts#scanHeadingsForToc`) so "what is a section"
2896
+ is identical across slice + TOC. Empty / whitespace / budget≤0 → `''` (no throw).
2897
+ - **`daemon/memory/index-toc-inject.ts`** — flag-gated wiring that reads the local
2898
+ INDEX page → `buildIndexToc` → the EXISTING `coreInject` carrier
2899
+ (`createHermesMemoryIntegration(...).coreInject` → MEMORY.md managed section,
2900
+ already tracked by `recall-stats.recordCoreInject`). So an agent always has the
2901
+ current memory map in context. Gated on **`FF_MEMORY_INDEX_INJECT_ENABLED`**
2902
+ (default OFF → zero behaviour change, no `coreInject` call).
2903
+ - **`MemoryStore.loadIndexPageContent()`** — reads the workspace's
2904
+ `pageType='index'` page content (queried by the raw `'index'` string the cloud
2905
+ emits and `materialisePage` stores verbatim).
2906
+ - **Daemon trigger:** wired into `syncMemoryFromCloud` (runs on initial sync +
2907
+ post `host.acked`) — "dynamic" = re-injects whenever the synced map changes. The
2908
+ runner supplies the per-hermes-profile MEMORY.md carrier path(s); the wiring
2909
+ stops at "build TOC → call existing coreInject" and does NOT touch adapter-turn /
2910
+ envelope orchestration (that live-activation path stays held back).
2911
+ - **Deferred (follow-up):** a "memory map injected" UI indicator (dim 1) and the
2912
+ default-on activation (a separate convergence gate, like envelope).
2913
+
2914
+ ### Added — Memory at-rest encryption (memory202 doc 06, MVP, flag-gated OFF)
2915
+
2916
+ - **Local-first AES-256-GCM encryption for memory pages.** The daemon holds a
2917
+ per-workspace symmetric key; the cloud stores only ciphertext and NEVER holds
2918
+ the key. Encrypted pages are excluded from cloud full-text search (cloud can't
2919
+ read them) — searchable only on the daemon (local plaintext). Non-encrypted
2920
+ pages are completely unaffected. Gated on `FF_MEMORY_ENCRYPTION_ENABLED`
2921
+ (default OFF → zero behavior change).
2922
+ - **`daemon/memory/crypto-cipher.ts`** — `encrypt`/`decrypt` over
2923
+ `crypto.createCipheriv('aes-256-gcm', …)` with a random 12-byte IV per call;
2924
+ self-describing packed format `v1:<iv>:<tag>:<ct>` (base64url). Authenticated:
2925
+ a GCM-tag/ciphertext tamper or wrong key throws on decrypt (never leaks
2926
+ plaintext).
2927
+ - **`daemon/memory/key-manager.ts`** — per-workspace 32-byte key generated on
2928
+ first need, persisted `<baseDir>/<workspaceId>/.memkey` (0600), reloaded on
2929
+ demand. The key never leaves the daemon. **Fail-closed:**
2930
+ `PRISMER_EPHEMERAL_STORAGE=true` (agent-rt emptyDir) → refuses to encrypt
2931
+ (returns null; caller writes plaintext with a loud warn) so a key written to
2932
+ ephemeral storage can never be lost-then-orphan-the-ciphertext. A persist /
2933
+ round-trip-verify failure likewise fails closed.
2934
+ - **`MemoryStore.write({ encrypted })`** threads the flag to the local
2935
+ `memory_pages.encrypted` column (was hardcoded 0); local SQLite + FTS keep
2936
+ PLAINTEXT (recall needs it) — encryption happens only on the cloud-bound
2937
+ outbox payload.
2938
+ - **Outbox flush encryption** (`outbox-worker.ts`): a `memory.page.upsert` whose
2939
+ page row is `encrypted=true` has its `payload.content` encrypted in the POST
2940
+ body only. A page that should be encrypted but cannot be (no key) is held back
2941
+ (left pending), NEVER POSTed in the clear.
2942
+ - **Sync-down decrypt** (`cloud-sync.ts`): ciphertext pulled from the cloud is
2943
+ decrypted with the local key before landing as plaintext in local FTS. **Cross-
2944
+ device deferral:** key absent / decrypt fails → stores an unreadable sentinel
2945
+ (never ciphertext-as-content), sync never crashes.
2946
+ - **Deferred (per doc 06):** agent-rt ephemeral-pod encrypted memory (fail-closed
2947
+ for now), cross-device key exchange, key rotation / re-encryption.
2948
+
2949
+ ### Added — Hermes per-dispatch identifier auto-resolution (release203/15c WS-E3)
2950
+
2951
+ - **`cloud deliver` / `cloud task attach` / `cloud file send` / `cloud attach`
2952
+ now work on Hermes agents WITHOUT manually copying `--run-id` /
2953
+ `--conversation-id`.** Hermes (persistence) agents have no per-dispatch env
2954
+ (the gateway spawns once with a frozen env; per-dispatch ids only reach the
2955
+ model via `<execution_context>` XML, never the tool-shell). Previously the
2956
+ agent had to abstract the ids out of `<execution_context>` and pass them as
2957
+ flags or the daemon proxy returned "taskId required".
2958
+ - **`RunSessionRegistry.lookupActiveByAgent(agentImUserId, { adapterName?,
2959
+ conversationId?, ttlMs? })`** (`daemon/memory/run-session-map.ts`) — reverse-
2960
+ lookup the agent's CURRENT in-flight dispatch from `local_run_sessions`
2961
+ (reusing the existing `(conversation_id, agent_im_user_id, adapter_name)`
2962
+ index; NO new table/migration). Returns `null` (0 active), the ctx (exactly
2963
+ 1), or `{ ambiguous, candidates }` (>1 with no narrowing key). 15min in-flight
2964
+ TTL window.
2965
+ - **`daemon/asset/deliver.ts`** `validate()` relaxed: `taskId` may be empty when
2966
+ `resolveActiveDispatch:true` + `agentImUserId` present; the handler then
2967
+ reverse-looks-up to fill taskId(=runId)/conversationId. Race-safety (doc §5):
2968
+ conversation-narrowed lookups are unambiguous; **>1 in-flight run with no
2969
+ narrowing → HTTP 409, NEVER a silent mis-attach**; miss → 400 "未找到活跃
2970
+ dispatch,请传 --run-id". `DeliverRequest` gains `agentImUserId?` /
2971
+ `resolveActiveDispatch?`; `DeliverHandlerResult` status union gains `409`.
2972
+ - **Hermes `terminal.env_passthrough` config (`adapters/persistence/hermes/
2973
+ index.ts`)** — registers `PRISMER_AGENT_USERNAME` / `PRISMER_AGENT_IM_USER_ID`
2974
+ / `PRISMER_DAEMON_PORT` / `PRISMER_WORKSPACE_ID` as passthrough so they survive
2975
+ Hermes' `_scrub_child_env` (allowlist scrub: only `_SAFE_ENV_PREFIXES` +
2976
+ registered passthrough reach the tool subprocess). **This corrects the design
2977
+ doc's premise** that the identity vars were already present in the tool-shell
2978
+ env: they are present in the GATEWAY env but were silently scrubbed from the
2979
+ CHILD env, which would otherwise leave `cloud deliver` with neither a dispatch
2980
+ id NOR an agent identity to auto-resolve from. These vars are identity/
2981
+ transport (not credentials, no KEY/TOKEN substring → not GHSA-blocklisted).
2982
+ - Live cookbook `scripts/cookbook/regress-hermes-deliver-autoresolve.ts`: a real
2983
+ hermes agent runs `cloud deliver` with no flags → daemon log shows
2984
+ `[deliver] active-dispatch auto-resolve … runId=<this dispatch>` + the file
2985
+ lands as an asset; the >1-run ambiguity → 409 path is verified against the real
2986
+ migrated local SQLite schema.
2987
+
2988
+ ### Added
2989
+
2990
+ - **Daemon-side Dream scheduler wired into boot** (memory202 doc 05). The
2991
+ `DreamScheduler` existed but was never instantiated (`new DreamScheduler`
2992
+ appeared only in tests) — the memory lifecycle was dead code. `attachMemoryRunner`
2993
+ now instantiates + `start()`s it behind `FF_MEMORY_DREAM_ENABLED` (default OFF,
2994
+ mirrors `FF_MEMORY_HTML_SIDECAR_ENABLED`), injecting a `CloudDreamRunner` that
2995
+ POSTs `/api/im/memory/page-dream` (fixed from a stale `/memory/page-dream` that
2996
+ would 404). Workspace tracking is fed via a new `MemoryOutboxWorker.onWorkspaceFlushed`
2997
+ hook → `scheduler.recordSessionEnd`. `recordActivity` is intentionally NOT fed
2998
+ from outbox flush (it drives the idle gate; feeding background flushes would
2999
+ perma-block dream) — the live-activity idle gate stays inert until adapter glue
3000
+ feeds `recordActivity` at LLM-call / tool-dispatch points (follow-up).
3001
+ `MemoryRunnerWiring.stop()` stops the scheduler. Flag OFF = zero behaviour change.
3002
+
3003
+ ### Changed
3004
+
3005
+ - **`workspace member` list/add/update/remove logic extracted to a shared
3006
+ command-builder** (release203/15b WS-E4 §7.3 "B" rollout). The member-management
3007
+ verbs — identical endpoints (`/api/im/workspaces/:id/members`), identical role
3008
+ contract (`admin|member`; `owner` rejected as a v2.1+ RFC), identical
3009
+ remove-cascade — were duplicated verbatim against `cloud workspace member`. They
3010
+ now live once in `src/cli/shared/workspace-member-builder.ts`
3011
+ (`buildWorkspaceMemberCommand(adapter)`, parameterized by a
3012
+ `WorkspaceMemberAdapter` seam: `list/add/update/remove` + `emitList/emitMember/
3013
+ emitRemove` + `fail`). `prismer` injects a `CloudClient` + `ui.table`/`ui.line`
3014
+ adapter; `cloud` injects a `PrismerClient.workspaces.members` + padded-text
3015
+ adapter. Same no-dependency-edge / tsup-inline property as the `task wait` PoC
3016
+ (verified empirically: builder body inlined into `dist/cli.js`, no runtime
3017
+ `runtime/src` require). Behaviour preserved: success-path output and per-verb
3018
+ error codes byte-identical; the `--role` validation message is now the single
3019
+ canonical one (was already cloud's; prismer's variant retired). The `add` verb
3020
+ accepts BOTH the prismer positional (`<imUserId>`) and the cloud `--user` flag,
3021
+ so neither binary's existing invocation breaks. Gates:
3022
+ `regress-cloud-task-wait.ts` 4/4 + `regress-skill-cli-contract.ts` no new drift
3023
+ (workspace subcommands resolve via the followed builder import). The other
3024
+ candidate namespaces (`agent`/`memory`/`asset`) were audited and NOT extracted —
3025
+ their cross-CLI "overlap" is name-only over divergent transports/outputs, so a
3026
+ shared builder would change one side's UX (see `docs/release203/15b-*` §5).
3027
+
3028
+ - **`task wait` poll/settle logic extracted to a shared command-builder**
3029
+ (release203/15 WS-E4 §7.3 "B", PoC). The settle loop that both `prismer task
3030
+ wait` and `cloud task wait` (in `@prismer/sdk`) carried verbatim now lives once
3031
+ in `src/cli/shared/task-wait-builder.ts` — a transport/output-agnostic
3032
+ `buildTaskWaitCommand(adapter)` parameterized by an injected `TaskWaitAdapter`
3033
+ (`getTask`/`emit`/`fail`). `prismer task wait` supplies a `CloudClient` + JSON
3034
+ adapter; `cloud task wait` supplies a `PrismerClient.im` + text adapter. The
3035
+ two packages have no dependency edge — `@prismer/sdk` imports this module via a
3036
+ build-time relative path and `tsup` inlines it, so both tarballs stay
3037
+ standalone. Behaviour is byte-for-byte unchanged (settle on
3038
+ review/completed/failed/cancelled by default, `--terminal-only`,
3039
+ failed/cancelled → non-zero exit). Rollout for the remaining overlapping
3040
+ namespaces: `docs/release203/15b-shared-builder-plan.md`. Gates:
3041
+ `regress-cloud-task-wait.ts` 4/4 + `regress-skill-cli-contract.ts` green.
3042
+
3043
+ ### Added
3044
+
3045
+ - **`cloud role create` / `validate` accept a role bundle DIRECTORY**
3046
+ (release203/16 §5.4 / §9 decision 2, P4). A role authoring unit is now either a
3047
+ single `role.json` (legacy, still accepted) OR a directory containing
3048
+ `role.json` + optional `SOUL.md`. When a directory carries a non-empty
3049
+ `SOUL.md`, its raw markdown becomes the role's `operatingPrinciples`
3050
+ (Hermes-compatible persona, slot#1), overriding/filling role.json's field;
3051
+ `AGENTS.md` is NOT part of the bundle (it's a per-role skill concept). Detection
3052
+ is by `statSync().isDirectory()`. New shared pure helpers `readRoleBundle` /
3053
+ `validateRoleBundle` (`src/bundle/index.ts`) back both the CLI and the zero-dep
3054
+ `role-builder/scripts/ingest-role.mjs` offline path. No cloud schema or
3055
+ service-signature change — only the source of `operatingPrinciples` (SOUL.md
3056
+ file vs json field). Tests: `bundle.test.ts` +7 dir-bundle cases,
3057
+ `skill-role-lifecycle-cli.test.ts` +2 CLI cases (49/49 green); runtime tsc clean.
3058
+ - **Daemon honors cloud version-skew directive on `host.acked`** (release203/19
3059
+ §1.2/§2.2/§3, P2). The daemon already reports `daemonVersion` on every
3060
+ `agent.host.declare`; the cloud now compares it against its configured
3061
+ `EXPECTED_DAEMON_VERSION` and, per the `DAEMON_VERSION_SKEW_POLICY` Nacos flag
3062
+ (default `warn` ⇒ observability only), may attach an `upgradeDirective` to
3063
+ `host.acked`. The daemon reacts via `applyUpgradeDirective`:
3064
+ - `warn` / absent — no-op (normal operation; the common case).
3065
+ - `refuse_dispatch` — reject NEW dispatches in `onTaskDispatch` while in-flight
3066
+ runs drain and the daemon stays up.
3067
+ - `drain_respawn` — reject NEW dispatches AND arm a one-shot drain watcher that,
3068
+ once `runningTasks` empties (or a 30-min cap elapses), runs `stop()` and
3069
+ `process.exit(0)` so the device/k8s controller re-pulls a new-image pod.
3070
+
3071
+ Backward compatible: a legacy cloud never sends a directive (field absent ⇒
3072
+ ignored). `HostAckedPayload.upgradeDirective` + `DaemonUpgradeDirective` added
3073
+ to `types/im-events.ts`. Covered by
3074
+ `test/daemon-version-skew-directive.test.ts`.
3075
+
3076
+ ### Fixed
3077
+
3078
+ - **Daemon start-lock no longer self-locks on container restart** (release203/19
3079
+ §1.1/§2.1, P0). In a container the daemon runs as **pid 1** and writes
3080
+ `daemon.pid=1`. On a same-pod-sandbox container restart the emptyDir keeps the
3081
+ pidfile, so the new daemon — also pid 1 — saw `pidAlive(1)===true` and
3082
+ crash-looped with `Daemon already running (pid 1)` (Exited(1) loop hit by the
3083
+ doc17 e2e). The start guard now treats a **self-referential pidfile**
3084
+ (`existingPid === process.pid`) as our own stale leftover and proceeds,
3085
+ clearing the stale file before claiming it. New `daemonAlreadyRunning()` helper
3086
+ in `cli/util.ts` centralises the decision (`existingPid && existingPid !== ourPid
3087
+ && pidAlive(existingPid)`); both `runForeground` and `startBackground` use it.
3088
+ The pidfile is retained for `status`/`logs`/`stop`, but is no longer the sole
3089
+ liveness authority. The crash-safe positive fix for the residual pid-reuse class
3090
+ on non-pid-1 hosts (OS advisory flock) is intentionally deferred — Node has no
3091
+ built-in flock and the runtime carries no `fs-ext`/`proper-lockfile` dep; the
3092
+ self-pid guard fully resolves the container scenario without a native addon.
3093
+ Covered by `test/daemon-start-lock.test.ts`.
3094
+
3095
+ ### Added
3096
+
3097
+ - **`prismer task wait <id>` / `cloud task wait <id>`** (release203/15 §WS-E2).
3098
+ Block until an ALREADY-delegated task settles, then print it — the
3099
+ orchestrator's "delegate → wait → act on the result" primitive (the
3100
+ `tasks`/`agent-coordination` skills point here) so it doesn't hand-roll a
3101
+ `cloud task get` poll loop. Reuses `pollUntilTerminal` (now parameterised by
3102
+ stop-statuses). Settles on `review/completed/failed/cancelled` by default —
3103
+ `review` included because for a delegated task it means "assignee finished,
3104
+ awaiting YOUR approval"; blocking until `completed` would deadlock the very
3105
+ orchestrator that must approve. `--terminal-only` for strict terminal; exits
3106
+ non-zero on `failed`/`cancelled`. Gate: `regress-cloud-task-wait.ts` (real CLI
3107
+ against the live API on completed + review tasks).
3108
+
3109
+ - **Hermes (persistence) tool-call-mapper → structured `ToolCallDetail`**
3110
+ (release203/15 §WS-G). Hermes tool steps now carry a structured `detail` so
3111
+ `execute_code`/terminal/file/search/fetch calls render rich + expandable in the
3112
+ unified timeline, exactly like coding agents — instead of the bare row
3113
+ ("import subprocess (+3 行)" = inputSummary) with no expand. New
3114
+ `adapters/persistence/hermes/tool-call-mapper.ts` reuses the
3115
+ `ToolCallDetail` union from `coding/shared/agent-sdk-types.ts` (imported, not
3116
+ redefined) and maps the Hermes tool `args` shape: `execute_code`/`terminal`/
3117
+ `bash`/`shell`/`run_command` → `shell{command}` (command pulled from
3118
+ `args.code`/`args.command`), read/write/edit file tools → `read`/`write`/`edit`,
3119
+ `search`/`grep`/`web_search` → `search`, `fetch`/`browser`/`open_url` → `fetch`;
3120
+ unknown tools → `undefined` (falls back to inputSummary, no regression).
3121
+ Wired at `sessions-sse.ts` `tool.started` (pass `{detail, status:'running'}`,
3122
+ which also fixes altitude — shell → `'action'` so consecutive `execute_code`
3123
+ collapse into a group like coding) and `tool.completed` (rebuilds the
3124
+ same-shape detail from started args + output/exitCode, stashed by `toolCallId`
3125
+ since the completion payload drops the original args). The entire downstream
3126
+ (recorder `capDetail` → cloud task-step-recorder → unified WS → agent-message
3127
+ decode → ActivityDetail rendering + `canExpand=detailHasBody`) already supported
3128
+ rich details — this is purely the missing producer half; no cloud/schema/render
3129
+ change. ⚠️ The Hermes gateway's `_tool_progress` callback frequently drops the
3130
+ real tool output (`sessions-sse.ts:375-401`); where output is absent,
3131
+ `detail.output`/`result` is left undefined (never a fake) — command + expand +
3132
+ exitCode-when-present still render. Real-output forwarding is a separate
3133
+ Hermes-gateway gap. 13 unit tests.
3134
+
3135
+ - **`cloud skill create` + `cloud role` commands** (Standardization SS-01/SS-02
3136
+ ingestion). New CLI verbs that push material-derived bundles into the live
3137
+ platform with the configured API key: `cloud skill create <bundleDir>
3138
+ [--install --agent <id>]` reads a SS-01 skill bundle (frontmatter + files),
3139
+ builds a server-matching merkle `contentManifest`, and `POST`s
3140
+ `/api/im/skills` (+ install). New `cloud role` group — `create <role.json>
3141
+ [--apply --agent <id> --workspace-id <id>]`, `apply <slug>`, `list`, `show` —
3142
+ ingests/applies SS-02 role templates (`/api/im/role-templates` + `/apply`).
3143
+ Backs the new `skill-builder` / `role-builder` built-in skills (each also ships
3144
+ a zero-dependency `scripts/ingest*.mjs` for SDK-only/CI environments). Note:
3145
+ community skill creates are slug-prefixed server-side (`name` →
3146
+ `community-<name>`); use the returned slug for install + role `requiredSkills`.
3147
+ Verified 8/8 against the local stack (skill create+install+GET, role
3148
+ create+apply+GET, profile roleTemplate projection).
3149
+
3150
+ - **Per-role Hermes-native skill scope** (release203/13 P1). Roles can now govern
3151
+ which of Hermes' ~90 bundled native skills an agent sees via `skills_list`, not
3152
+ just our injected built-ins. The hermes adapter reads `nativeSkillScope`
3153
+ (`{ mode: 'allow'|'deny', categories?, skills? }`) — projected from
3154
+ `IMRoleTemplate.nativeSkillScope` into `profile.config` (top-level, with a
3155
+ `roleTemplate` snapshot fallback; `resolveNativeSkillScope` mirrors
3156
+ `resolveMcpAllowlist`'s Priority-1/2). `deny` subtracts more on top of the
3157
+ global `HERMES_NATIVE_SKILL_DENYLIST` floor; `allow` keeps ONLY the listed
3158
+ categories/skills (net-body) — the bundled set (enumerated from the home-root
3159
+ `skills/` dir, cached by mtime) minus the allow-set. allow-mode fail-safes to
3160
+ the deny floor when the bundle can't be enumerated (fresh boot). Our injected
3161
+ skills are never disabled (enumeration reads the pure-bundle root, not
3162
+ `profileDir/skills/`). Verified: 9 unit tests + 11 e2e assertions on the real
3163
+ bundle.
3164
+
3165
+ ### Changed
3166
+
3167
+ - **Daemon-side FORCE: all coding agents launch autonomous (no permission
3168
+ confirmation)** (release203). Coding agents run in agent-rt pods via the daemon
3169
+ and are ALWAYS non-interactive — a tool permission prompt can never be answered,
3170
+ so the provider blocked until the 300s watchdog (confirmed live: run f27vyi hung
3171
+ 328s). The pod itself is the isolation boundary, so coding sessions now launch
3172
+ fully autonomous regardless of the stored profile config. Force-seam = the
3173
+ canonical `CodeAgentDriver` (`adapters/coding/shared/code-agent-driver.ts`):
3174
+ `withAutonomousLaunch(provider, config)` is applied on BOTH `createSession`
3175
+ (`buildSessionConfig`) and `resumeSession` (the resume overrides), so it covers
3176
+ EXISTING agents whose stored handle metadata carries a stale/absent `modeId`,
3177
+ not just new ones. Policy: keep an already-autonomous `modeId`, otherwise force
3178
+ the per-provider autonomous mode:
3179
+ - **claude-code** → `modeId: 'bypassPermissions'` (the claude-agent-sdk
3180
+ `permissionMode` equivalent of `--dangerously-skip-permissions`; the SDK skips
3181
+ `canUseTool` entirely in this mode).
3182
+ - **codex** → `modeId: 'full-access'` + `sandboxMode: 'danger-full-access'` +
3183
+ `approvalPolicy: 'never'` (≙ `--dangerously-bypass-approvals-and-sandbox`).
3184
+ - **opencode** → `modeId: 'build'` + `featureValues.auto_accept = true` (opencode
3185
+ has no CLI bypass flag — `auto_accept` drives its `tryAutoApproveToolPermission`
3186
+ path, auto-replying `"once"` to every tool permission request).
3187
+
3188
+ CLI fallback adapters (registered as `claude-code-cli` / `codex-cli` D21
3189
+ fallbacks in `daemon/runner.ts`) also launch with the real bypass flags:
3190
+ `claude --dangerously-skip-permissions` (`adapters/coding/claude-code/index.ts`)
3191
+ and `codex … --dangerously-bypass-approvals-and-sandbox` (supersedes `--sandbox`,
3192
+ `adapters/coding/codex/index.ts` `buildCodexArgs`; the codex `resume` subcommand
3193
+ rejects sandbox/approval flags and inherits the persisted session's policy from
3194
+ turn-1, so no re-pass). Cloud `buildProfileConfig` is unchanged — this daemon
3195
+ force is defense-in-depth. New assertions:
3196
+ `test/code-agent-driver-autonomous.test.ts` (+ colocated driver test) and a
3197
+ codex CLI-args case in `test/codex-adapter-session-streaming.test.ts`.
3198
+
3199
+ - **Persistence agents (Hermes) launch autonomous too — no approval gating**
3200
+ (release203, "Hermes 也全自治"). The persistence analog of the coding-agent
3201
+ force above. The hermes gateway spawn (`adapters/persistence/hermes/index.ts`)
3202
+ now sets `HERMES_YOLO_MODE: 'true'` in the child env, disabling hermes' native
3203
+ dangerous-command approval gate. Rationale: the non-interactive agent-rt pod has
3204
+ no human at a prompt to answer an `approval.request`, and that event tears down
3205
+ our SSE stream rather than routing to an approval UI — so a flagged command
3206
+ would just block ~5min then fail. The pod IS the sandbox; hermes' hardline
3207
+ catastrophic patterns still block even with YOLO on (desired floor). Companion
3208
+ change in `daemon/dispatch.ts`: `DEFAULT_OPERATING_PRINCIPLES` no longer carries
3209
+ an approval-seeking line — `resolveOperatingPrinciples()` appends the policy line
3210
+ per `approvalPolicy`, and the `autonomous` branch injects zero approval-seeking
3211
+ text. Role-template refs (`templates/roles/ceo.json`, `skill-author.json`) set
3212
+ `approvalPolicy: 'autonomous'`, drop `human-approval` from required skills, and
3213
+ rewrote the `[Authority and approval]` clauses to autonomous + an
3214
+ anti-confabulation rule. NOTE: the 205 DB-seeded role templates are NOT migrated
3215
+ — existing agents keep their snapshot and pick up the fix on recreation (same
3216
+ model as the coding `modeId` fix).
3217
+
3218
+ ### Added
3219
+
3220
+ - **Daemon follows the unified single-WS realtime** (release203/12 P3.1). New
3221
+ `WsRealtimeSubscriber` (`daemon/gateway/ws-realtime-subscriber.ts`) consumes the
3222
+ cloud's unified `WS /ws/realtime?token=&since=` (the SAME endpoint the browser
3223
+ uses) instead of the legacy `GET /api/im/sync/stream` SSE. It demuxes the
3224
+ `{ch:'sync',name:'sync',data}` frames, ignores the `ch:'tasks'` projection (the
3225
+ daemon only ever materialized the sync fan-out), and feeds the SAME
3226
+ `Materializer` (rm_* upsert + per-conversation watermark + local relay) as the
3227
+ SSE path. Cursor parity: it persists the per-user seq under the SAME
3228
+ `chats`/`__sse__` watermark, so a daemon upgrading SSE→WS resumes from exactly
3229
+ where it left off; `sync.backfill.truncated{newestSeq}` still jumps the cursor
3230
+ (no replay storm). The runner constructs the WS subscriber by DEFAULT;
3231
+ `PRISMER_UNIFIED_WS=0` opts back to `SseSubscriber` (per-deploy kill switch,
3232
+ mirrors the client flag). Removes the daemon's second long-lived SSE connection
3233
+ to cloud — one user = one realtime connection.
3234
+
3235
+ - **Canonical agent identity injection** (release203/11 §2, Slice A — fixes净身
3236
+ coding agents answering "who are you?" as generic Claude). `TaskDispatchRequestPayload`
3237
+ gains an additive `identityContext?: { identity, user, scope }` (composed
3238
+ CLOUD-side — only cloud has the names). `dispatch.ts` now: (a) substitutes a
3239
+ new `CODING_SOUL_DEFAULT` engineering persona into the SOUL/persona slot when a
3240
+ coding agent (`claude-code`/`codex`/`opencode`) has an empty `config.systemPrompt`,
3241
+ and (b) forwards `identityContext` on a SEPARATE `metadata.identityContext` key
3242
+ (NOT folded into `metadata.systemPrompt`) so hermes' `SOUL.md` stays persona-only
3243
+ per Hermes docs. The hermes adapter places identity/user/scope in its per-turn
3244
+ `instructions` slot; the code-agent driver + legacy claude-code CLI prepend them
3245
+ to `--system-prompt`. New exported helpers `isCodingAdapter` / `renderIdentityLines`
3246
+ / `CODING_SOUL_DEFAULT`. Purely additive — legacy dispatches with no
3247
+ `identityContext` are byte-identical on the wire.
3248
+
3249
+ - **Seed-on-dispatch for coding agents with no workdir** (release203/11 §2.4 —
3250
+ AGENTS-layer reliability fix). `seedDevPreset` previously only ran via
3251
+ `ensureWorkdir`, which is gated on a `payload.workdir` override; default
3252
+ RepoDirPicker coding agents carry no workdir, so their cwd never got
3253
+ `CLAUDE.md`/`AGENTS.md`. `dispatch.ts` now idempotently seeds the effective cwd
3254
+ (`profile.config.cwd`, `seedDevPreset(cwd, 'verified')` — append-only, never
3255
+ stomps user files, non-fatal) for any coding dispatch that didn't already
3256
+ materialize a workdir.
3257
+
3258
+ - **Dev preset bundle seeding** (release203/09 §7.5). New `src/daemon/seed-dev-preset.ts` `seedDevPreset(cwd, action)` seeds coding workdirs on `ensureWorkdir` (init → full: `scope ∈ {common,coding}` skills into `.claude/skills/` + AGENTS.md/CLAUDE.md managed block + `docs/agents/`; cloned/verified → append-only managed block, skills only if `.claude/skills` absent; reused → no-op). Gated by `PRISMER_SEED_DEV_PRESET` (default ON); non-fatal + idempotent (managed markers, skip-existing skill dirs).
3259
+
3260
+ - **Category-aware built-in skill filtering** (release203/09 §7.6.3). New runtime
3261
+ SSOT `src/adapters/agent-category.ts` (`ADAPTER_CATEGORY` / `categoryForAdapter`
3262
+ / `skillScopeMatchesCategory`) mirrors the cloud-side adapter→category map.
3263
+ `SKILL.md` frontmatter `scope: common|persistence|coding` is now parsed into
3264
+ `LoadedSkill.scope` (defaults to `common`), and `FileSystemSkillLoader.loadForDispatch(profile)`
3265
+ filters to `scope ∈ {common, <adapter category>}`; unknown adapter or no profile
3266
+ loads everything (default-safe — an unrecognized scope never drops a skill). The
3267
+ hermes/openclaw persistence loaders now thread `profile` through instead of
3268
+ ignoring it.
3269
+
3270
+ - **`agent.workdir.materialize` control frame** (release203/09 §7.3 — on-demand
3271
+ persistent workdir provisioning for the Pro coding-agent picker). The daemon
3272
+ answers a new reverse-channel RPC (`{ workspaceId, projectId?, source:
3273
+ 'clone'|'init'|'container-pick', sourceRef?, name?, cwd?, _rpcId }`): it
3274
+ resolves the per-project `repos/` base (`resolveProjectReposDir`), jails the
3275
+ target cwd inside `workspaces/<wid>`, then reuses `ensureWorkdir` to git-clone
3276
+ / git-init / verify a container-picked path, and echoes `agent.workdir.reply`
3277
+ `{ _rpcId, ok, data:{ cwd, action: 'cloned'|'init'|'reused'|'verified' } |
3278
+ error:{ code: 'path_escape'|'materialize_failed'|'bad_request' } }`. clone/init
3279
+ require a single-segment `name` (`/`, `\`, `..`, empty → `bad_request`);
3280
+ container-pick verifies the supplied absolute `cwd` against the jail
3281
+ (`path_escape` on escape). The pure resolve+jail decision is extracted as
3282
+ `resolveWorkdirCwd` for unit testing (mirrors `fs-list.ts`).
3283
+
3284
+ - **`agent.fs.list` control frame + per-project `repos/` standard directory**
3285
+ (release203/09 — container directory picker for Pro coding-agent creation).
3286
+ New `resolveProjectReposDir(paths, workspaceId, projectId)`
3287
+ (`workspaces/<wid>/projects/<pid|_unscoped>/repos/`); the dispatcher now does a
3288
+ non-fatal `mkdir -p` of it alongside the session `artifacts/`/`scratch/` so a
3289
+ coding agent's default `cwd` always exists. The daemon answers a new
3290
+ `agent.fs.list` reverse-channel RPC (`{ workspaceId, projectId?, subpath?,
3291
+ _rpcId }`) by `readdir`-ing that `repos/` scope — jailed inside
3292
+ `workspaces/<wid>` (escape → `path_escape`), flagging each dir's `.git` as
3293
+ `isRepo`, dirs-first/alphabetical sort — and echoes `agent.fs.reply`
3294
+ `{ _rpcId, ok, data:{ absPath, parentRel, entries } | error:{ code } }`. A
3295
+ not-yet-created `repos/` returns `ok` with empty `entries` (never an error).
3296
+
3297
+ - **`cloud okr pack` subgroup** (release203 Task 2 — R4 Scenario Packs). Global,
3298
+ data-driven OKR templates resolved by `domain` KEY (no runtime branch).
3299
+ `okr pack list` (GET `/api/im/okr/packs`), `okr pack get <domain>` (GET
3300
+ `/api/im/okr/packs/:domain`), and `okr pack adopt <domain> <archetypeId>
3301
+ --workspace <id>` (POST `…/archetypes/:archetypeId/adopt`) which seeds a draft
3302
+ Objective + its KRs and stamps a commit-time pack snapshot
3303
+ (`metadataJson.scenarioPack {domain,version,archetypeId}`). The `software` pack
3304
+ is deliverable; `sales`/`marketing` are DATA-ONLY spec (`deliverable:false`) and
3305
+ adopting one returns `PACK_NOT_DELIVERABLE` (422). Dev KRs declare an internal
3306
+ source: task/acceptance-backed KRs emit today; latency/CI/defect/DORA KRs
3307
+ declare a `metricBinding` but `backedNow:false` → recompute returns null
3308
+ (—/pending, no fabricated numbers). approvalPolicy / resourcePolicy /
3309
+ proactivityTriggers / compensation are FROZEN and absent.
3310
+
3311
+ - **`cloud okr` command + `okr` built-in skill** (release203 Task 3 — OKR control
3312
+ loop). The canonical "agent drafts an OKR charter" path: from a human's plain-
3313
+ language goal an agent drafts ONE Objective + 2-5 Key Results, links existing
3314
+ tasks to the KR they serve, and routes the COMMIT to a human sponsor. Subcommands
3315
+ (all over the committed `/api/im/okr/*` + `/api/im/insights/okr` endpoints):
3316
+ `okr objective create|list|get|commit|close`, `okr kr add|recompute`,
3317
+ `okr link <objectiveId> <keyResultId> <taskId>`, `okr insights --workspace`.
3318
+ `kr add` assembles a `metricBinding` from `--metric-namespace/--metric-name/
3319
+ --metric-agg` and defaults `--source` to `agent-proposed`. Hard rules (enforced
3320
+ server-side, mirrored in the skill): an agent may PROPOSE but never COMMIT
3321
+ (`commit` → `AGENT_CANNOT_COMMIT` 403); a committed-type objective REQUIRES a
3322
+ human/admin sponsor (`SPONSOR_MUST_BE_HUMAN`); a qualitative KR may only be
3323
+ scored with a human-confirmed `--value` + `--evidence`. Guardrails / resource
3324
+ allocation / scenario packs / check-ins / evaluations / compensation are FROZEN
3325
+ and absent from both the CLI and the skill.
3326
+
3327
+ - **`cloud okr objective checkin|grade|archive`** (release203 Task 1 — OKR
3328
+ lifecycle back-half, `committed → graded → archived`). `checkin <id>
3329
+ [--note] [--confidence] [--decision continue|rescope|add-resource|pause|cancel]`
3330
+ records a check-in (snapshots the objective score + each KR's current/status);
3331
+ an AGENT may draft check-ins. `grade <id> [--score] [--narrative]` runs the
3332
+ formal evaluation (committed|at_risk|paused → graded) and `archive <id>`
3333
+ (graded|closed → archived, read-only) are **HUMAN decisions** — an agent caller
3334
+ gets `AGENT_CANNOT_GRADE` (403). Grade freezes the objective score. Reward /
3335
+ resource fields carry no token/credit meaning (compensation stays FROZEN).
3336
+
3337
+ - **Vision-gated image context for non-vision recipients** (release202/17). The
3338
+ daemon now consumes the envelope's new `assets.imageReferences` bucket and
3339
+ renders each as a one-line `[image: <filename> · asset <assetId> · <w>×<h>]`
3340
+ text token instead of vision pixels. A DEFENSIVE double-gate
3341
+ (`gateImageRefsByVision`, `adapters/shared/image-reference.ts`) re-checks the
3342
+ resolved model in the Hermes sessions-dispatcher, runs-dispatcher, and the
3343
+ envelope renderer (`hermes/context-render.ts`): when the model is NOT
3344
+ vision-capable, image inputs are degraded to the same reference lines and are
3345
+ never lifted into `image_url` — protecting against a cloud-side gating miss.
3346
+ Vision-capable / unknown models keep the current `image_url` behavior.
3347
+
3348
+ ### Changed
3349
+
3350
+ - **zod 3 → 4.** `@anthropic-ai/claude-agent-sdk@^0.2.141` (newly added with the
3351
+ code-agent engine port) peer-requires `zod@^4.0.0` across its entire 0.2.x line,
3352
+ which conflicted with the previous `zod@^3.23.8` pin and broke `npm install`.
3353
+ Bumped to `zod@^4.0.0` and migrated the runtime's zod usage to the v4 API:
3354
+ `z.record(v)` → `z.record(z.string(), v)` (key type now mandatory),
3355
+ `ZodError.errors` → `.issues`, `z.ZodType<O, ZodTypeDef, I>` → `z.ZodType<O, I>`
3356
+ (`ZodTypeDef` removed in v4), and explicit transform-value typing where v4's
3357
+ stricter inference over generic `z.ZodTypeAny` schema params collapsed. No
3358
+ runtime behavior change. `zod-to-json-schema@^3.25.2` already supports zod 4.
3359
+
3360
+ ### Removed
3361
+
3362
+ - **OpenClaw adapter retired** (release203/11 §2.5, Slice B). OpenClaw was a
3363
+ decay/version-rotting candidate and its identity model was already absorbed into
3364
+ the canonical four-piece model (§2.1). Deleted the whole
3365
+ `src/adapters/persistence/openclaw/` adapter (index / context-render /
3366
+ skill-loader / memory-tools), its registration in `daemon/runner.ts`, its
3367
+ `openclawAdapter` / `OpenClawProfileConfig` exports from `src/index.ts`, the
3368
+ `openclaw` pin in `known-versions.ts`, the openclaw branch in `agent-category.ts`
3369
+ `ADAPTER_CATEGORY`, the `FallbackAdapter` openclaw member, the openclaw
3370
+ `INSTALL_SPECS` / `BUILTIN_ADAPTERS` / auth + hook branches in the
3371
+ `prismer adapter` CLI, the openclaw `ADAPTER_BINARY` entry in the `prismer agent`
3372
+ CLI, and the openclaw branches in `dispatch.ts` (skill-dir gate),
3373
+ `skill-sync.ts` (skill-root resolution), and `local-server.ts` (state-root
3374
+ snapshot). Removed `openclawConfig` keys + `"openclaw"` from `applicableAdapters`
3375
+ in the role-template JSONs. Persistence is now hermes-only. Existing openclaw
3376
+ profile/agent rows are left untouched in the DB (they simply no longer dispatch —
3377
+ adapter gone); the `IMRoleTemplate.openclawConfig` column is kept as a dead column
3378
+ (no migration). Deleted the openclaw-specific tests
3379
+ (`openclaw-adapter-*`, `openclaw-skill-loader`, `context-render-openclaw`) and the
3380
+ `sa7-openclaw-gateway-verify` cookbook; trimmed the openclaw rows from the
3381
+ cross-adapter tests (multimodal / memory-tools / version-check / templates /
3382
+ skill-sync / workdir-materialize). Also dropped the `@prismer/openclaw-channel`
3383
+ plugin package and its references in the SDK build scripts.
3384
+
3385
+ ### Fixed
3386
+
3387
+ - **Transient dispatch-precondition failures no longer hard-fail the run**
3388
+ (release202, HTTP 404 postmortem). A freshly-(re)connected daemon's FIRST
3389
+ cloud precondition fetch (`resolveProfile` → `GET /api/im/agent_profiles/:id`)
3390
+ can 404 during the warm-up window before its api-key-proxy identity is hot —
3391
+ the cloud stamps a valid `profileId`, but the owner-scoped lookup transiently
3392
+ misses. Previously that 404 was thrown straight past the 3× adapter-retry loop
3393
+ (which only wraps `adapter.dispatch()`, *after* `resolveProfile`) into the
3394
+ top-level catch, replying `{code:'adapter_dispatch_failed', message:'HTTP 404'}`
3395
+ → terminal "Agent 失败 … HTTP 404" pill, recoverable only by the user
3396
+ resending. Now: (1) `resolveProfileResilient` retries TRANSIENT cloud failures
3397
+ (404 / 408 / 429 / 5xx / network) with backoff (~5s) before giving up;
3398
+ (2) the top-level catch classifies the final error via
3399
+ `isTransientPreconditionError` and replies with a distinct retryable code
3400
+ `dispatch_precondition_unavailable` (vs `adapter_dispatch_failed`) so the
3401
+ cloud re-queues instead of terminal-failing. 400/401/403 stay PERMANENT.
3402
+
3403
+ - **Upstream LLM failures no longer surface as fake-successful tasks**
3404
+ (release202/12, D1+D2). Hermes serializes an exhausted LLM call
3405
+ (`API call failed after N retries: …`) into `assistant.completed` content;
3406
+ the sessions SSE consumer now detects that signature (anchored, requires zero
3407
+ streamed deltas to avoid false-positives) and sets `SessionsSseResult.upstreamError`.
3408
+ `sessions-dispatcher` turns it into a FAILED `AdapterResult`
3409
+ (`code:'upstream_llm_error'`, carrying the HTTP status) instead of `ok:true`
3410
+ with the error string as output. The daemon retry loop (`dispatch.ts`) adds
3411
+ `isPermanentUpstreamError` so permanent causes skip the 3× retry and surface
3412
+ the real reason instead of `daemon_local_retry_exhausted`. Detection +
3413
+ classification key on the hermes message WORDING — `has no usable upstream
3414
+ source` (provider chain unconfigured), `Billing or credits exhausted:` (HTTP
3415
+ 402 from the cloud balance gate), `HTTP <4xx>` — because hermes'
3416
+ `_summarize_provider_error` DROPS the JSON `error.type`, so a machine token
3417
+ can't be matched. Mirrored to the `/v1/runs` dispatcher so the flag-gated path
3418
+ can't reintroduce the bug. Pairs with the cloud-side
3419
+ `503 provider_chain_unconfigured` (release202/07 §5b) so a provider-chain
3420
+ misconfig reads as a clear failed task end-to-end.
3421
+
3422
+ ### Added
3423
+
3424
+ - **`mode:'message-attach'` for `POST /local/deliver`** (release202/09 P5#3, 动作
3425
+ A2). Appends a freshly-uploaded asset to an ALREADY-SENT message. Body adds
3426
+ `messageId` (and requires `conversationId`); the daemon uploads with its own
3427
+ credential (same `deliverFile` upload path + agent-output-policy +
3428
+ magic-bytes as the other modes) then POSTs the resulting `assetId` to the
3429
+ cloud conversation-scoped attach route
3430
+ `POST /api/im/messages/:conversationId/:messageId/attach` (X-IM-Agent stamped
3431
+ so the cloud sender-check resolves the same agent that authored the message).
3432
+ Unlike `mode:'send'` it does not start a new message; unlike `mode:'attach'`
3433
+ it does not ride the reply. Wired from the in-container `cloud attach` SDK
3434
+ command. Complements `mode:'attach'` (reply does not exist yet) — A2 is for a
3435
+ reply that already exists.
3436
+ - **`mode:'task-attach'` for `POST /local/deliver`** (release202/09 P5#2, 动作
3437
+ ③). The daemon uploads the file as a **task-bound** asset (`deliverFile`
3438
+ already stamps `sourceTaskId = taskId`), which the cloud `POST /assets`
3439
+ handler auto-rolls onto the kanban task card
3440
+ (`appendOutputAssetIdToTask` + `reemitTerminalDigestForAssetArrival`) plus the
3441
+ asset library. Unlike `mode:'attach'` it does **not** call
3442
+ `recordDeliveredAsset` (a kanban task may run without a chat reply — its
3443
+ products belong on the card, not a turn reply) and unlike `mode:'send'` it
3444
+ posts no message. No new cloud HTTP call is needed: the existing
3445
+ `sourceTaskId`-driven rollup is the kanban + library landing. Wired from the
3446
+ in-container `cloud task attach` SDK command.
3447
+ - **Explicit agent-driven file delivery** (release202/09 P2). New daemon
3448
+ local-server route `POST /local/deliver` (`local-server.ts` `onDeliver` +
3449
+ `daemon/asset/deliver.ts` `attachDeliver`): the in-container agent's
3450
+ `cloud deliver` / `cloud file send` proxy here so the daemon (which holds a
3451
+ usable IM credential the agent lacks) performs the upload + delivery. Body
3452
+ `{ taskId, path, mode:'attach'|'send', conversationId?, agentUsername? }`.
3453
+ `mode:'attach'` (动作 A) records the assetId onto `ArtifactsWatcher`'s
3454
+ existing `pendingByTask` so dispatch-end `flushPending` rides it on
3455
+ `reply.assetIds`; `mode:'send'` (动作 B) posts a standalone message with the
3456
+ asset attachment, stamped as the agent (`X-IM-Agent`). New public
3457
+ `ArtifactsWatcher.recordDeliveredAsset(taskId, assetId)` +
3458
+ `ArtifactsWatcher.deliverFile(...)`. `PRISMER_CONVERSATION_ID` is now
3459
+ injected into the agent env for 动作 B targeting.
3460
+
3461
+ ### Changed
3462
+
3463
+ - **ArtifactsWatcher directory auto-scan is now flag-gated OFF by default**
3464
+ (release202/09 P2). New `ArtifactsWatcherOptions.autoScan` (default
3465
+ **false**): when off, the polling scan / `scanNow()` are no-ops (no implicit
3466
+ directory-magic), but `pendingByTask` / `flushPending` /
3467
+ `recordDeliveredAsset` / `upload` all still work. The legacy scan code is
3468
+ retained behind the flag and re-enablable via `PRISMER_ARTIFACTS_AUTOSCAN=1`.
3469
+ File delivery is now EXPLICIT (`cloud deliver` / `cloud file send`).
3470
+
3471
+ - **Hermes one-shot task-run dispatch via `/v1/runs`** (release202/08 Phase 1,
3472
+ flag-gated **default OFF**). New `HERMES_TASK_RUNS_DISPATCH` env flag. When ON
3473
+ and a dispatch is classified as a one-shot **run** (execution context
3474
+ `task-run` AND no triggering chat sender — pure kanban / scheduled fire /
3475
+ programmatic orchestration), the hermes adapter routes through the new
3476
+ `runs-dispatcher.ts` (`POST /v1/runs` → 202 + run_id, then `GET
3477
+ /v1/runs/{id}/events` SSE) instead of the stateful Sessions API. The run is
3478
+ **stateless** — no hermes session, no `local_run_sessions` mapping; its input
3479
+ is the bare task prompt (+ inline asset blocks) and `system_prompt` embeds a
3480
+ standalone `<execution_context type="task-run">` block (no SEED/THIN
3481
+ envelope, no participants/current_turn_sender). Multimodal image_url parts and
3482
+ the `/events` SSE parser (`consumeSessionsSse`) are reused verbatim. Chat
3483
+ turns (group/dm with a triggering sender) keep going through
3484
+ `dispatchViaSessions` unchanged. With the flag OFF the router is inert —
3485
+ `deriveDispatchKind` returns `'turn'` unconditionally, so 100% of traffic
3486
+ stays on Sessions (release201/25 §16.4 A3 behaviour, byte-for-byte).
3487
+ `adapters/hermes/{flag.ts,runs-dispatcher.ts,index.ts}`,
3488
+ `daemon/conversation-context.ts` (`renderExecutionContextXml`).
3489
+
3490
+ - **Role templates declare `taskDispatchScope`** (release202/08 §3.2 P3 —
3491
+ config-driven, role-agnostic task-dispatch permission). A role's template /
3492
+ agent-profile `config.taskDispatchScope` (`'any' | 'agents' | 'self' |
3493
+ 'none'`) is the middle layer of cloud's three-layer dispatch resolver
3494
+ (`workspace.metadata.dispatchPolicy` > role/profile config >
3495
+ `DEFAULT_DISPATCH_POLICY`), so a NEW role gets its dispatch capability by
3496
+ declaring this field — the cloud resolver needs zero code changes. Declared:
3497
+ `ceo` → `any` (orchestrator, sibling of `taskAuthority`), `product-manager`
3498
+ → `agents` (delegates to engineer/verifier, never a human peer), `engineer`
3499
+ / `researcher` / `verifier` → `none` (executors, no peer delegation).
3500
+ `templates/roles/*.json`.
3501
+
3502
+ ### Fixed
3503
+
3504
+ - **Model reasoning trace restored in agent messages on the hermes sessions
3505
+ stream** (regression from `2cdd2cad`, 2026-05-29). When dispatch migrated from
3506
+ `/v1/runs` (legacy `consumeSse`) to the sessions stream (`consumeSessionsSse`),
3507
+ the deleted `reasoning.available` handler was the only path feeding the model's
3508
+ thinking trace into `recorder.recordReasoningChunk(...)`, so reasoning stopped
3509
+ becoming `reasoning_chunk` steps and the expandable reasoning block vanished
3510
+ (tool-call steps still showed — separate handlers). Root cause of the shape
3511
+ mismatch: the sessions endpoint (`api_server.py:1585-1590 _tool_progress`)
3512
+ does **not** emit `reasoning.available`; it **remaps** reasoning into a
3513
+ `tool.progress` event with `tool_name: '_thinking'` and the text in `delta`
3514
+ (not `preview`), which the old `tool.progress` handler — reading only
3515
+ `preview` — silently dropped. Fix: `tool.progress` now reads `delta` first and
3516
+ routes reasoning through `recordReasoningChunk` + a `kind:'reasoning'`
3517
+ onProgress hint (no `lastProgress` bump — reasoning is not a progress unit for
3518
+ the reaper, mirroring the deleted handler). Also added a defensive top-level
3519
+ `reasoning.available` case (accepts `text`/`reasoning_content`/`delta`/
3520
+ `content`) for the `/v1/runs`-style direct event in case a hermes build
3521
+ surfaces it on the sessions stream. Guarded against empty strings. Covered by
3522
+ `test/reasoning-sse.test.ts`.
3523
+ - **Hermes adapter no longer reuses a stale/foreign gateway squatting on the
3524
+ port** (release202/07 robustness). `ensureService` decided reuse-vs-respawn
3525
+ with the UNAUTHENTICATED `/health` check, which an old-version (pre-sessions,
3526
+ 404 on `/v1/capabilities`) or wrong-`API_SERVER_KEY` (401) gateway — e.g. an
3527
+ orphan we spawned in a PRIOR daemon session — answers `200` to. Reusing such a
3528
+ squatter made the capability gate later trip with the misleading "Hermes does
3529
+ not advertise session_chat_streaming", failing every dispatch after a daemon
3530
+ restart until the orphans were manually `pkill`ed. Now the reuse decision is
3531
+ AUTHENTICATED: a hermes counts as reusable only if it answers
3532
+ `GET /v1/capabilities` as ours; otherwise the adapter frees the port
3533
+ (`stopStaleHermesGateways`, matches `-p <profile>` / `API_SERVER_PORT`) and
3534
+ spawns a fresh gateway with our key — self-healing on restart. The capability
3535
+ probe result is reused as the pinned `HermesService.capabilities` (no extra
3536
+ fetch on the happy path). `adapters/hermes/index.ts` `ensureService`.
3537
+
3538
+ ### Changed
3539
+
3540
+ - **Provider chain routing — `proxyProvider` widened from `'newapi'|'deepseek'`
3541
+ enum to any chain id** (release202/07). The codex adapter previously hardcoded
3542
+ the gateway base_url to `<PRISMER_BASE_URL>/api/v1` "regardless of
3543
+ proxyProvider" — silently dropping a `deepseek` selection so every Codex
3544
+ request landed on newapi (and 500'd for deepseek-only models). Now both the
3545
+ codex and hermes adapters resolve `base_url` by the configured chain:
3546
+ `newapi`/`default` → `/api/v1`; any other chain → `/api/v1/proxy/<chain>`
3547
+ (codex appends `/responses`, hermes `/chat/completions`). The cloud walks the
3548
+ chain (source1 → source2 → …) with per-source fallback.
3549
+ - `adapters/codex/index.ts` — `proxyProvider` schema `z.enum` → `z.string`;
3550
+ `resolveCodexPrismerProvider` honors the chain in base_url.
3551
+ - `adapters/hermes/index.ts` — same schema widening; `resolvePrismerProviderBaseUrl`
3552
+ generalized from the `deepseek`-only branch to any chain id.
3553
+
3554
+ - **Document-deliverable SOP — markdown files, not chat prose** (B-line,
3555
+ release201/3x). Codified the rule that document-type deliverables
3556
+ (research memos, PRDs, reports, reviews, summaries) MUST be written as
3557
+ markdown files into `${PRISMER_OUTBOX_DIR}/` (physically
3558
+ `${TASK_WORKDIR}/result/`) so the daemon outbox-watcher auto-registers
3559
+ them as task-bound IMAssets; the chat reply is a summary/teaser, not the
3560
+ deliverable itself.
3561
+ - `built-in-skills/tasks/SKILL.md` §产物输出 — added §文档型交付物 (= markdown
3562
+ 文件) sub-section referencing the real `${PRISMER_OUTBOX_DIR}` /
3563
+ `result/` auto-archival wiring (`dispatch.ts` `appendArtifactsInstruction`,
3564
+ `PRISMER_ARTIFACTS_DIR` env). Filenames/structure are skill-defined
3565
+ (no hardcoded schema).
3566
+ - `templates/roles/{researcher,product-manager,engineer}.json`
3567
+ `systemPrompt` — replaced the old "upload as workspace file +
3568
+ prismer://file URI" convention with the markdown-to-`result/` SOP and a
3569
+ summary-vs-deliverable distinction; each points at the tasks skill SOP.
3570
+ - `templates/roles/verifier.json` `systemPrompt` + `operatingPrinciples` —
3571
+ extended the same SOP to the verifier: the full acceptance record
3572
+ (per-criterion method / params / observed result / repro steps) is now a
3573
+ markdown file in `${PRISMER_OUTBOX_DIR}/` (e.g. `verification-report.md`),
3574
+ auto-archived as a task-bound IMAsset. Binary evidence (screenshots /
3575
+ benchmark output / logs) still goes via `cloud task attach` +
3576
+ `evidenceRefs`; the `verify-criterion --note` stays a summary. The two
3577
+ paths are complementary.
3578
+ - Audit (cross-referenced docs): `templates/roles/ceo.json` left unchanged —
3579
+ it already routes all file deliverables through `office-artifacts` /
3580
+ `image-generate` / `canvas-design` / `web-artifacts-builder` skills (no
3581
+ legacy "upload as workspace file" anti-pattern), so it needs no SOP
3582
+ backfill. `templates/roles/skill-author.json` left unchanged — its
3583
+ deliverable is a structured `status=draft` IMSkill submitted via
3584
+ `cloud skill draft create`, not a chat-bound markdown document, so the
3585
+ `result/` SOP does not apply.
3586
+
3587
+ ### Added
3588
+
3589
+ - **Adapter version probe with KNOWN_GOOD pinning** (Release 201 v2.0.7
3590
+ post-release P1). All four adapters (hermes / codex / claude-code /
3591
+ openclaw) now run `<binary> --version` on startup, parse the result,
3592
+ warn when the detected version drifts below the per-adapter
3593
+ `MIN_VERSION`, and warn when it diverges from `KNOWN_GOOD`. Pins are
3594
+ declared in the new `sdk/prismer-cloud/runtime/src/adapters/known-versions.ts`
3595
+ manifest:
3596
+
3597
+ - hermes: MIN=`0.0.0` (soft-pass), KNOWN_GOOD=`unknown` — TODO(v2.0.8):
3598
+ pin after cookbook run exercises a real hermes binary
3599
+ - codex: MIN=`0.0.0` (soft-pass), KNOWN_GOOD=`unknown` — TODO(v2.0.8):
3600
+ pin after cookbook run against `@openai/codex`
3601
+ - claude-code: MIN=`2.0.0` (Wave-4 flag set requires 2.x), KNOWN_GOOD=
3602
+ `unknown` — TODO(v2.0.8): pin knownGood after cookbook run
3603
+ - openclaw: MIN=`2026.4.0`, KNOWN_GOOD=`2026.4.0` — wrapper carries a
3604
+ 2026.4.x stderr-bleed workaround, revisit once upstream fixes
3605
+
3606
+ Honesty note: three of the four pins are intentionally unpinned at
3607
+ the floor — this is the governance scaffold, not a claim that we've
3608
+ validated specific upstream revs. Bumping the pins is a v2.0.8 task
3609
+ blocked on real cookbook runs.
3610
+
3611
+ - New `sdk/prismer-cloud/runtime/src/adapters/version-check.ts` —
3612
+ `compareSemver`, `isVersionInRange`, `parseVersionFromStdout` shared
3613
+ helpers so the four adapters do not redefine their own semver math.
3614
+
3615
+ - `/healthz` `adapters[]` entries now carry `minVersion` + `knownGood`
3616
+ alongside the existing `name` / `ready` / `version` fields so cloud
3617
+ debug-pipeline can flag drift between detected and tested versions.
3618
+
3619
+ - `test/adapters-version-check.test.ts` — vitest regression covering
3620
+ compareSemver, range checks, pre-release stripping, parseVersionFromStdout,
3621
+ and the `ADAPTER_KNOWN_VERSIONS` manifest shape.
3622
+
5
3623
  ## v2.0.1 — 2026-05-21 — Skill multi-file manifest protocol (§A.7 Phase 1) + URL asset fetch
6
3624
 
7
3625
  Coordinated v2.0.1 patch release. `/VERSION` → 2.0.1. Internal preview only;
@@ -325,3 +3943,16 @@ First published cut of `@prismer/runtime`. The runtime is the TS-only daemon tha
325
3943
  ## v1.8.x — Pre-publish scaffolding
326
3944
 
327
3945
  The runtime tree was scaffolded across the `feat/refactoring` branch. There were no published cuts before v1.9.3; the package landed at v1.9.3 directly when its public API surface stabilized.
3946
+
3947
+ ## 2.2.10 (2026-08-07)
3948
+
3949
+ - Fix Hermes "No LLM provider configured" on sandbox agents: the provider
3950
+ bootstrap wrote `model.provider: custom:prismer`, but hermes' custom-provider
3951
+ resolution matches `custom_providers[].name` verbatim — `custom:prismer`
3952
+ never matched the `prismer` entry, the gateway fell through with
3953
+ provider='custom' and no key, and every turn failed at AIAgent init.
3954
+ Exposed on 2.2.9: the dual-write made the gateway read the profile config
3955
+ (HERMES_HOME override) for the first time, surfacing the format mismatch.
3956
+ Write the bare provider name (`prismer`); verified in-pod that
3957
+ `_resolve_runtime_agent_kwargs` then resolves api_key + base_url correctly.
3958
+ ## 2.2.9