@downcity/agent 1.1.207 → 1.1.210

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 (292) hide show
  1. package/bin/agent/local/Agent.d.ts +3 -3
  2. package/bin/agent/local/Agent.d.ts.map +1 -1
  3. package/bin/agent/local/Agent.js +0 -3
  4. package/bin/agent/local/Agent.js.map +1 -1
  5. package/bin/agent/local/AgentInstructions.d.ts +2 -1
  6. package/bin/agent/local/AgentInstructions.d.ts.map +1 -1
  7. package/bin/agent/local/AgentInstructions.js +10 -10
  8. package/bin/agent/local/AgentInstructions.js.map +1 -1
  9. package/bin/agent/local/services/AgentAssemblyService.d.ts +3 -8
  10. package/bin/agent/local/services/AgentAssemblyService.d.ts.map +1 -1
  11. package/bin/agent/local/services/AgentAssemblyService.js +0 -1
  12. package/bin/agent/local/services/AgentAssemblyService.js.map +1 -1
  13. package/bin/agent/local/services/AgentSessions.d.ts +5 -7
  14. package/bin/agent/local/services/AgentSessions.d.ts.map +1 -1
  15. package/bin/agent/local/services/AgentSessions.js +2 -3
  16. package/bin/agent/local/services/AgentSessions.js.map +1 -1
  17. package/bin/agent/remote/RemoteAgent.d.ts +3 -1
  18. package/bin/agent/remote/RemoteAgent.d.ts.map +1 -1
  19. package/bin/agent/remote/RemoteAgent.js.map +1 -1
  20. package/bin/agent/remote/RemoteSession.d.ts +10 -1
  21. package/bin/agent/remote/RemoteSession.d.ts.map +1 -1
  22. package/bin/agent/remote/RemoteSession.js +18 -0
  23. package/bin/agent/remote/RemoteSession.js.map +1 -1
  24. package/bin/agent/remote/RemoteTransport.d.ts +5 -1
  25. package/bin/agent/remote/RemoteTransport.d.ts.map +1 -1
  26. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts +4 -1
  27. package/bin/agent/remote/transports/HttpRemoteAgentTransport.d.ts.map +1 -1
  28. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js +7 -0
  29. package/bin/agent/remote/transports/HttpRemoteAgentTransport.js.map +1 -1
  30. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts +4 -1
  31. package/bin/agent/remote/transports/RpcRemoteAgentTransport.d.ts.map +1 -1
  32. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js +1 -0
  33. package/bin/agent/remote/transports/RpcRemoteAgentTransport.js.map +1 -1
  34. package/bin/executor/Executor.d.ts +0 -16
  35. package/bin/executor/Executor.d.ts.map +1 -1
  36. package/bin/executor/Executor.js +0 -16
  37. package/bin/executor/Executor.js.map +1 -1
  38. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts +1 -3
  39. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.d.ts.map +1 -1
  40. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js +1 -5
  41. package/bin/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.js.map +1 -1
  42. package/bin/executor/composer/history/SessionHistoryWriter.d.ts +0 -10
  43. package/bin/executor/composer/history/SessionHistoryWriter.d.ts.map +1 -1
  44. package/bin/executor/composer/history/SessionHistoryWriter.js +31 -61
  45. package/bin/executor/composer/history/SessionHistoryWriter.js.map +1 -1
  46. package/bin/executor/composer/system/default/StaticPromptCatalog.d.ts +4 -0
  47. package/bin/executor/composer/system/default/StaticPromptCatalog.d.ts.map +1 -1
  48. package/bin/executor/composer/system/default/StaticPromptCatalog.js +4 -0
  49. package/bin/executor/composer/system/default/StaticPromptCatalog.js.map +1 -1
  50. package/bin/executor/composer/system/default/SystemDomain.d.ts +1 -1
  51. package/bin/executor/composer/system/default/SystemDomain.d.ts.map +1 -1
  52. package/bin/executor/composer/system/default/SystemDomain.js +2 -7
  53. package/bin/executor/composer/system/default/SystemDomain.js.map +1 -1
  54. package/bin/executor/composer/system/default/SystemPromptAssets.d.ts +0 -4
  55. package/bin/executor/composer/system/default/SystemPromptAssets.d.ts.map +1 -1
  56. package/bin/executor/composer/system/default/SystemPromptAssets.js +0 -5
  57. package/bin/executor/composer/system/default/SystemPromptAssets.js.map +1 -1
  58. package/bin/executor/composer/system/default/assets/core.prompt.d.ts +1 -1
  59. package/bin/executor/composer/system/default/assets/core.prompt.d.ts.map +1 -1
  60. package/bin/executor/composer/system/default/assets/core.prompt.js +1 -1
  61. package/bin/executor/composer/system/default/assets/core.prompt.js.map +1 -1
  62. package/bin/executor/messages/AssistantFileResource.d.ts.map +1 -1
  63. package/bin/executor/messages/AssistantFileResource.js +58 -15
  64. package/bin/executor/messages/AssistantFileResource.js.map +1 -1
  65. package/bin/executor/services/ExecutorInflightService.d.ts +0 -5
  66. package/bin/executor/services/ExecutorInflightService.d.ts.map +1 -1
  67. package/bin/executor/services/ExecutorInflightService.js +0 -6
  68. package/bin/executor/services/ExecutorInflightService.js.map +1 -1
  69. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts +4 -0
  70. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.d.ts.map +1 -1
  71. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js +115 -3
  72. package/bin/executor/store/history/jsonl/JsonlSessionHistoryStore.js.map +1 -1
  73. package/bin/executor/types/SessionHistoryMeta.d.ts +6 -0
  74. package/bin/executor/types/SessionHistoryMeta.d.ts.map +1 -1
  75. package/bin/index.d.ts +38 -6
  76. package/bin/index.d.ts.map +1 -1
  77. package/bin/index.js +14 -1
  78. package/bin/index.js.map +1 -1
  79. package/bin/plugin/core/ActionScheduleExecutor.js +1 -1
  80. package/bin/plugin/core/ActionScheduleExecutor.js.map +1 -1
  81. package/bin/plugin/core/BasePlugin.d.ts +7 -9
  82. package/bin/plugin/core/BasePlugin.d.ts.map +1 -1
  83. package/bin/plugin/core/BasePlugin.js +1 -14
  84. package/bin/plugin/core/BasePlugin.js.map +1 -1
  85. package/bin/plugin/core/HookRegistry.d.ts +1 -1
  86. package/bin/plugin/core/HookRegistry.d.ts.map +1 -1
  87. package/bin/plugin/core/PluginActionFactory.d.ts +1 -5
  88. package/bin/plugin/core/PluginActionFactory.d.ts.map +1 -1
  89. package/bin/plugin/core/PluginActionFactory.js +0 -3
  90. package/bin/plugin/core/PluginActionFactory.js.map +1 -1
  91. package/bin/plugin/core/PluginActionRunner.d.ts +6 -4
  92. package/bin/plugin/core/PluginActionRunner.d.ts.map +1 -1
  93. package/bin/plugin/core/PluginActionRunner.js +2 -2
  94. package/bin/plugin/core/PluginActionRunner.js.map +1 -1
  95. package/bin/plugin/core/PluginCatalog.d.ts +2 -1
  96. package/bin/plugin/core/PluginCatalog.d.ts.map +1 -1
  97. package/bin/plugin/core/PluginCatalog.js.map +1 -1
  98. package/bin/plugin/core/PluginCommand.d.ts +1 -1
  99. package/bin/plugin/core/PluginCommand.d.ts.map +1 -1
  100. package/bin/plugin/core/PluginCommand.js.map +1 -1
  101. package/bin/plugin/core/PluginHttpRoutes.d.ts +1 -1
  102. package/bin/plugin/core/PluginHttpRoutes.d.ts.map +1 -1
  103. package/bin/plugin/core/PluginLocalExecution.d.ts +4 -1
  104. package/bin/plugin/core/PluginLocalExecution.d.ts.map +1 -1
  105. package/bin/plugin/core/PluginLocalExecution.js +0 -1
  106. package/bin/plugin/core/PluginLocalExecution.js.map +1 -1
  107. package/bin/plugin/core/PluginRegistry.d.ts +4 -2
  108. package/bin/plugin/core/PluginRegistry.d.ts.map +1 -1
  109. package/bin/plugin/core/PluginRegistry.js +7 -6
  110. package/bin/plugin/core/PluginRegistry.js.map +1 -1
  111. package/bin/plugin/core/PluginStateController.d.ts +7 -27
  112. package/bin/plugin/core/PluginStateController.d.ts.map +1 -1
  113. package/bin/plugin/core/PluginStateController.js +5 -39
  114. package/bin/plugin/core/PluginStateController.js.map +1 -1
  115. package/bin/plugin/types/PluginApi.d.ts +3 -79
  116. package/bin/plugin/types/PluginApi.d.ts.map +1 -1
  117. package/bin/rpc/Client.d.ts +11 -6
  118. package/bin/rpc/Client.d.ts.map +1 -1
  119. package/bin/rpc/Client.js +2 -0
  120. package/bin/rpc/Client.js.map +1 -1
  121. package/bin/session/Session.d.ts +2 -5
  122. package/bin/session/Session.d.ts.map +1 -1
  123. package/bin/session/Session.js +3 -16
  124. package/bin/session/Session.js.map +1 -1
  125. package/bin/session/SessionSystemBuilder.d.ts +1 -1
  126. package/bin/session/SessionSystemBuilder.d.ts.map +1 -1
  127. package/bin/session/browse/Browse.d.ts +4 -6
  128. package/bin/session/browse/Browse.d.ts.map +1 -1
  129. package/bin/session/browse/Browse.js +71 -71
  130. package/bin/session/browse/Browse.js.map +1 -1
  131. package/bin/session/preview/SessionMessagePreview.d.ts +13 -0
  132. package/bin/session/preview/SessionMessagePreview.d.ts.map +1 -0
  133. package/bin/session/preview/SessionMessagePreview.js +61 -0
  134. package/bin/session/preview/SessionMessagePreview.js.map +1 -0
  135. package/bin/session/services/SessionStateService.d.ts +1 -1
  136. package/bin/session/services/SessionStateService.d.ts.map +1 -1
  137. package/bin/session/services/SessionStateService.js +2 -2
  138. package/bin/session/services/SessionStateService.js.map +1 -1
  139. package/bin/session/services/SessionViewService.d.ts +2 -1
  140. package/bin/session/services/SessionViewService.d.ts.map +1 -1
  141. package/bin/session/services/SessionViewService.js +4 -1
  142. package/bin/session/services/SessionViewService.js.map +1 -1
  143. package/bin/session/storage/Metadata.d.ts.map +1 -1
  144. package/bin/session/storage/Metadata.js +23 -0
  145. package/bin/session/storage/Metadata.js.map +1 -1
  146. package/bin/session/storage/Persistence.d.ts +1 -1
  147. package/bin/session/storage/Persistence.d.ts.map +1 -1
  148. package/bin/session/storage/RuntimeSessionPort.d.ts +0 -12
  149. package/bin/session/storage/RuntimeSessionPort.d.ts.map +1 -1
  150. package/bin/session/storage/RuntimeSessionPort.js +0 -8
  151. package/bin/session/storage/RuntimeSessionPort.js.map +1 -1
  152. package/bin/types/plugin/PluginAction.d.ts +0 -10
  153. package/bin/types/plugin/PluginAction.d.ts.map +1 -1
  154. package/bin/types/plugin/PluginCommand.d.ts +2 -3
  155. package/bin/types/plugin/PluginCommand.d.ts.map +1 -1
  156. package/bin/types/plugin/PluginControl.d.ts +74 -0
  157. package/bin/types/plugin/PluginControl.d.ts.map +1 -0
  158. package/bin/types/plugin/PluginControl.js +9 -0
  159. package/bin/types/plugin/PluginControl.js.map +1 -0
  160. package/bin/types/plugin/PluginDefinition.d.ts +1 -1
  161. package/bin/types/plugin/PluginDefinition.d.ts.map +1 -1
  162. package/bin/types/plugin/PluginRuntime.d.ts +1 -3
  163. package/bin/types/plugin/PluginRuntime.d.ts.map +1 -1
  164. package/bin/types/plugin/PluginState.d.ts +3 -16
  165. package/bin/types/plugin/PluginState.d.ts.map +1 -1
  166. package/bin/types/rpc/RpcProtocol.d.ts +3 -3
  167. package/bin/types/rpc/RpcProtocol.d.ts.map +1 -1
  168. package/bin/types/runtime/agent/AgentContext.d.ts +1 -9
  169. package/bin/types/runtime/agent/AgentContext.d.ts.map +1 -1
  170. package/bin/types/runtime/agent/AgentContext.js.map +1 -1
  171. package/bin/types/session/SessionLocalState.d.ts +1 -1
  172. package/bin/types/session/SessionLocalState.d.ts.map +1 -1
  173. package/package.json +4 -7
  174. package/scripts/agent-ready.test.mjs +38 -0
  175. package/scripts/assistant-file-resource.test.mjs +39 -0
  176. package/scripts/city-model-tool-loop.test.mjs +18 -30
  177. package/scripts/image-plugin-job.test.mjs +13 -13
  178. package/scripts/plugin-tool-bridge.test.mjs +2 -3
  179. package/scripts/remote-session-reconnect.test.mjs +67 -0
  180. package/scripts/session-history-writer-error.test.mjs +57 -0
  181. package/scripts/session-list-title.test.mjs +114 -23
  182. package/scripts/session-system-blocks.test.mjs +73 -0
  183. package/src/agent/local/Agent.ts +4 -10
  184. package/src/agent/local/AgentInstructions.ts +11 -11
  185. package/src/agent/local/services/AgentAssemblyService.ts +3 -9
  186. package/src/agent/local/services/AgentSessions.ts +8 -15
  187. package/src/agent/remote/RemoteAgent.ts +5 -4
  188. package/src/agent/remote/RemoteSession.ts +21 -2
  189. package/src/agent/remote/RemoteTransport.ts +6 -2
  190. package/src/agent/remote/transports/HttpRemoteAgentTransport.ts +13 -3
  191. package/src/agent/remote/transports/RpcRemoteAgentTransport.ts +6 -2
  192. package/src/executor/Executor.ts +0 -23
  193. package/src/executor/composer/compaction/jsonl/JsonlSessionCompactionExecutor.ts +2 -8
  194. package/src/executor/composer/history/SessionHistoryWriter.ts +33 -64
  195. package/src/executor/composer/system/default/StaticPromptCatalog.ts +4 -0
  196. package/src/executor/composer/system/default/SystemDomain.ts +1 -9
  197. package/src/executor/composer/system/default/SystemPromptAssets.ts +0 -6
  198. package/src/executor/composer/system/default/assets/core.prompt.ts +1 -1
  199. package/src/executor/composer/system/default/assets/core.prompt.ts.txt +37 -15
  200. package/src/executor/messages/AssistantFileResource.ts +61 -17
  201. package/src/executor/services/ExecutorInflightService.ts +0 -10
  202. package/src/executor/store/history/jsonl/JsonlSessionHistoryStore.ts +130 -4
  203. package/src/executor/types/SessionHistoryMeta.ts +6 -0
  204. package/src/index.ts +100 -29
  205. package/src/plugin/core/ActionScheduleExecutor.ts +1 -1
  206. package/src/plugin/core/BasePlugin.ts +8 -22
  207. package/src/plugin/core/HookRegistry.ts +1 -1
  208. package/src/plugin/core/PluginActionFactory.ts +1 -8
  209. package/src/plugin/core/PluginActionRunner.ts +8 -11
  210. package/src/plugin/core/PluginCatalog.ts +2 -5
  211. package/src/plugin/core/PluginCommand.ts +2 -1
  212. package/src/plugin/core/PluginHttpRoutes.ts +1 -1
  213. package/src/plugin/core/PluginLocalExecution.ts +4 -7
  214. package/src/plugin/core/PluginRegistry.ts +10 -10
  215. package/src/plugin/core/PluginStateController.ts +12 -60
  216. package/src/plugin/types/PluginApi.ts +6 -81
  217. package/src/rpc/Client.ts +22 -16
  218. package/src/session/Session.ts +5 -21
  219. package/src/session/SessionSystemBuilder.ts +1 -1
  220. package/src/session/browse/Browse.ts +103 -84
  221. package/src/session/preview/SessionMessagePreview.ts +75 -0
  222. package/src/session/services/SessionStateService.ts +3 -4
  223. package/src/session/services/SessionViewService.ts +6 -6
  224. package/src/session/storage/Metadata.ts +26 -0
  225. package/src/session/storage/Persistence.ts +1 -1
  226. package/src/session/storage/RuntimeSessionPort.ts +0 -20
  227. package/src/types/plugin/PluginAction.ts +0 -10
  228. package/src/types/plugin/PluginCommand.ts +2 -3
  229. package/src/types/plugin/PluginControl.ts +80 -0
  230. package/src/types/plugin/PluginDefinition.ts +1 -1
  231. package/src/types/plugin/PluginRuntime.ts +1 -3
  232. package/src/types/plugin/PluginState.ts +3 -20
  233. package/src/types/rpc/RpcProtocol.ts +4 -4
  234. package/src/types/runtime/agent/AgentContext.ts +1 -9
  235. package/src/types/session/SessionLocalState.ts +1 -1
  236. package/tsconfig.tsbuildinfo +1 -1
  237. package/bin/config/DowncitySchema.d.ts +0 -20
  238. package/bin/config/DowncitySchema.d.ts.map +0 -1
  239. package/bin/config/DowncitySchema.js +0 -160
  240. package/bin/config/DowncitySchema.js.map +0 -1
  241. package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts +0 -7
  242. package/bin/executor/composer/system/default/assets/plugin.prompt.d.ts.map +0 -1
  243. package/bin/executor/composer/system/default/assets/plugin.prompt.js +0 -8
  244. package/bin/executor/composer/system/default/assets/plugin.prompt.js.map +0 -1
  245. package/bin/plugin/core/Activation.d.ts +0 -18
  246. package/bin/plugin/core/Activation.d.ts.map +0 -1
  247. package/bin/plugin/core/Activation.js +0 -21
  248. package/bin/plugin/core/Activation.js.map +0 -1
  249. package/bin/plugin/core/Manager.d.ts +0 -7
  250. package/bin/plugin/core/Manager.d.ts.map +0 -1
  251. package/bin/plugin/core/Manager.js +0 -6
  252. package/bin/plugin/core/Manager.js.map +0 -1
  253. package/bin/plugin/types/Plugin.d.ts +0 -15
  254. package/bin/plugin/types/Plugin.d.ts.map +0 -1
  255. package/bin/plugin/types/Plugin.js +0 -9
  256. package/bin/plugin/types/Plugin.js.map +0 -1
  257. package/bin/plugin/types/PluginLifecycle.d.ts +0 -34
  258. package/bin/plugin/types/PluginLifecycle.d.ts.map +0 -1
  259. package/bin/plugin/types/PluginLifecycle.js +0 -9
  260. package/bin/plugin/types/PluginLifecycle.js.map +0 -1
  261. package/bin/plugin/types/PluginState.d.ts +0 -5
  262. package/bin/plugin/types/PluginState.d.ts.map +0 -1
  263. package/bin/plugin/types/PluginState.js +0 -5
  264. package/bin/plugin/types/PluginState.js.map +0 -1
  265. package/bin/plugin/types/Plugins.d.ts +0 -61
  266. package/bin/plugin/types/Plugins.d.ts.map +0 -1
  267. package/bin/plugin/types/Plugins.js +0 -5
  268. package/bin/plugin/types/Plugins.js.map +0 -1
  269. package/bin/session/index.d.ts +0 -15
  270. package/bin/session/index.d.ts.map +0 -1
  271. package/bin/session/index.js +0 -15
  272. package/bin/session/index.js.map +0 -1
  273. package/bin/types/agent/AgentTypes.d.ts +0 -18
  274. package/bin/types/agent/AgentTypes.d.ts.map +0 -1
  275. package/bin/types/agent/AgentTypes.js +0 -10
  276. package/bin/types/agent/AgentTypes.js.map +0 -1
  277. package/bin/utils/logger/Format.d.ts +0 -11
  278. package/bin/utils/logger/Format.d.ts.map +0 -1
  279. package/bin/utils/logger/Format.js +0 -10
  280. package/bin/utils/logger/Format.js.map +0 -1
  281. package/src/config/DowncitySchema.ts +0 -175
  282. package/src/executor/composer/system/default/assets/plugin.prompt.ts +0 -9
  283. package/src/executor/composer/system/default/assets/plugin.prompt.ts.txt +0 -20
  284. package/src/plugin/core/Activation.ts +0 -26
  285. package/src/plugin/core/Manager.ts +0 -17
  286. package/src/plugin/types/Plugin.ts +0 -63
  287. package/src/plugin/types/PluginLifecycle.ts +0 -35
  288. package/src/plugin/types/PluginState.ts +0 -12
  289. package/src/plugin/types/Plugins.ts +0 -67
  290. package/src/session/index.ts +0 -57
  291. package/src/types/agent/AgentTypes.ts +0 -58
  292. package/src/utils/logger/Format.ts +0 -15
@@ -4,6 +4,6 @@
4
4
  */
5
5
 
6
6
  // Source: src/executor/composer/system/default/assets/core.prompt.ts.txt
7
- const TEXT_MODULE_CONTENT = "You have permission to use and modify only the current project: {{project_path}}. The current year is {{current_year}}.\n1. `.downcity/` is Downcity's runtime data directory. You normally do not need to read or modify it manually; the system writes and injects it automatically. Its structure and purpose are:\n - `.downcity/agents/<agentId>/sessions/`: session messages.\n - `.downcity/memory/`: medium- and long-term memory.\n - `.downcity/profile/Primary.md`, `.downcity/profile/other.md`: global profile memory. When present, these files are automatically injected as system prompts.\n - `.downcity/public/`: public static assets, served at `GET /downcity/public/<path>`. Use it for files that must be accessible externally. Do not store sensitive information there. The Agent HTTP gateway exposes `.downcity/public/` as static HTTP files, so you may share those URLs with the user for downloading or viewing generated files, while taking care not to expose secrets.\n - `.downcity/logs/<YYYY-MM-DD>.jsonl`: runtime logs in JSONL format. Use them for troubleshooting, and avoid pasting raw logs in full to the user.\n - `.downcity/.cache/`: idempotency and deduplication cache for ingress/egress. Do not edit it manually.\n - `.downcity/.debug/`: debug artifacts, such as managed process pid/log/meta files and adapter event captures. Inspect only when troubleshooting.\n - `.downcity/data/`: small persistent data store, reserved for runtime use.\n - `.downcity/task/`: Task directory.\n - `.downcity/sandbox/`: the current agent's local command sandbox HOME/cache/tmp. Shell and script commands share it.\n2. `PROFILE.md` and `SOUL.md` are configuration files for you. You do not need to read them.\n\n# Most Important\n\nCommand execution tools:\n- Prefer `shell_exec` for short, one-off commands.\n- Use `shell_session` for long-running tasks, tasks that need status checks, or processes that need stdin interaction.\n- Start a long-running command with `shell_session({ action: \"start\", cmd })` and keep the returned `shell_id`.\n- `shell_id` identifies a shell session; it is not a chat `session_id`.\n- During long-running tasks, read new output with `shell_session({ action: \"read\", shell_id })`. Do not implement your own high-frequency empty polling loop.\n- When a process needs stdin, use `shell_session({ action: \"send\", shell_id, input })`.\n- When sending stdin to an unrestricted shell session, every `send` call must include a clear `reason` and wait for user confirmation. Approval for `start` only authorizes starting the process; it does not authorize later stdin writes.\n- When a command session is complete and no longer needed, release it with `shell_session({ action: \"stop\", shell_id })`.\n- Do not forward very long raw shell output directly to the user. Summarize it first.\n- Shell commands run in the Safe Sandbox by default: the project directory is readable/writable, network is available, HOME points to `.downcity/sandbox/`, and the real user HOME plus system directories are not writable.\n- When installing Python dependencies, prefer a project-local `.venv`; do not use `pip install --user`.\n- If a task requires global installation, host-directory writes, or host-level capabilities, you may request `sandbox: \"unrestricted\"`. You must provide a clear `reason` and wait for user confirmation before executing.\n- Do not attempt `sudo`, Xcode Command Line Tools installation, SSH/keychain/shell profile modification, or obviously destructive commands. These requests are rejected even with unrestricted sandbox access.\n- If the user rejects an unrestricted sandbox request, do not keep asking for the same command. Explain the impact and provide a project-local alternative when possible.\n- Downloads for models, tool caches, and temporary state should naturally live under `.downcity/sandbox/` or the project directory. Do not assume access to the real user's cache.\n\n# Default Decisions And Clarification\n\n- Execute first and communicate second by default: for low-risk, reversible requests where the user's intent is clear enough, use the current date, timezone, chat context, and common defaults to proceed. Do not repeatedly ask about obvious event titles, default platforms, or clear time expressions.\n- Ask a clarification only when missing information would materially change the outcome, such as the date, target object, amount, account, recipient, or when an action is irreversible, high-risk, or privacy-sensitive.\n- When handling relative time expressions, prefer the runtime-provided `current_date`, `current_time`, and `timezone`. If inbound `<info>` explicitly provides `user_timezone`, use that first; otherwise use the runtime clock timezone. For expressions such as \"today\", \"tomorrow\", \"2 PM\", or \"two hours earlier\", resolve them to absolute times before acting, and state the absolute date and time in your reply.\n- When a task depends on external permissions, system capabilities, or third-party connections such as calendars, reminders, chat channels, or system authorization, probe availability first before promising to create, send, or write anything.\n- If probing shows a blocker caused by system permissions, host environment, or connection state, state the real blocker and next step directly. Do not first promise that you can do it and then spend multiple turns asking follow-up questions.\n- If you have enough information to complete several low-risk default actions in one pass, complete them and briefly state which assumptions you used.\n\n# Very Important\n\nSafety and boundaries:\n- Do not run destructive commands such as `rm -rf` or `git reset --hard` unless the user explicitly asks for them.\n- For API keys, tokens, secrets, environment variables, bot credentials, and similar sensitive configuration, prefer guiding the user to maintain them in Console surfaces such as `Global / Env` or `Global / Channel Accounts`. Do not ask the user to paste secret values directly into the current chat.\n- Secret-listing capabilities can return only configured key names and descriptions. They never return secret values. Do not ask the user to \"send the secret to you\" or keep trying to retrieve plaintext secrets.\n";
7
+ const TEXT_MODULE_CONTENT = "# Downcity Agent\n\nYou are a Downcity Agent running inside the Downcity Agent runtime.\n\nYour job is to help the user inspect, edit, run, debug, and operate the current project through the runtime tools and enabled plugins.\n\nYou have permission to use and modify only the project bound to the current session unless the user explicitly asks for a broader action and the runtime grants the required capability.\n\n# Harness Design\n\nDowncity Agent is a harness around an existing human-owned workspace, not a replacement platform or a detached autonomous worker.\n\n- The harness lets the Agent enter the user's existing project instead of forcing the user to migrate work into a new AI platform.\n- The project structure is the control surface. Files, directories, scripts, docs, configuration, and existing workflows define the practical business boundary.\n- The Agent inherits the workspace's knowledge topology. Read and follow the existing structure before creating new abstractions, stores, or process layers.\n- The harness keeps the human in control. The user owns intent, boundaries, approvals, and final judgment; the Agent supplies high-density execution inside those boundaries.\n- Agent state should remain observable and handoff-friendly. Prefer outputs, changes, and explanations that a human can inspect, resume, or correct through normal project artifacts.\n- Autonomy should be progressive. Execute low-risk, reversible work directly; pause for clarification or approval when the boundary, consequence, or owner is unclear.\n- Tools and plugins are harness surfaces. Use them to act precisely in the current execution context without treating any tool as a reason to ignore the user's native workflow.\n- Prefer project-native, maintainable changes over clever platform-shaped abstractions. The best result should feel like it naturally belongs in the existing workspace.\n\n# Shell Commands\n\nCommand execution tools:\n- Prefer `shell_exec` for short, one-off commands.\n- Use `shell_session` for long-running tasks, tasks that need status checks, or processes that need stdin interaction.\n- Start a long-running command with `shell_session({ action: \"start\", cmd })` and keep the returned `shell_id`.\n- `shell_id` identifies a shell session; it is not a chat `session_id`.\n- During long-running tasks, read new output with `shell_session({ action: \"read\", shell_id })`. Do not implement your own high-frequency empty polling loop.\n- When a process needs stdin, use `shell_session({ action: \"send\", shell_id, input })`.\n- When sending stdin to an unrestricted shell session, every `send` call must include a clear `reason` and wait for user confirmation. Approval for `start` only authorizes starting the process; it does not authorize later stdin writes.\n- When a command session is complete and no longer needed, release it with `shell_session({ action: \"stop\", shell_id })`.\n- Do not forward very long raw shell output directly to the user. Summarize it first.\n- Shell commands run in the Safe Sandbox by default: the project directory is readable/writable, network is available, HOME points to `.downcity/sandbox/`, and the real user HOME plus system directories are not writable.\n- When installing Python dependencies, prefer a project-local `.venv`; do not use `pip install --user`.\n- If a task requires global installation, host-directory writes, or host-level capabilities, you may request `sandbox: \"unrestricted\"`. You must provide a clear `reason` and wait for user confirmation before executing.\n- Do not attempt `sudo`, Xcode Command Line Tools installation, SSH/keychain/shell profile modification, or obviously destructive commands. These requests are rejected even with unrestricted sandbox access.\n- If the user rejects an unrestricted sandbox request, do not keep asking for the same command. Explain the impact and provide a project-local alternative when possible.\n- Downloads for models, tool caches, and temporary state should naturally live under `.downcity/sandbox/` or the project directory. Do not assume access to the real user's cache.\n\n# Plugin System\n\nYou are working in a plugin-based execution environment.\n\nPlugin call rules:\n- When you need plugin capabilities, prefer invoking the plugin action through the available tools.\n- If `plugin_read` is available and you are unsure about a plugin action, parameter schema, or example, first call `plugin_read({ plugin, action? })` to read metadata.\n- If `plugin_call` is available, call the action with `plugin_call({ plugin, action, payload })`.\n- `plugin_call.plugin` is the plugin name, for example `skill`, `task`, `memory`, or `contact`.\n- `plugin_call.action` is the action name, for example `list`, `lookup`, `create`, or `run`.\n- `plugin_call.payload` is a structured JSON payload. Pass `{}` when there are no parameters. If the action metadata declares an input schema, the payload must conform to that schema.\n- `ActionSchedule` is an internal Agent capability for delayed plugin actions. It is not a standalone plugin.\n\nSpecific plugin capabilities are defined by the action metadata returned by `plugin_read` and by each plugin's system prompt.\n\n# Default Decisions And Clarification\n\n- Execute first and communicate second by default: for low-risk, reversible requests where the user's intent is clear enough, use the current date, timezone, chat context, and common defaults to proceed. Do not repeatedly ask about obvious event titles, default platforms, or clear time expressions.\n- Ask a clarification only when missing information would materially change the outcome, such as the date, target object, amount, account, recipient, or when an action is irreversible, high-risk, or privacy-sensitive.\n- When handling relative time expressions, prefer the runtime-provided `current_date`, `current_time`, and `timezone`. If inbound `<info>` explicitly provides `user_timezone`, use that first; otherwise use the runtime clock timezone. For expressions such as \"today\", \"tomorrow\", \"2 PM\", or \"two hours earlier\", resolve them to absolute times before acting, and state the absolute date and time in your reply.\n- When a task depends on external permissions, system capabilities, or third-party connections such as calendars, reminders, chat channels, or system authorization, probe availability first before promising to create, send, or write anything.\n- If probing shows a blocker caused by system permissions, host environment, or connection state, state the real blocker and next step directly. Do not first promise that you can do it and then spend multiple turns asking follow-up questions.\n- If you have enough information to complete several low-risk default actions in one pass, complete them and briefly state which assumptions you used.\n\n# Very Important\n\nSafety and boundaries:\n- Do not run destructive commands such as `rm -rf` or `git reset --hard` unless the user explicitly asks for them.\n- For API keys, tokens, secrets, environment variables, bot credentials, and similar sensitive configuration, prefer guiding the user to maintain them in Console surfaces such as `Global / Env` or `Global / Channel Accounts`. Do not ask the user to paste secret values directly into the current chat.\n- Secret-listing capabilities can return only configured key names and descriptions. They never return secret values. Do not ask the user to \"send the secret to you\" or keep trying to retrieve plaintext secrets.\n";
8
8
 
9
9
  export default TEXT_MODULE_CONTENT;
@@ -1,18 +1,25 @@
1
- You have permission to use and modify only the current project: {{project_path}}. The current year is {{current_year}}.
2
- 1. `.downcity/` is Downcity's runtime data directory. You normally do not need to read or modify it manually; the system writes and injects it automatically. Its structure and purpose are:
3
- - `.downcity/agents/<agentId>/sessions/`: session messages.
4
- - `.downcity/memory/`: medium- and long-term memory.
5
- - `.downcity/profile/Primary.md`, `.downcity/profile/other.md`: global profile memory. When present, these files are automatically injected as system prompts.
6
- - `.downcity/public/`: public static assets, served at `GET /downcity/public/<path>`. Use it for files that must be accessible externally. Do not store sensitive information there. The Agent HTTP gateway exposes `.downcity/public/` as static HTTP files, so you may share those URLs with the user for downloading or viewing generated files, while taking care not to expose secrets.
7
- - `.downcity/logs/<YYYY-MM-DD>.jsonl`: runtime logs in JSONL format. Use them for troubleshooting, and avoid pasting raw logs in full to the user.
8
- - `.downcity/.cache/`: idempotency and deduplication cache for ingress/egress. Do not edit it manually.
9
- - `.downcity/.debug/`: debug artifacts, such as managed process pid/log/meta files and adapter event captures. Inspect only when troubleshooting.
10
- - `.downcity/data/`: small persistent data store, reserved for runtime use.
11
- - `.downcity/task/`: Task directory.
12
- - `.downcity/sandbox/`: the current agent's local command sandbox HOME/cache/tmp. Shell and script commands share it.
13
- 2. `PROFILE.md` and `SOUL.md` are configuration files for you. You do not need to read them.
14
-
15
- # Most Important
1
+ # Downcity Agent
2
+
3
+ You are a Downcity Agent running inside the Downcity Agent runtime.
4
+
5
+ Your job is to help the user inspect, edit, run, debug, and operate the current project through the runtime tools and enabled plugins.
6
+
7
+ You have permission to use and modify only the project bound to the current session unless the user explicitly asks for a broader action and the runtime grants the required capability.
8
+
9
+ # Harness Design
10
+
11
+ Downcity Agent is a harness around an existing human-owned workspace, not a replacement platform or a detached autonomous worker.
12
+
13
+ - The harness lets the Agent enter the user's existing project instead of forcing the user to migrate work into a new AI platform.
14
+ - The project structure is the control surface. Files, directories, scripts, docs, configuration, and existing workflows define the practical business boundary.
15
+ - The Agent inherits the workspace's knowledge topology. Read and follow the existing structure before creating new abstractions, stores, or process layers.
16
+ - The harness keeps the human in control. The user owns intent, boundaries, approvals, and final judgment; the Agent supplies high-density execution inside those boundaries.
17
+ - Agent state should remain observable and handoff-friendly. Prefer outputs, changes, and explanations that a human can inspect, resume, or correct through normal project artifacts.
18
+ - Autonomy should be progressive. Execute low-risk, reversible work directly; pause for clarification or approval when the boundary, consequence, or owner is unclear.
19
+ - Tools and plugins are harness surfaces. Use them to act precisely in the current execution context without treating any tool as a reason to ignore the user's native workflow.
20
+ - Prefer project-native, maintainable changes over clever platform-shaped abstractions. The best result should feel like it naturally belongs in the existing workspace.
21
+
22
+ # Shell Commands
16
23
 
17
24
  Command execution tools:
18
25
  - Prefer `shell_exec` for short, one-off commands.
@@ -31,6 +38,21 @@ Command execution tools:
31
38
  - If the user rejects an unrestricted sandbox request, do not keep asking for the same command. Explain the impact and provide a project-local alternative when possible.
32
39
  - Downloads for models, tool caches, and temporary state should naturally live under `.downcity/sandbox/` or the project directory. Do not assume access to the real user's cache.
33
40
 
41
+ # Plugin System
42
+
43
+ You are working in a plugin-based execution environment.
44
+
45
+ Plugin call rules:
46
+ - When you need plugin capabilities, prefer invoking the plugin action through the available tools.
47
+ - If `plugin_read` is available and you are unsure about a plugin action, parameter schema, or example, first call `plugin_read({ plugin, action? })` to read metadata.
48
+ - If `plugin_call` is available, call the action with `plugin_call({ plugin, action, payload })`.
49
+ - `plugin_call.plugin` is the plugin name, for example `skill`, `task`, `memory`, or `contact`.
50
+ - `plugin_call.action` is the action name, for example `list`, `lookup`, `create`, or `run`.
51
+ - `plugin_call.payload` is a structured JSON payload. Pass `{}` when there are no parameters. If the action metadata declares an input schema, the payload must conform to that schema.
52
+ - `ActionSchedule` is an internal Agent capability for delayed plugin actions. It is not a standalone plugin.
53
+
54
+ Specific plugin capabilities are defined by the action metadata returned by `plugin_read` and by each plugin's system prompt.
55
+
34
56
  # Default Decisions And Clarification
35
57
 
36
58
  - Execute first and communicate second by default: for low-risk, reversible requests where the user's intent is clear enough, use the current date, timezone, chat context, and common defaults to proceed. Do not repeatedly ask about obvious event titles, default platforms, or clear time expressions.
@@ -15,6 +15,11 @@ import fs from "fs-extra";
15
15
  import type { FileUIPart } from "ai";
16
16
  import { getDowncityResourcesDirPath } from "@/config/Paths.js";
17
17
 
18
+ /** 单个 assistant 资源下载允许占用的最长时间。 */
19
+ const ASSISTANT_RESOURCE_TIMEOUT_MS = 30_000;
20
+ /** 单个 assistant 资源允许写入内存与磁盘的最大字节数。 */
21
+ const MAX_ASSISTANT_RESOURCE_BYTES = 25 * 1024 * 1024;
22
+
18
23
  type ParsedDataUrl = {
19
24
  /**
20
25
  * data URL 声明的媒体类型。
@@ -186,22 +191,49 @@ async function read_remote_resource(raw_url: string): Promise<{
186
191
  filename?: string;
187
192
  bytes: Buffer;
188
193
  }> {
189
- const result = await fetch_with_retry(raw_url);
190
- if (!result.ok) {
191
- throw new Error(
192
- `Failed to download assistant file resource: ${result.status} :: url=${raw_url}`,
193
- );
194
- }
195
- const array_buffer = await result.arrayBuffer();
196
- const bytes = Buffer.from(array_buffer);
197
- if (bytes.length === 0) {
198
- throw new Error(`Downloaded assistant file resource is empty :: url=${raw_url}`);
194
+ const abort_controller = new AbortController();
195
+ const timeout = setTimeout(() => {
196
+ abort_controller.abort(new Error("Assistant resource download timed out"));
197
+ }, ASSISTANT_RESOURCE_TIMEOUT_MS);
198
+ try {
199
+ const result = await fetch_with_retry(raw_url, abort_controller.signal);
200
+ if (!result.ok) {
201
+ throw new Error(
202
+ `Failed to download assistant file resource: ${result.status} :: url=${raw_url}`,
203
+ );
204
+ }
205
+ const declared_size = Number(result.headers.get("content-length"));
206
+ if (Number.isFinite(declared_size) && declared_size > MAX_ASSISTANT_RESOURCE_BYTES) {
207
+ throw new Error(`Assistant file resource exceeds 25 MiB :: url=${raw_url}`);
208
+ }
209
+ if (!result.body) {
210
+ throw new Error(`Assistant file resource has no response body :: url=${raw_url}`);
211
+ }
212
+
213
+ const reader = result.body.getReader();
214
+ const chunks: Buffer[] = [];
215
+ let total_bytes = 0;
216
+ while (true) {
217
+ const { done, value } = await reader.read();
218
+ if (done) break;
219
+ total_bytes += value.byteLength;
220
+ if (total_bytes > MAX_ASSISTANT_RESOURCE_BYTES) {
221
+ await reader.cancel();
222
+ throw new Error(`Assistant file resource exceeds 25 MiB :: url=${raw_url}`);
223
+ }
224
+ chunks.push(Buffer.from(value));
225
+ }
226
+ if (total_bytes === 0) {
227
+ throw new Error(`Downloaded assistant file resource is empty :: url=${raw_url}`);
228
+ }
229
+ return {
230
+ mediaType: result.headers.get("content-type")?.split(";")[0]?.trim(),
231
+ filename: filename_from_url(raw_url),
232
+ bytes: Buffer.concat(chunks, total_bytes),
233
+ };
234
+ } finally {
235
+ clearTimeout(timeout);
199
236
  }
200
- return {
201
- mediaType: result.headers.get("content-type")?.split(";")[0]?.trim(),
202
- filename: filename_from_url(raw_url),
203
- bytes,
204
- };
205
237
  }
206
238
 
207
239
  /**
@@ -213,13 +245,14 @@ async function read_remote_resource(raw_url: string): Promise<{
213
245
  */
214
246
  async function fetch_with_retry(
215
247
  raw_url: string,
248
+ signal: AbortSignal,
216
249
  ): Promise<Response> {
217
250
  const delays_ms = [250, 1_000];
218
251
  const attempts = delays_ms.length + 1;
219
252
  let last_error: unknown;
220
253
  for (let attempt = 0; attempt < attempts; attempt += 1) {
221
254
  try {
222
- return await fetch(raw_url);
255
+ return await fetch(raw_url, { signal });
223
256
  } catch (error) {
224
257
  last_error = error;
225
258
  if (!is_transient_fetch_error(error) || attempt === attempts - 1) {
@@ -286,7 +319,14 @@ async function read_local_resource(
286
319
  : path.isAbsolute(raw)
287
320
  ? raw
288
321
  : path.resolve(projectRoot, raw);
322
+ const file_stat = await fs.stat(file_path);
323
+ if (file_stat.size > MAX_ASSISTANT_RESOURCE_BYTES) {
324
+ throw new Error(`Assistant file resource exceeds 25 MiB: ${file_path}`);
325
+ }
289
326
  const bytes = await fs.readFile(file_path);
327
+ if (bytes.length > MAX_ASSISTANT_RESOURCE_BYTES) {
328
+ throw new Error(`Assistant file resource exceeds 25 MiB: ${file_path}`);
329
+ }
290
330
  if (bytes.length === 0) {
291
331
  throw new Error(`Assistant file resource is empty: ${file_path}`);
292
332
  }
@@ -306,6 +346,9 @@ async function materialize_file_part(params: {
306
346
  }
307
347
  const parsed_data_url = parse_data_url(raw_url);
308
348
  if (parsed_data_url) {
349
+ if (parsed_data_url.bytes.length > MAX_ASSISTANT_RESOURCE_BYTES) {
350
+ throw new Error("Assistant data URL resource exceeds 25 MiB");
351
+ }
309
352
  const media_type =
310
353
  String(params.part.mediaType || parsed_data_url.media_type || "").trim() ||
311
354
  "application/octet-stream";
@@ -352,8 +395,9 @@ async function materialize_file_part(params: {
352
395
  } catch (error) {
353
396
  // 关键点(中文):远程下载失败时保留原始 URL,不让整张图导致 action 失败。
354
397
  // 这样图片至少以链接形式存在,用户可手动点击或后续再试。
398
+ const error_message = error instanceof Error ? error.message : String(error);
355
399
  console.warn(
356
- `[AssistantFileResource] remote download failed, keeping original url :: ${describe_error_cause(error instanceof Error ? error : new Error(String(error)))} :: url=${raw_url}`,
400
+ `[AssistantFileResource] remote download failed, keeping original url :: ${error_message}`,
357
401
  );
358
402
  return params.part;
359
403
  }
@@ -23,10 +23,6 @@ interface ExecutorInflightServiceOptions {
23
23
  */
24
24
  history_store: SessionHistoryStore;
25
25
 
26
- /**
27
- * inflight 更新完成后的异步通知。
28
- */
29
- run_after_session_updated_async?: () => Promise<void>;
30
26
  }
31
27
 
32
28
  /**
@@ -35,13 +31,10 @@ interface ExecutorInflightServiceOptions {
35
31
  export class ExecutorInflightService {
36
32
  private readonly session_id: string;
37
33
  private readonly history_store: SessionHistoryStore;
38
- private readonly run_after_session_updated_async?: ExecutorInflightServiceOptions["run_after_session_updated_async"];
39
34
 
40
35
  constructor(options: ExecutorInflightServiceOptions) {
41
36
  this.session_id = String(options.session_id || "").trim();
42
37
  this.history_store = options.history_store;
43
- this.run_after_session_updated_async =
44
- options.run_after_session_updated_async;
45
38
 
46
39
  if (!this.session_id) {
47
40
  throw new Error("ExecutorInflightService requires a non-empty session_id");
@@ -98,8 +91,5 @@ export class ExecutorInflightService {
98
91
  };
99
92
 
100
93
  await this.history_store.write_inflight(next_message);
101
- if (this.run_after_session_updated_async) {
102
- await this.run_after_session_updated_async();
103
- }
104
94
  }
105
95
  }
@@ -34,6 +34,7 @@ import type {
34
34
  SessionHistoryCompactInput,
35
35
  SessionHistoryStore,
36
36
  } from "@/executor/store/history/SessionHistoryStore.js";
37
+ import { resolve_session_message_preview } from "@/session/preview/SessionMessagePreview.js";
37
38
 
38
39
  /**
39
40
  * JSONL history store 构造参数。
@@ -359,6 +360,19 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
359
360
  ...(this.normalizeText(raw.modelLabel)
360
361
  ? { modelLabel: this.normalizeText(raw.modelLabel) }
361
362
  : {}),
363
+ ...(typeof raw.messageCount === "number" &&
364
+ Number.isInteger(raw.messageCount) &&
365
+ raw.messageCount >= 0
366
+ ? { messageCount: raw.messageCount }
367
+ : {}),
368
+ ...(this.normalizeText(raw.previewText)
369
+ ? { previewText: this.normalizeText(raw.previewText) }
370
+ : {}),
371
+ ...(typeof raw.historyBytes === "number" &&
372
+ Number.isInteger(raw.historyBytes) &&
373
+ raw.historyBytes >= 0
374
+ ? { historyBytes: raw.historyBytes }
375
+ : {}),
362
376
  };
363
377
  } catch {
364
378
  return {
@@ -391,6 +405,19 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
391
405
  ...(this.normalizeText(next.modelLabel)
392
406
  ? { modelLabel: this.normalizeText(next.modelLabel) }
393
407
  : {}),
408
+ ...(typeof next.messageCount === "number" &&
409
+ Number.isInteger(next.messageCount) &&
410
+ next.messageCount >= 0
411
+ ? { messageCount: next.messageCount }
412
+ : {}),
413
+ ...(this.normalizeText(next.previewText)
414
+ ? { previewText: this.normalizeText(next.previewText) }
415
+ : {}),
416
+ ...(typeof next.historyBytes === "number" &&
417
+ Number.isInteger(next.historyBytes) &&
418
+ next.historyBytes >= 0
419
+ ? { historyBytes: next.historyBytes }
420
+ : {}),
394
421
  };
395
422
  await fs.writeJson(this.getMetaFilePath(), normalized, { spaces: 2 });
396
423
  }
@@ -432,6 +459,82 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
432
459
  );
433
460
  }
434
461
 
462
+ private async get_history_bytes_unsafe(): Promise<number> {
463
+ try {
464
+ const file_stat = await statNative(this.getMessagesFilePath());
465
+ return file_stat.size;
466
+ } catch {
467
+ return 0;
468
+ }
469
+ }
470
+
471
+ private async read_persisted_records_unsafe(): Promise<SessionRecordV1[]> {
472
+ const raw = await fs.readFile(this.getMessagesFilePath(), "utf8").catch(() => "");
473
+ const records: SessionRecordV1[] = [];
474
+ for (const line of raw.split("\n").filter(Boolean)) {
475
+ try {
476
+ const record = this.normalizePersistedMessage(JSON.parse(line));
477
+ if (record) records.push(record);
478
+ } catch {
479
+ // 关键点(中文):摘要重建跳过损坏行,与正式历史读取语义保持一致。
480
+ }
481
+ }
482
+ return records;
483
+ }
484
+
485
+ private async write_history_summary_unsafe(
486
+ records: SessionRecordV1[],
487
+ ): Promise<void> {
488
+ const current = await this.readMetaUnsafe();
489
+ const last_record = records[records.length - 1];
490
+ const preview_text = last_record
491
+ ? resolve_session_message_preview(last_record).slice(0, 180).trim()
492
+ : "";
493
+ const { previewText: _previous_preview, ...metadata_without_preview } = current;
494
+ void _previous_preview;
495
+ await this.writeMetaUnsafe({
496
+ ...metadata_without_preview,
497
+ updatedAt: Date.now(),
498
+ messageCount: records.length,
499
+ historyBytes: await this.get_history_bytes_unsafe(),
500
+ ...(preview_text ? { previewText: preview_text } : {}),
501
+ });
502
+ }
503
+
504
+ private async update_history_summary_after_append_unsafe(input: {
505
+ /** 本次追加前 messages.jsonl 的字节长度。 */
506
+ previous_history_bytes: number;
507
+ /** 本次实际追加的 records,按写入顺序排列。 */
508
+ appended_records: SessionRecordV1[];
509
+ }): Promise<void> {
510
+ if (input.appended_records.length === 0) return;
511
+ const current = await this.readMetaUnsafe();
512
+ const previous_message_count = current.messageCount;
513
+ if (
514
+ current.historyBytes !== input.previous_history_bytes ||
515
+ typeof previous_message_count !== "number"
516
+ ) {
517
+ await this.write_history_summary_unsafe(
518
+ await this.read_persisted_records_unsafe(),
519
+ );
520
+ return;
521
+ }
522
+
523
+ const last_record = input.appended_records[input.appended_records.length - 1];
524
+ const preview_text = resolve_session_message_preview(last_record)
525
+ .slice(0, 180)
526
+ .trim();
527
+ const { previewText: _previous_preview, ...metadata_without_preview } = current;
528
+ void _previous_preview;
529
+ await this.writeMetaUnsafe({
530
+ ...metadata_without_preview,
531
+ updatedAt: Date.now(),
532
+ messageCount: previous_message_count + input.appended_records.length,
533
+ historyBytes: await this.get_history_bytes_unsafe(),
534
+ ...(preview_text ? { previewText: preview_text } : {}),
535
+ });
536
+ }
537
+
435
538
  private async rewriteMessagesUnsafe(messages: SessionRecordV1[]): Promise<void> {
436
539
  const file = this.getMessagesFilePath();
437
540
  const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
@@ -445,7 +548,7 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
445
548
 
446
549
  private async upsert_action_record_unsafe(
447
550
  message: SessionActionRecordV1,
448
- ): Promise<void> {
551
+ ): Promise<SessionRecordV1[]> {
449
552
  const file = this.getMessagesFilePath();
450
553
  const raw = await fs.readFile(file, "utf8").catch(() => "");
451
554
  const messages: SessionRecordV1[] = [];
@@ -468,6 +571,7 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
468
571
 
469
572
  if (!replaced) messages.push(message);
470
573
  await this.rewriteMessagesUnsafe(messages);
574
+ return messages;
471
575
  }
472
576
 
473
577
  private async withWriteLock<T>(fn: () => Promise<T>): Promise<T> {
@@ -555,8 +659,8 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
555
659
  }),
556
660
  getArchiveDirPath: () => this.getArchiveDirPath(),
557
661
  getMessagesFilePath: () => this.getMessagesFilePath(),
558
- readMetaUnsafe: () => this.readMetaUnsafe(),
559
- writeMetaUnsafe: (next) => this.writeMetaUnsafe(next),
662
+ writeHistorySummaryUnsafe: (records) =>
663
+ this.write_history_summary_unsafe(records),
560
664
  },
561
665
  {
562
666
  model: input.model,
@@ -575,21 +679,31 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
575
679
  const normalized = this.normalizePersistedMessage(message);
576
680
  if (!normalized) return;
577
681
  if (is_session_action_record(normalized)) {
578
- await this.upsert_action_record_unsafe(normalized);
682
+ const records = await this.upsert_action_record_unsafe(normalized);
683
+ await this.write_history_summary_unsafe(records);
579
684
  return;
580
685
  }
581
686
 
687
+ const previous_history_bytes = await this.get_history_bytes_unsafe();
688
+ const appended_records: SessionRecordV1[] = [];
689
+
582
690
  // 关键点(中文):若上一次 assistant 在运行中中断,新的 user 到来前先把残留快照收口到正式历史,
583
691
  // 避免 `list_records()` 时旧 inflight 跑到新 user 后面,打乱时序。
584
692
  if (is_session_message_record(normalized) && normalized.role === "user") {
585
693
  const current_inflight = await this.read_inflight_unsafe();
586
694
  if (current_inflight) {
587
695
  await this.appendMessageUnsafe(current_inflight);
696
+ appended_records.push(current_inflight);
588
697
  await this.removeInflightUnsafe();
589
698
  }
590
699
  }
591
700
 
592
701
  await this.appendMessageUnsafe(normalized);
702
+ appended_records.push(normalized);
703
+ await this.update_history_summary_after_append_unsafe({
704
+ previous_history_bytes,
705
+ appended_records,
706
+ });
593
707
  });
594
708
  }
595
709
 
@@ -611,7 +725,12 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
611
725
  .map((message) => `${JSON.stringify(message)}\n`)
612
726
  .join("");
613
727
  await this.withWriteLock(async () => {
728
+ const previous_history_bytes = await this.get_history_bytes_unsafe();
614
729
  await fs.appendFile(this.getMessagesFilePath(), payload, "utf8");
730
+ await this.update_history_summary_after_append_unsafe({
731
+ previous_history_bytes,
732
+ appended_records: normalized_list,
733
+ });
615
734
  });
616
735
  }
617
736
 
@@ -628,6 +747,7 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
628
747
 
629
748
  async finalize_inflight(message?: SessionRecordV1 | null): Promise<void> {
630
749
  await this.withWriteLock(async () => {
750
+ const previous_history_bytes = await this.get_history_bytes_unsafe();
631
751
  const current_inflight = await this.read_inflight_unsafe();
632
752
  const normalized_message = this.normalizePersistedMessage(message);
633
753
  const final_message =
@@ -640,6 +760,12 @@ export class JsonlSessionHistoryStore implements SessionHistoryStore {
640
760
  if (current_inflight) {
641
761
  await this.removeInflightUnsafe();
642
762
  }
763
+ if (final_message) {
764
+ await this.update_history_summary_after_append_unsafe({
765
+ previous_history_bytes,
766
+ appended_records: [final_message],
767
+ });
768
+ }
643
769
  });
644
770
  }
645
771
 
@@ -23,4 +23,10 @@ export type SessionHistoryMetaV1 = {
23
23
  title?: string;
24
24
  /** 当前 session 绑定模型的可读标签。 */
25
25
  modelLabel?: string;
26
+ /** 当前 session 已持久化记录数量,用于列表查询避免扫描完整历史。 */
27
+ messageCount?: number;
28
+ /** 当前 session 最后一条记录的用户可见摘要,用于列表预览。 */
29
+ previewText?: string;
30
+ /** 生成当前摘要时 messages.jsonl 的字节长度,用于校验摘要是否仍然有效。 */
31
+ historyBytes?: number;
26
32
  };