@code-yeongyu/senpi 2026.8.28 → 2026.8.30

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 (272) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/dist/cli/args.d.ts +2 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +4 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session-services.d.ts.map +1 -1
  7. package/dist/core/agent-session-services.js +2 -0
  8. package/dist/core/agent-session-services.js.map +1 -1
  9. package/dist/core/agent-session.d.ts +50 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +313 -25
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/auth-storage.d.ts +1 -0
  14. package/dist/core/auth-storage.d.ts.map +1 -1
  15. package/dist/core/auth-storage.js +3 -0
  16. package/dist/core/auth-storage.js.map +1 -1
  17. package/dist/core/bash-executor.d.ts.map +1 -1
  18. package/dist/core/bash-executor.js +155 -37
  19. package/dist/core/bash-executor.js.map +1 -1
  20. package/dist/core/credential-accounts.d.ts.map +1 -1
  21. package/dist/core/credential-accounts.js +45 -21
  22. package/dist/core/credential-accounts.js.map +1 -1
  23. package/dist/core/credential-pool/classify.d.ts +6 -1
  24. package/dist/core/credential-pool/classify.d.ts.map +1 -1
  25. package/dist/core/credential-pool/classify.js +22 -5
  26. package/dist/core/credential-pool/classify.js.map +1 -1
  27. package/dist/core/credential-pool/env-slots.d.ts.map +1 -1
  28. package/dist/core/credential-pool/env-slots.js +2 -0
  29. package/dist/core/credential-pool/env-slots.js.map +1 -1
  30. package/dist/core/credential-pool/failover.d.ts +8 -0
  31. package/dist/core/credential-pool/failover.d.ts.map +1 -1
  32. package/dist/core/credential-pool/failover.js +24 -6
  33. package/dist/core/credential-pool/failover.js.map +1 -1
  34. package/dist/core/credential-pool/rotation-stream.d.ts +13 -2
  35. package/dist/core/credential-pool/rotation-stream.d.ts.map +1 -1
  36. package/dist/core/credential-pool/rotation-stream.js +97 -10
  37. package/dist/core/credential-pool/rotation-stream.js.map +1 -1
  38. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts +8 -2
  39. package/dist/core/extensions/builtin/compaction/deterministic-fallback.d.ts.map +1 -1
  40. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js +160 -47
  41. package/dist/core/extensions/builtin/compaction/deterministic-fallback.js.map +1 -1
  42. package/dist/core/extensions/builtin/compaction/retained-message-safety.d.ts.map +1 -1
  43. package/dist/core/extensions/builtin/compaction/retained-message-safety.js +31 -8
  44. package/dist/core/extensions/builtin/compaction/retained-message-safety.js.map +1 -1
  45. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.d.ts.map +1 -1
  46. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js +8 -28
  47. package/dist/core/extensions/builtin/cursor-cli-oauth/stream.js.map +1 -1
  48. package/dist/core/extensions/builtin/goal/index.d.ts.map +1 -1
  49. package/dist/core/extensions/builtin/goal/index.js +7 -0
  50. package/dist/core/extensions/builtin/goal/index.js.map +1 -1
  51. package/dist/core/extensions/builtin/permission-system/external-dir.d.ts.map +1 -1
  52. package/dist/core/extensions/builtin/permission-system/external-dir.js +15 -5
  53. package/dist/core/extensions/builtin/permission-system/external-dir.js.map +1 -1
  54. package/dist/core/extensions/builtin/permission-system/parsers.d.ts.map +1 -1
  55. package/dist/core/extensions/builtin/permission-system/parsers.js +16 -1
  56. package/dist/core/extensions/builtin/permission-system/parsers.js.map +1 -1
  57. package/dist/core/extensions/builtin/service-tier.d.ts.map +1 -1
  58. package/dist/core/extensions/builtin/service-tier.js +13 -0
  59. package/dist/core/extensions/builtin/service-tier.js.map +1 -1
  60. package/dist/core/extensions/builtin/terminal/extension.d.ts.map +1 -1
  61. package/dist/core/extensions/builtin/terminal/extension.js +4 -2
  62. package/dist/core/extensions/builtin/terminal/extension.js.map +1 -1
  63. package/dist/core/extensions/builtin/terminal/manager.d.ts +5 -0
  64. package/dist/core/extensions/builtin/terminal/manager.d.ts.map +1 -1
  65. package/dist/core/extensions/builtin/terminal/manager.js +31 -3
  66. package/dist/core/extensions/builtin/terminal/manager.js.map +1 -1
  67. package/dist/core/extensions/builtin/terminal/monitor-permission.d.ts +8 -0
  68. package/dist/core/extensions/builtin/terminal/monitor-permission.d.ts.map +1 -0
  69. package/dist/core/extensions/builtin/terminal/monitor-permission.js +16 -0
  70. package/dist/core/extensions/builtin/terminal/monitor-permission.js.map +1 -0
  71. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts +13 -0
  72. package/dist/core/extensions/builtin/terminal/monitor-registry.d.ts.map +1 -1
  73. package/dist/core/extensions/builtin/terminal/monitor-registry.js +403 -3
  74. package/dist/core/extensions/builtin/terminal/monitor-registry.js.map +1 -1
  75. package/dist/core/extensions/builtin/terminal/session-bundle.d.ts.map +1 -1
  76. package/dist/core/extensions/builtin/terminal/session-bundle.js +1 -0
  77. package/dist/core/extensions/builtin/terminal/session-bundle.js.map +1 -1
  78. package/dist/core/extensions/builtin/terminal/tools/kill-bash.d.ts.map +1 -1
  79. package/dist/core/extensions/builtin/terminal/tools/kill-bash.js +5 -2
  80. package/dist/core/extensions/builtin/terminal/tools/kill-bash.js.map +1 -1
  81. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts +4 -0
  82. package/dist/core/extensions/builtin/terminal/tools/monitor.d.ts.map +1 -1
  83. package/dist/core/extensions/builtin/terminal/tools/monitor.js +40 -2
  84. package/dist/core/extensions/builtin/terminal/tools/monitor.js.map +1 -1
  85. package/dist/core/model-config-schema.d.ts +4 -4
  86. package/dist/core/model-resolver.js +2 -2
  87. package/dist/core/model-resolver.js.map +1 -1
  88. package/dist/core/model-runtime.d.ts +2 -0
  89. package/dist/core/model-runtime.d.ts.map +1 -1
  90. package/dist/core/model-runtime.js +74 -37
  91. package/dist/core/model-runtime.js.map +1 -1
  92. package/dist/core/retry-fallback/chains.d.ts +23 -1
  93. package/dist/core/retry-fallback/chains.d.ts.map +1 -1
  94. package/dist/core/retry-fallback/chains.js +59 -3
  95. package/dist/core/retry-fallback/chains.js.map +1 -1
  96. package/dist/core/retry-fallback/controller.d.ts.map +1 -1
  97. package/dist/core/retry-fallback/controller.js +9 -2
  98. package/dist/core/retry-fallback/controller.js.map +1 -1
  99. package/dist/core/retry-fallback/settings.d.ts.map +1 -1
  100. package/dist/core/retry-fallback/settings.js +9 -0
  101. package/dist/core/retry-fallback/settings.js.map +1 -1
  102. package/dist/core/retry-fallback/validate.js +1 -1
  103. package/dist/core/retry-fallback/validate.js.map +1 -1
  104. package/dist/core/sdk.js +1 -1
  105. package/dist/core/sdk.js.map +1 -1
  106. package/dist/core/session-manager.d.ts +9 -0
  107. package/dist/core/session-manager.d.ts.map +1 -1
  108. package/dist/core/session-manager.js +40 -0
  109. package/dist/core/session-manager.js.map +1 -1
  110. package/dist/core/settings-manager.d.ts +7 -0
  111. package/dist/core/settings-manager.d.ts.map +1 -1
  112. package/dist/core/settings-manager.js +6 -0
  113. package/dist/core/settings-manager.js.map +1 -1
  114. package/dist/core/tools/bash.d.ts.map +1 -1
  115. package/dist/core/tools/bash.js +97 -15
  116. package/dist/core/tools/bash.js.map +1 -1
  117. package/dist/core/tools/index.d.ts +1 -0
  118. package/dist/core/tools/index.d.ts.map +1 -1
  119. package/dist/core/tools/index.js +8 -0
  120. package/dist/core/tools/index.js.map +1 -1
  121. package/dist/core/tools/output-accumulator.d.ts +1 -1
  122. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  123. package/dist/core/tools/output-accumulator.js +76 -34
  124. package/dist/core/tools/output-accumulator.js.map +1 -1
  125. package/dist/main.d.ts +7 -0
  126. package/dist/main.d.ts.map +1 -1
  127. package/dist/main.js +10 -1
  128. package/dist/main.js.map +1 -1
  129. package/dist/modes/interactive/components/favorite-models-selector.d.ts.map +1 -1
  130. package/dist/modes/interactive/components/favorite-models-selector.js +2 -2
  131. package/dist/modes/interactive/components/favorite-models-selector.js.map +1 -1
  132. package/dist/modes/interactive/components/footer.d.ts +7 -0
  133. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  134. package/dist/modes/interactive/components/footer.js +34 -8
  135. package/dist/modes/interactive/components/footer.js.map +1 -1
  136. package/dist/modes/interactive/grok/chrome.d.ts +2 -0
  137. package/dist/modes/interactive/grok/chrome.d.ts.map +1 -1
  138. package/dist/modes/interactive/grok/chrome.js.map +1 -1
  139. package/dist/modes/interactive/interactive-host-runtime.d.ts +51 -1
  140. package/dist/modes/interactive/interactive-host-runtime.d.ts.map +1 -1
  141. package/dist/modes/interactive/interactive-host-runtime.js +545 -40
  142. package/dist/modes/interactive/interactive-host-runtime.js.map +1 -1
  143. package/dist/modes/interactive/interactive-mode.d.ts +7 -0
  144. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  145. package/dist/modes/interactive/interactive-mode.js +390 -239
  146. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  147. package/dist/modes/rpc/connection-handler.d.ts +8 -1
  148. package/dist/modes/rpc/connection-handler.d.ts.map +1 -1
  149. package/dist/modes/rpc/connection-handler.js +268 -63
  150. package/dist/modes/rpc/connection-handler.js.map +1 -1
  151. package/dist/modes/rpc/host-ensure.d.ts +2 -0
  152. package/dist/modes/rpc/host-ensure.d.ts.map +1 -1
  153. package/dist/modes/rpc/host-ensure.js +15 -2
  154. package/dist/modes/rpc/host-ensure.js.map +1 -1
  155. package/dist/modes/rpc/host-lifecycle.d.ts.map +1 -1
  156. package/dist/modes/rpc/host-lifecycle.js +31 -12
  157. package/dist/modes/rpc/host-lifecycle.js.map +1 -1
  158. package/dist/modes/rpc/host-watchdog.d.ts +2 -0
  159. package/dist/modes/rpc/host-watchdog.d.ts.map +1 -1
  160. package/dist/modes/rpc/host-watchdog.js +23 -12
  161. package/dist/modes/rpc/host-watchdog.js.map +1 -1
  162. package/dist/modes/rpc/multi-session-host.js +20 -8
  163. package/dist/modes/rpc/multi-session-host.js.map +1 -1
  164. package/dist/modes/rpc/rpc-client.d.ts +91 -16
  165. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  166. package/dist/modes/rpc/rpc-client.js +129 -37
  167. package/dist/modes/rpc/rpc-client.js.map +1 -1
  168. package/dist/modes/rpc/rpc-input-validation.d.ts +1 -0
  169. package/dist/modes/rpc/rpc-input-validation.d.ts.map +1 -1
  170. package/dist/modes/rpc/rpc-input-validation.js +66 -0
  171. package/dist/modes/rpc/rpc-input-validation.js.map +1 -1
  172. package/dist/modes/rpc/rpc-mode.d.ts +3 -1
  173. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  174. package/dist/modes/rpc/rpc-mode.js +3 -1
  175. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  176. package/dist/modes/rpc/rpc-types.d.ts +241 -3
  177. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  178. package/dist/modes/rpc/rpc-types.js.map +1 -1
  179. package/dist/modes/rpc/session-binding.d.ts +1 -0
  180. package/dist/modes/rpc/session-binding.d.ts.map +1 -1
  181. package/dist/modes/rpc/session-binding.js +21 -1
  182. package/dist/modes/rpc/session-binding.js.map +1 -1
  183. package/dist/modes/rpc/session-command-router.d.ts +22 -0
  184. package/dist/modes/rpc/session-command-router.d.ts.map +1 -1
  185. package/dist/modes/rpc/session-command-router.js +148 -16
  186. package/dist/modes/rpc/session-command-router.js.map +1 -1
  187. package/dist/modes/rpc/session-event-writer.d.ts +4 -0
  188. package/dist/modes/rpc/session-event-writer.d.ts.map +1 -1
  189. package/dist/modes/rpc/session-event-writer.js +58 -7
  190. package/dist/modes/rpc/session-event-writer.js.map +1 -1
  191. package/dist/modes/rpc/session-extension-ui-requests.d.ts +1 -0
  192. package/dist/modes/rpc/session-extension-ui-requests.d.ts.map +1 -1
  193. package/dist/modes/rpc/session-extension-ui-requests.js +5 -0
  194. package/dist/modes/rpc/session-extension-ui-requests.js.map +1 -1
  195. package/dist/modes/rpc/session-registry.d.ts +17 -2
  196. package/dist/modes/rpc/session-registry.d.ts.map +1 -1
  197. package/dist/modes/rpc/session-registry.js +67 -4
  198. package/dist/modes/rpc/session-registry.js.map +1 -1
  199. package/dist/modes/rpc/socket-event-fanout.d.ts +22 -0
  200. package/dist/modes/rpc/socket-event-fanout.d.ts.map +1 -0
  201. package/dist/modes/rpc/socket-event-fanout.js +84 -0
  202. package/dist/modes/rpc/socket-event-fanout.js.map +1 -0
  203. package/docs/rpc.md +40 -1
  204. package/docs/settings.md +34 -0
  205. package/docs/windows.md +2 -0
  206. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +62 -0
  207. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  208. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-indirect-eval.js +139 -90
  209. package/node_modules/@code-yeongyu/senpi-codemode/src/prompt/eval-prompt.ts +4 -4
  210. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  211. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +6 -3
  212. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  213. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.d.ts.map +1 -1
  214. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js +40 -20
  215. package/node_modules/@earendil-works/pi-agent-core/dist/harness/env/nodejs.js.map +1 -1
  216. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts +3 -3
  217. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.d.ts.map +1 -1
  218. package/node_modules/@earendil-works/pi-agent-core/dist/harness/types.js.map +1 -1
  219. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.d.ts +1 -1
  220. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.d.ts.map +1 -1
  221. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js +28 -9
  222. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js.map +1 -1
  223. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  224. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  225. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +58 -31
  226. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  227. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.d.ts +20 -0
  228. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.d.ts.map +1 -0
  229. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.js +443 -0
  230. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent/measure.js.map +1 -0
  231. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts +2 -0
  232. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.d.ts.map +1 -1
  233. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js +2 -0
  234. package/node_modules/@earendil-works/pi-ai/dist/api/cursor-agent.js.map +1 -1
  235. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +1 -1
  236. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  237. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts +1 -0
  238. package/node_modules/@earendil-works/pi-ai/dist/index.d.ts.map +1 -1
  239. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  240. package/node_modules/@earendil-works/pi-ai/dist/index.js.map +1 -1
  241. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  242. package/node_modules/@earendil-works/pi-ai/dist/providers/data/amazon-bedrock.json +1 -1
  243. package/node_modules/@earendil-works/pi-ai/dist/providers/data/baseten.json +1 -1
  244. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  245. package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
  246. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  247. package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
  248. package/node_modules/@earendil-works/pi-ai/dist/providers/data/nvidia.json +1 -1
  249. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  250. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  251. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  252. package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
  253. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  254. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai-coding-cn.json +1 -1
  255. package/node_modules/@earendil-works/pi-ai/dist/providers/data/zai.json +1 -1
  256. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  257. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  258. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  259. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts +7 -0
  260. package/node_modules/@earendil-works/pi-tui/dist/components/editor.d.ts.map +1 -1
  261. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +5 -0
  262. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js.map +1 -1
  263. package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts +1 -1
  264. package/node_modules/@earendil-works/pi-tui/dist/mux.d.ts.map +1 -1
  265. package/node_modules/@earendil-works/pi-tui/dist/mux.js +2 -2
  266. package/node_modules/@earendil-works/pi-tui/dist/mux.js.map +1 -1
  267. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +1 -0
  268. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  269. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +19 -0
  270. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  271. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  272. package/package.json +7 -7
@@ -1,7 +1,7 @@
1
1
  import { existsSync, realpathSync } from "node:fs";
2
2
  import { basename, dirname, isAbsolute, resolve } from "node:path";
3
3
  import { ProviderScope, runWithProviderScope } from "@earendil-works/pi-ai/node/provider-scope";
4
- import { createAgentSessionRuntime, } from "../../core/agent-session-runtime.js";
4
+ import { AgentSessionRuntime, createAgentSessionRuntime, } from "../../core/agent-session-runtime.js";
5
5
  import { SessionManager } from "../../core/session-manager.js";
6
6
  export class RpcSessionRegistryError extends Error {
7
7
  constructor(code, reason) {
@@ -32,6 +32,7 @@ export class RpcSessionRegistry {
32
32
  }
33
33
  async openSession(profile) {
34
34
  this.validateProfile(profile);
35
+ this.syncRuntimeMetadata();
35
36
  const sessionPath = profile.sessionPath ? canonicalPath(profile.sessionPath) : undefined;
36
37
  if (sessionPath && this.reservations.has(sessionPath)) {
37
38
  // Attach-on-open: a live session outlives individual client attachments, so a
@@ -70,9 +71,34 @@ export class RpcSessionRegistry {
70
71
  profile: storedProfile,
71
72
  sessionPath,
72
73
  reservationKey: sessionPath,
74
+ cwd: storedProfile.cwd,
73
75
  attachments: 1,
74
76
  lifecycleMutex: Promise.resolve(),
75
77
  };
78
+ entry.switchSession = (sessionPath, options) => {
79
+ const operation = entry.lifecycleMutex.then(async () => {
80
+ if (entry.state !== "open" || !entry.runtime) {
81
+ throw new RpcSessionRegistryError("unknown_session");
82
+ }
83
+ const runtime = entry.runtime;
84
+ const cwdOverride = options?.cwdOverride;
85
+ const cwdChanged = cwdOverride !== undefined && runtime.session.sessionManager.getCwd() !== cwdOverride;
86
+ const result = await runtime.switchSession(sessionPath, options);
87
+ if (result.cancelled || !cwdChanged)
88
+ return result;
89
+ // A multi-session binding outlives an individual replacement. Keep the
90
+ // entry's runtime object aligned with the replacement so every attached
91
+ // client resolves getters and future commands against the new cwd-bound
92
+ // runtime, not the object created during open_session.
93
+ const replacement = new AgentSessionRuntime(runtime.session, runtime.services, this.options.createRuntime, [...runtime.diagnostics], runtime.modelFallbackMessage, runtime.launchProfile);
94
+ replacement.setRebindSession(entry.rebindSession);
95
+ entry.runtime = replacement;
96
+ this.syncRuntimeMetadata();
97
+ return result;
98
+ });
99
+ entry.lifecycleMutex = operation.then(() => undefined, () => undefined);
100
+ return operation;
101
+ };
76
102
  this.entries.set(handle, entry);
77
103
  try {
78
104
  const manager = sessionPath
@@ -115,6 +141,15 @@ export class RpcSessionRegistry {
115
141
  throw new RpcSessionRegistryError("open_failed", error instanceof Error ? error.message : undefined);
116
142
  }
117
143
  }
144
+ /**
145
+ * Read-only lookup with no state transitions or attachment accounting.
146
+ * Exists so lifecycle decisions (e.g. deferring a dropped connection's
147
+ * release while a turn is still streaming) can inspect the live entry
148
+ * without claiming it.
149
+ */
150
+ peek(handle) {
151
+ return this.entries.get(handle);
152
+ }
118
153
  getForCommand(handle, command) {
119
154
  const entry = this.entries.get(handle);
120
155
  if (!entry)
@@ -152,10 +187,13 @@ export class RpcSessionRegistry {
152
187
  }
153
188
  /** Completes a close previously made visible by beginClose(). */
154
189
  async closeMarked(handle) {
190
+ this.syncRuntimeMetadata();
155
191
  const entry = this.entries.get(handle);
156
192
  if (entry?.state !== "closing")
157
193
  throw new RpcSessionRegistryError("unknown_session");
194
+ const previousLifecycle = entry.lifecycleMutex;
158
195
  entry.lifecycleMutex = (async () => {
196
+ await previousLifecycle;
159
197
  try {
160
198
  await entry.runtime?.session.abort();
161
199
  await entry.runtime?.session.waitForIdle();
@@ -165,22 +203,47 @@ export class RpcSessionRegistry {
165
203
  finally {
166
204
  entry.state = "closed";
167
205
  this.entries.delete(handle);
168
- if (entry.sessionPath)
169
- this.reservations.delete(entry.sessionPath);
206
+ if (entry.reservationKey)
207
+ this.reservations.delete(entry.reservationKey);
170
208
  }
171
209
  })();
172
210
  return entry.lifecycleMutex;
173
211
  }
174
212
  list() {
213
+ this.syncRuntimeMetadata();
175
214
  return [...this.entries].map(([sessionId, entry]) => ({
176
215
  sessionId,
177
216
  durableSessionId: entry.durableSessionId,
178
217
  sessionPath: entry.sessionPath,
179
- cwd: entry.profile.cwd,
218
+ cwd: entry.cwd,
180
219
  name: entry.runtime?.session.sessionManager.getSessionName(),
181
220
  status: entry.state,
182
221
  }));
183
222
  }
223
+ /** Reconcile path and durable identity after runtime replacement. */
224
+ syncRuntimeMetadata() {
225
+ for (const entry of this.entries.values()) {
226
+ const manager = entry.runtime?.session.sessionManager;
227
+ if (!manager)
228
+ continue;
229
+ const currentPath = manager.getSessionFile();
230
+ // Preserve the originally canonicalized key while the runtime still points at
231
+ // the same path. SessionManager may expose a symlink-resolved spelling after
232
+ // opening a file that did not exist yet; treating that as replacement would
233
+ // break ordinary attach-on-open aliases.
234
+ if (currentPath !== entry.sessionPath) {
235
+ const currentKey = currentPath ? canonicalPath(currentPath) : undefined;
236
+ if (entry.reservationKey)
237
+ this.reservations.delete(entry.reservationKey);
238
+ if (currentKey)
239
+ this.reservations.add(currentKey);
240
+ entry.reservationKey = currentKey;
241
+ entry.sessionPath = currentPath;
242
+ }
243
+ entry.durableSessionId = manager.getSessionId();
244
+ entry.cwd = manager.getCwd();
245
+ }
246
+ }
184
247
  validateProfile(profile) {
185
248
  if (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath))) {
186
249
  throw new RpcSessionRegistryError("invalid_path");
@@ -1 +1 @@
1
- {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../../src/modes/rpc/session-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, realpathSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, resolve } from \"node:path\";\nimport { ProviderScope, runWithProviderScope } from \"@earendil-works/pi-ai/node/provider-scope\";\nimport {\n\ttype AgentSessionLaunchProfile,\n\ttype AgentSessionRuntime,\n\ttype CreateAgentSessionRuntimeFactory,\n\tcreateAgentSessionRuntime,\n} from \"../../core/agent-session-runtime.ts\";\nimport { SessionManager } from \"../../core/session-manager.ts\";\n\n/** The immutable flags selected when a routing session is opened. */\nexport interface RpcSessionLaunchProfile extends AgentSessionLaunchProfile {\n\tsessionPath?: string;\n}\n\nexport type SessionRuntime = Pick<AgentSessionRuntime, \"session\" | \"dispose\">;\nexport type RpcSessionState = \"opening\" | \"open\" | \"closing\" | \"closed\";\n\nexport interface RpcSessionEntry {\n\tstate: RpcSessionState;\n\truntime?: SessionRuntime;\n\tscope: ProviderScope;\n\tprofile: Readonly<RpcSessionLaunchProfile>;\n\tdurableSessionId?: string;\n\tsessionPath?: string;\n\t/** Canonical reservation key for path-opened sessions; matches the reservations set. */\n\treservationKey?: string;\n\t/** Live attachments (open + later attaches). The runtime is disposed only when the last one closes. */\n\tattachments: number;\n\tlifecycleMutex: Promise<void>;\n}\n\nexport class RpcSessionRegistryError extends Error {\n\treadonly code: \"unknown_session\" | \"session_closing\" | \"session_path_in_use\" | \"invalid_path\" | \"open_failed\";\n\n\tconstructor(code: RpcSessionRegistryError[\"code\"], reason?: string) {\n\t\tsuper(code === \"open_failed\" && reason ? `${code}: ${reason}` : code);\n\t\tthis.code = code;\n\t\tthis.name = \"RpcSessionRegistryError\";\n\t}\n}\n\nexport interface RpcSessionRegistryOptions {\n\tagentDir: string;\n\tcreateRuntime: CreateAgentSessionRuntimeFactory;\n}\n\nexport interface OpenRpcSession {\n\tsessionId: string;\n\tdurableSessionId: string;\n\tsessionPath?: string;\n\t/** True when this open attached to an already-open session instead of creating one. */\n\tattached?: boolean;\n}\n\nfunction canonicalPath(path: string): string {\n\tconst absolutePath = resolve(path);\n\tif (existsSync(absolutePath)) return realpathSync(absolutePath);\n\treturn `${realpathSync(dirname(absolutePath))}/${basename(absolutePath)}`;\n}\n\nfunction frozenProfile(profile: RpcSessionLaunchProfile): Readonly<RpcSessionLaunchProfile> {\n\treturn Object.freeze({\n\t\t...profile,\n\t\t...(profile.creationModel ? { creationModel: Object.freeze({ ...profile.creationModel }) } : {}),\n\t});\n}\n\n/** Process-local lifecycle owner for multi-session RPC runtimes. */\nexport class RpcSessionRegistry {\n\tprivate readonly entries = new Map<string, RpcSessionEntry>();\n\tprivate readonly reservations = new Set<string>();\n\tprivate nextHandle = 0;\n\tprivate readonly options: RpcSessionRegistryOptions;\n\n\tconstructor(options: RpcSessionRegistryOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync openSession(profile: RpcSessionLaunchProfile): Promise<OpenRpcSession> {\n\t\tthis.validateProfile(profile);\n\t\tconst sessionPath = profile.sessionPath ? canonicalPath(profile.sessionPath) : undefined;\n\t\tif (sessionPath && this.reservations.has(sessionPath)) {\n\t\t\t// Attach-on-open: a live session outlives individual client attachments, so a\n\t\t\t// resume (or a second surface) for an already-hosted path joins the existing\n\t\t\t// runtime instead of failing. Entries still opening or closing keep the\n\t\t\t// exclusive reservation and reject as before.\n\t\t\tconst existing = [...this.entries].find(\n\t\t\t\t([, entry]) => entry.reservationKey === sessionPath && entry.state === \"open\",\n\t\t\t);\n\t\t\tif (!existing) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\tconst [handle, entry] = existing;\n\t\t\tentry.attachments += 1;\n\t\t\tif (!entry.durableSessionId) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\treturn {\n\t\t\t\tsessionId: handle,\n\t\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\t\tsessionPath: entry.sessionPath,\n\t\t\t\tattached: true,\n\t\t\t};\n\t\t}\n\t\tif (sessionPath) this.reservations.add(sessionPath);\n\n\t\t// Resume vs create parity (D1 + omo SenpiSessionRuntime.ts:198-200):\n\t\t// Create-only launch semantics mirror classic startup flags. A resumed\n\t\t// session restores its persisted model and thinking level instead of being\n\t\t// overridden by the new open_session request.\n\t\tconst isResume = sessionPath !== undefined && existsSync(sessionPath);\n\t\tconst storedProfile = frozenProfile({ ...profile, ...(sessionPath ? { sessionPath } : {}) });\n\t\tconst runtimeProfile = isResume\n\t\t\t? frozenProfile({ ...storedProfile, creationModel: undefined, initialThinkingLevel: undefined })\n\t\t\t: storedProfile;\n\n\t\tconst handle = `rpc-${++this.nextHandle}`;\n\t\tconst entry: RpcSessionEntry = {\n\t\t\tstate: \"opening\",\n\t\t\tscope: new ProviderScope(),\n\t\t\tprofile: storedProfile,\n\t\t\tsessionPath,\n\t\t\treservationKey: sessionPath,\n\t\t\tattachments: 1,\n\t\t\tlifecycleMutex: Promise.resolve(),\n\t\t};\n\t\tthis.entries.set(handle, entry);\n\t\ttry {\n\t\t\tconst manager = sessionPath\n\t\t\t\t? SessionManager.open(sessionPath, undefined, storedProfile.cwd)\n\t\t\t\t: SessionManager.create(storedProfile.cwd);\n\t\t\tentry.runtime = await runWithProviderScope(entry.scope, () =>\n\t\t\t\tcreateAgentSessionRuntime(this.options.createRuntime, {\n\t\t\t\t\tcwd: manager.getCwd(),\n\t\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\t\tsessionManager: manager,\n\t\t\t\t\tlaunchProfile: runtimeProfile,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.sessionPath ??= manager.getSessionFile();\n\t\t\tentry.state = \"open\";\n\t\t\treturn { sessionId: handle, durableSessionId: entry.durableSessionId, sessionPath: entry.sessionPath };\n\t\t} catch (error) {\n\t\t\t// Runtime construction may have started extensions, watchers, and provider\n\t\t\t// registrations before it rejects. Keep the reservation and entry private\n\t\t\t// until all of those resources have been torn down, then release them as\n\t\t\t// one rollback so the path can immediately be opened again.\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t} catch {\n\t\t\t\t// The original construction error remains the externally visible cause.\n\t\t\t} finally {\n\t\t\t\ttry {\n\t\t\t\t\tawait entry.scope.close?.();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.entries.delete(handle);\n\t\t\t\t\tif (sessionPath) this.reservations.delete(sessionPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (error instanceof RpcSessionRegistryError) throw error;\n\t\t\tthrow new RpcSessionRegistryError(\"open_failed\", error instanceof Error ? error.message : undefined);\n\t\t}\n\t}\n\n\tgetForCommand(handle: string, command: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (entry.state === \"closing\" && ![\"abort\", \"abort_bash\", \"extension_ui_response\"].includes(command)) {\n\t\t\tthrow new RpcSessionRegistryError(\"session_closing\");\n\t\t}\n\t\tif (entry.state !== \"open\" && entry.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\treturn entry;\n\t}\n\n\t/**\n\t * Starts a close synchronously. Call this before disposing any session-owned\n\t * binding so a concurrent command cannot reach a half-disposed handler.\n\t * Each call releases one attachment; the entry transitions to \"closing\" only\n\t * when the last attachment closes, so callers must check the returned state\n\t * and skip finalization while other attachments remain.\n\t */\n\tbeginClose(handle: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"open\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.attachments -= 1;\n\t\tif (entry.attachments > 0) return entry;\n\t\tentry.state = \"closing\";\n\t\treturn entry;\n\t}\n\n\tasync close(handle: string): Promise<void> {\n\t\tconst entry = this.beginClose(handle);\n\t\t// Other attachments may still own the live session; only the last close finalizes.\n\t\tif (entry.state !== \"closing\") return;\n\t\treturn this.closeMarked(handle);\n\t}\n\n\t/** Completes a close previously made visible by beginClose(). */\n\tasync closeMarked(handle: string): Promise<void> {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.lifecycleMutex = (async () => {\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.session.abort();\n\t\t\t\tawait entry.runtime?.session.waitForIdle();\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t\tawait entry.scope.close?.();\n\t\t\t} finally {\n\t\t\t\tentry.state = \"closed\";\n\t\t\t\tthis.entries.delete(handle);\n\t\t\t\tif (entry.sessionPath) this.reservations.delete(entry.sessionPath);\n\t\t\t}\n\t\t})();\n\t\treturn entry.lifecycleMutex;\n\t}\n\n\tlist(): Array<{\n\t\tsessionId: string;\n\t\tdurableSessionId?: string;\n\t\tsessionPath?: string;\n\t\tcwd: string;\n\t\tname?: string;\n\t\tstatus: RpcSessionState;\n\t}> {\n\t\treturn [...this.entries].map(([sessionId, entry]) => ({\n\t\t\tsessionId,\n\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\tsessionPath: entry.sessionPath,\n\t\t\tcwd: entry.profile.cwd,\n\t\t\tname: entry.runtime?.session.sessionManager.getSessionName(),\n\t\t\tstatus: entry.state,\n\t\t}));\n\t}\n\n\tprivate validateProfile(profile: RpcSessionLaunchProfile): void {\n\t\tif (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath))) {\n\t\t\tthrow new RpcSessionRegistryError(\"invalid_path\");\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"session-registry.js","sourceRoot":"","sources":["../../../src/modes/rpc/session-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, realpathSync } from \"node:fs\";\nimport { basename, dirname, isAbsolute, resolve } from \"node:path\";\nimport { ProviderScope, runWithProviderScope } from \"@earendil-works/pi-ai/node/provider-scope\";\nimport {\n\ttype AgentSessionLaunchProfile,\n\tAgentSessionRuntime,\n\ttype CreateAgentSessionRuntimeFactory,\n\tcreateAgentSessionRuntime,\n} from \"../../core/agent-session-runtime.ts\";\nimport { SessionManager } from \"../../core/session-manager.ts\";\n\n/** The immutable flags selected when a routing session is opened. */\nexport interface RpcSessionLaunchProfile extends AgentSessionLaunchProfile {\n\tsessionPath?: string;\n}\n\nexport type SessionRuntime = AgentSessionRuntime;\nexport type RpcSessionState = \"opening\" | \"open\" | \"closing\" | \"closed\";\n\nexport interface RpcSessionEntry {\n\tstate: RpcSessionState;\n\truntime?: SessionRuntime;\n\t/** Resolves replacement against the runtime currently owned by this entry. */\n\tswitchSession?: SessionRuntime[\"switchSession\"];\n\t/** Rebind callback installed by the shared RPC connection handler. */\n\trebindSession?: Parameters<SessionRuntime[\"setRebindSession\"]>[0];\n\tscope: ProviderScope;\n\tprofile: Readonly<RpcSessionLaunchProfile>;\n\tdurableSessionId?: string;\n\tsessionPath?: string;\n\t/** Canonical reservation key for path-opened sessions; matches the reservations set. */\n\treservationKey?: string;\n\t/** Current runtime cwd, which can change when a session is replaced. */\n\tcwd: string;\n\t/** Live attachments (open + later attaches). The runtime is disposed only when the last one closes. */\n\tattachments: number;\n\tlifecycleMutex: Promise<void>;\n}\n\nexport class RpcSessionRegistryError extends Error {\n\treadonly code: \"unknown_session\" | \"session_closing\" | \"session_path_in_use\" | \"invalid_path\" | \"open_failed\";\n\n\tconstructor(code: RpcSessionRegistryError[\"code\"], reason?: string) {\n\t\tsuper(code === \"open_failed\" && reason ? `${code}: ${reason}` : code);\n\t\tthis.code = code;\n\t\tthis.name = \"RpcSessionRegistryError\";\n\t}\n}\n\nexport interface RpcSessionRegistryOptions {\n\tagentDir: string;\n\tcreateRuntime: CreateAgentSessionRuntimeFactory;\n}\n\nexport interface OpenRpcSession {\n\tsessionId: string;\n\tdurableSessionId: string;\n\tsessionPath?: string;\n\t/** True when this open attached to an already-open session instead of creating one. */\n\tattached?: boolean;\n}\n\nfunction canonicalPath(path: string): string {\n\tconst absolutePath = resolve(path);\n\tif (existsSync(absolutePath)) return realpathSync(absolutePath);\n\treturn `${realpathSync(dirname(absolutePath))}/${basename(absolutePath)}`;\n}\n\nfunction frozenProfile(profile: RpcSessionLaunchProfile): Readonly<RpcSessionLaunchProfile> {\n\treturn Object.freeze({\n\t\t...profile,\n\t\t...(profile.creationModel ? { creationModel: Object.freeze({ ...profile.creationModel }) } : {}),\n\t});\n}\n\n/** Process-local lifecycle owner for multi-session RPC runtimes. */\nexport class RpcSessionRegistry {\n\tprivate readonly entries = new Map<string, RpcSessionEntry>();\n\tprivate readonly reservations = new Set<string>();\n\tprivate nextHandle = 0;\n\tprivate readonly options: RpcSessionRegistryOptions;\n\n\tconstructor(options: RpcSessionRegistryOptions) {\n\t\tthis.options = options;\n\t}\n\n\tasync openSession(profile: RpcSessionLaunchProfile): Promise<OpenRpcSession> {\n\t\tthis.validateProfile(profile);\n\t\tthis.syncRuntimeMetadata();\n\t\tconst sessionPath = profile.sessionPath ? canonicalPath(profile.sessionPath) : undefined;\n\t\tif (sessionPath && this.reservations.has(sessionPath)) {\n\t\t\t// Attach-on-open: a live session outlives individual client attachments, so a\n\t\t\t// resume (or a second surface) for an already-hosted path joins the existing\n\t\t\t// runtime instead of failing. Entries still opening or closing keep the\n\t\t\t// exclusive reservation and reject as before.\n\t\t\tconst existing = [...this.entries].find(\n\t\t\t\t([, entry]) => entry.reservationKey === sessionPath && entry.state === \"open\",\n\t\t\t);\n\t\t\tif (!existing) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\tconst [handle, entry] = existing;\n\t\t\tentry.attachments += 1;\n\t\t\tif (!entry.durableSessionId) throw new RpcSessionRegistryError(\"session_path_in_use\");\n\t\t\treturn {\n\t\t\t\tsessionId: handle,\n\t\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\t\tsessionPath: entry.sessionPath,\n\t\t\t\tattached: true,\n\t\t\t};\n\t\t}\n\t\tif (sessionPath) this.reservations.add(sessionPath);\n\n\t\t// Resume vs create parity (D1 + omo SenpiSessionRuntime.ts:198-200):\n\t\t// Create-only launch semantics mirror classic startup flags. A resumed\n\t\t// session restores its persisted model and thinking level instead of being\n\t\t// overridden by the new open_session request.\n\t\tconst isResume = sessionPath !== undefined && existsSync(sessionPath);\n\t\tconst storedProfile = frozenProfile({ ...profile, ...(sessionPath ? { sessionPath } : {}) });\n\t\tconst runtimeProfile = isResume\n\t\t\t? frozenProfile({ ...storedProfile, creationModel: undefined, initialThinkingLevel: undefined })\n\t\t\t: storedProfile;\n\n\t\tconst handle = `rpc-${++this.nextHandle}`;\n\t\tconst entry: RpcSessionEntry = {\n\t\t\tstate: \"opening\",\n\t\t\tscope: new ProviderScope(),\n\t\t\tprofile: storedProfile,\n\t\t\tsessionPath,\n\t\t\treservationKey: sessionPath,\n\t\t\tcwd: storedProfile.cwd,\n\t\t\tattachments: 1,\n\t\t\tlifecycleMutex: Promise.resolve(),\n\t\t};\n\t\tentry.switchSession = (sessionPath, options) => {\n\t\t\tconst operation = entry.lifecycleMutex.then(async () => {\n\t\t\t\tif (entry.state !== \"open\" || !entry.runtime) {\n\t\t\t\t\tthrow new RpcSessionRegistryError(\"unknown_session\");\n\t\t\t\t}\n\t\t\t\tconst runtime = entry.runtime;\n\t\t\t\tconst cwdOverride = options?.cwdOverride;\n\t\t\t\tconst cwdChanged = cwdOverride !== undefined && runtime.session.sessionManager.getCwd() !== cwdOverride;\n\t\t\t\tconst result = await runtime.switchSession(sessionPath, options);\n\t\t\t\tif (result.cancelled || !cwdChanged) return result;\n\n\t\t\t\t// A multi-session binding outlives an individual replacement. Keep the\n\t\t\t\t// entry's runtime object aligned with the replacement so every attached\n\t\t\t\t// client resolves getters and future commands against the new cwd-bound\n\t\t\t\t// runtime, not the object created during open_session.\n\t\t\t\tconst replacement = new AgentSessionRuntime(\n\t\t\t\t\truntime.session,\n\t\t\t\t\truntime.services,\n\t\t\t\t\tthis.options.createRuntime,\n\t\t\t\t\t[...runtime.diagnostics],\n\t\t\t\t\truntime.modelFallbackMessage,\n\t\t\t\t\truntime.launchProfile,\n\t\t\t\t);\n\t\t\t\treplacement.setRebindSession(entry.rebindSession);\n\t\t\t\tentry.runtime = replacement;\n\t\t\t\tthis.syncRuntimeMetadata();\n\t\t\t\treturn result;\n\t\t\t});\n\t\t\tentry.lifecycleMutex = operation.then(\n\t\t\t\t() => undefined,\n\t\t\t\t() => undefined,\n\t\t\t);\n\t\t\treturn operation;\n\t\t};\n\t\tthis.entries.set(handle, entry);\n\t\ttry {\n\t\t\tconst manager = sessionPath\n\t\t\t\t? SessionManager.open(sessionPath, undefined, storedProfile.cwd)\n\t\t\t\t: SessionManager.create(storedProfile.cwd);\n\t\t\tentry.runtime = await runWithProviderScope(entry.scope, () =>\n\t\t\t\tcreateAgentSessionRuntime(this.options.createRuntime, {\n\t\t\t\t\tcwd: manager.getCwd(),\n\t\t\t\t\tagentDir: this.options.agentDir,\n\t\t\t\t\tsessionManager: manager,\n\t\t\t\t\tlaunchProfile: runtimeProfile,\n\t\t\t\t}),\n\t\t\t);\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.sessionPath ??= manager.getSessionFile();\n\t\t\tentry.state = \"open\";\n\t\t\treturn { sessionId: handle, durableSessionId: entry.durableSessionId, sessionPath: entry.sessionPath };\n\t\t} catch (error) {\n\t\t\t// Runtime construction may have started extensions, watchers, and provider\n\t\t\t// registrations before it rejects. Keep the reservation and entry private\n\t\t\t// until all of those resources have been torn down, then release them as\n\t\t\t// one rollback so the path can immediately be opened again.\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t} catch {\n\t\t\t\t// The original construction error remains the externally visible cause.\n\t\t\t} finally {\n\t\t\t\ttry {\n\t\t\t\t\tawait entry.scope.close?.();\n\t\t\t\t} finally {\n\t\t\t\t\tthis.entries.delete(handle);\n\t\t\t\t\tif (sessionPath) this.reservations.delete(sessionPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (error instanceof RpcSessionRegistryError) throw error;\n\t\t\tthrow new RpcSessionRegistryError(\"open_failed\", error instanceof Error ? error.message : undefined);\n\t\t}\n\t}\n\n\t/**\n\t * Read-only lookup with no state transitions or attachment accounting.\n\t * Exists so lifecycle decisions (e.g. deferring a dropped connection's\n\t * release while a turn is still streaming) can inspect the live entry\n\t * without claiming it.\n\t */\n\tpeek(handle: string): RpcSessionEntry | undefined {\n\t\treturn this.entries.get(handle);\n\t}\n\n\tgetForCommand(handle: string, command: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (!entry) throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tif (entry.state === \"closing\" && ![\"abort\", \"abort_bash\", \"extension_ui_response\"].includes(command)) {\n\t\t\tthrow new RpcSessionRegistryError(\"session_closing\");\n\t\t}\n\t\tif (entry.state !== \"open\" && entry.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\treturn entry;\n\t}\n\n\t/**\n\t * Starts a close synchronously. Call this before disposing any session-owned\n\t * binding so a concurrent command cannot reach a half-disposed handler.\n\t * Each call releases one attachment; the entry transitions to \"closing\" only\n\t * when the last attachment closes, so callers must check the returned state\n\t * and skip finalization while other attachments remain.\n\t */\n\tbeginClose(handle: string): RpcSessionEntry {\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"open\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tentry.attachments -= 1;\n\t\tif (entry.attachments > 0) return entry;\n\t\tentry.state = \"closing\";\n\t\treturn entry;\n\t}\n\n\tasync close(handle: string): Promise<void> {\n\t\tconst entry = this.beginClose(handle);\n\t\t// Other attachments may still own the live session; only the last close finalizes.\n\t\tif (entry.state !== \"closing\") return;\n\t\treturn this.closeMarked(handle);\n\t}\n\n\t/** Completes a close previously made visible by beginClose(). */\n\tasync closeMarked(handle: string): Promise<void> {\n\t\tthis.syncRuntimeMetadata();\n\t\tconst entry = this.entries.get(handle);\n\t\tif (entry?.state !== \"closing\") throw new RpcSessionRegistryError(\"unknown_session\");\n\t\tconst previousLifecycle = entry.lifecycleMutex;\n\t\tentry.lifecycleMutex = (async () => {\n\t\t\tawait previousLifecycle;\n\t\t\ttry {\n\t\t\t\tawait entry.runtime?.session.abort();\n\t\t\t\tawait entry.runtime?.session.waitForIdle();\n\t\t\t\tawait entry.runtime?.dispose();\n\t\t\t\tawait entry.scope.close?.();\n\t\t\t} finally {\n\t\t\t\tentry.state = \"closed\";\n\t\t\t\tthis.entries.delete(handle);\n\t\t\t\tif (entry.reservationKey) this.reservations.delete(entry.reservationKey);\n\t\t\t}\n\t\t})();\n\t\treturn entry.lifecycleMutex;\n\t}\n\n\tlist(): Array<{\n\t\tsessionId: string;\n\t\tdurableSessionId?: string;\n\t\tsessionPath?: string;\n\t\tcwd: string;\n\t\tname?: string;\n\t\tstatus: RpcSessionState;\n\t}> {\n\t\tthis.syncRuntimeMetadata();\n\t\treturn [...this.entries].map(([sessionId, entry]) => ({\n\t\t\tsessionId,\n\t\t\tdurableSessionId: entry.durableSessionId,\n\t\t\tsessionPath: entry.sessionPath,\n\t\t\tcwd: entry.cwd,\n\t\t\tname: entry.runtime?.session.sessionManager.getSessionName(),\n\t\t\tstatus: entry.state,\n\t\t}));\n\t}\n\n\t/** Reconcile path and durable identity after runtime replacement. */\n\tprivate syncRuntimeMetadata(): void {\n\t\tfor (const entry of this.entries.values()) {\n\t\t\tconst manager = entry.runtime?.session.sessionManager;\n\t\t\tif (!manager) continue;\n\t\t\tconst currentPath = manager.getSessionFile();\n\t\t\t// Preserve the originally canonicalized key while the runtime still points at\n\t\t\t// the same path. SessionManager may expose a symlink-resolved spelling after\n\t\t\t// opening a file that did not exist yet; treating that as replacement would\n\t\t\t// break ordinary attach-on-open aliases.\n\t\t\tif (currentPath !== entry.sessionPath) {\n\t\t\t\tconst currentKey = currentPath ? canonicalPath(currentPath) : undefined;\n\t\t\t\tif (entry.reservationKey) this.reservations.delete(entry.reservationKey);\n\t\t\t\tif (currentKey) this.reservations.add(currentKey);\n\t\t\t\tentry.reservationKey = currentKey;\n\t\t\t\tentry.sessionPath = currentPath;\n\t\t\t}\n\t\t\tentry.durableSessionId = manager.getSessionId();\n\t\t\tentry.cwd = manager.getCwd();\n\t\t}\n\t}\n\n\tprivate validateProfile(profile: RpcSessionLaunchProfile): void {\n\t\tif (!isAbsolute(profile.cwd) || (profile.sessionPath !== undefined && !isAbsolute(profile.sessionPath))) {\n\t\t\tthrow new RpcSessionRegistryError(\"invalid_path\");\n\t\t}\n\t}\n}\n"]}
@@ -0,0 +1,22 @@
1
+ type SocketSink = {
2
+ writeRaw(chunk: string): void;
3
+ waitForBackpressure(): Promise<void>;
4
+ };
5
+ /** Independent FIFO actor for one socket. It never shares a drain promise with another sink. */
6
+ export declare class SocketEventSinkActor {
7
+ private readonly queue;
8
+ private queuedBytes;
9
+ private draining?;
10
+ private closed;
11
+ private failure?;
12
+ private readonly sink;
13
+ private readonly onFailure;
14
+ private readonly maxQueueBytes;
15
+ constructor(sink: SocketSink, onFailure: (cause: unknown) => void, maxQueueBytes?: number);
16
+ enqueue(line: string, key?: string, onWritten?: () => void): void;
17
+ flush(): Promise<void>;
18
+ close(): void;
19
+ private drain;
20
+ }
21
+ export {};
22
+ //# sourceMappingURL=socket-event-fanout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-event-fanout.d.ts","sourceRoot":"","sources":["../../../src/modes/rpc/socket-event-fanout.ts"],"names":[],"mappings":"AAAA,KAAK,UAAU,GAAG;IACjB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC,CAAC;AAMF,gGAAgG;AAChG,qBAAa,oBAAoB;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAgB;IACjC,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAC,CAAU;IAE1B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2B;IACrD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,EAAE,aAAa,SAAsB;IAMtG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI;IA6B3D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,KAAK;CA0Bb"}
@@ -0,0 +1,84 @@
1
+ const DEFAULT_QUEUE_BYTES = 4 * 1024 * 1024;
2
+ /** Independent FIFO actor for one socket. It never shares a drain promise with another sink. */
3
+ export class SocketEventSinkActor {
4
+ constructor(sink, onFailure, maxQueueBytes = DEFAULT_QUEUE_BYTES) {
5
+ this.queue = [];
6
+ this.queuedBytes = 0;
7
+ this.closed = false;
8
+ this.sink = sink;
9
+ this.onFailure = onFailure;
10
+ this.maxQueueBytes = maxQueueBytes;
11
+ }
12
+ enqueue(line, key, onWritten) {
13
+ if (this.closed)
14
+ return;
15
+ const bytes = Buffer.byteLength(line);
16
+ if (key !== undefined) {
17
+ const existing = this.queue.find((entry) => entry.key === key);
18
+ if (existing) {
19
+ this.queuedBytes += bytes - existing.bytes;
20
+ existing.line = line;
21
+ existing.bytes = bytes;
22
+ if (this.queuedBytes <= this.maxQueueBytes)
23
+ return;
24
+ }
25
+ }
26
+ if (this.queuedBytes + bytes > this.maxQueueBytes) {
27
+ this.closed = true;
28
+ this.queue.length = 0;
29
+ this.queuedBytes = 0;
30
+ try {
31
+ this.sink.writeRaw(`${JSON.stringify({ type: "overflow", error: "overflow, resync required" })}\n`);
32
+ }
33
+ catch (cause) {
34
+ this.onFailure(cause);
35
+ }
36
+ this.onFailure(new Error("socket event queue overflow"));
37
+ return;
38
+ }
39
+ this.queue.push({ line, bytes, key, onWritten });
40
+ this.queuedBytes += bytes;
41
+ void this.drain();
42
+ }
43
+ async flush() {
44
+ while (this.draining)
45
+ await this.draining;
46
+ if (this.failure !== undefined)
47
+ throw this.failure;
48
+ }
49
+ close() {
50
+ this.closed = true;
51
+ this.queue.length = 0;
52
+ this.queuedBytes = 0;
53
+ }
54
+ drain() {
55
+ if (this.draining)
56
+ return this.draining;
57
+ this.draining = (async () => {
58
+ try {
59
+ while (!this.closed && this.queue.length > 0) {
60
+ const entry = this.queue.shift();
61
+ this.queuedBytes -= entry.bytes;
62
+ this.sink.writeRaw(entry.line);
63
+ await this.sink.waitForBackpressure();
64
+ entry.onWritten?.();
65
+ }
66
+ }
67
+ catch (cause) {
68
+ this.failure = cause;
69
+ this.closed = true;
70
+ this.queue.length = 0;
71
+ this.queuedBytes = 0;
72
+ this.onFailure(cause);
73
+ }
74
+ })().finally(() => {
75
+ this.draining = undefined;
76
+ // An enqueue that lands between the loop's exit and this reaction sees the
77
+ // stale settled promise and starts nothing; reschedule for it here.
78
+ if (!this.closed && this.queue.length > 0)
79
+ void this.drain();
80
+ });
81
+ return this.draining;
82
+ }
83
+ }
84
+ //# sourceMappingURL=socket-event-fanout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-event-fanout.js","sourceRoot":"","sources":["../../../src/modes/rpc/socket-event-fanout.ts"],"names":[],"mappings":"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","sourcesContent":["type SocketSink = {\n\twriteRaw(chunk: string): void;\n\twaitForBackpressure(): Promise<void>;\n};\n\ntype QueueEntry = { line: string; bytes: number; key?: string; onWritten?: () => void };\n\nconst DEFAULT_QUEUE_BYTES = 4 * 1024 * 1024;\n\n/** Independent FIFO actor for one socket. It never shares a drain promise with another sink. */\nexport class SocketEventSinkActor {\n\tprivate readonly queue: QueueEntry[] = [];\n\tprivate queuedBytes = 0;\n\tprivate draining?: Promise<void>;\n\tprivate closed = false;\n\tprivate failure?: unknown;\n\n\tprivate readonly sink: SocketSink;\n\tprivate readonly onFailure: (cause: unknown) => void;\n\tprivate readonly maxQueueBytes: number;\n\n\tconstructor(sink: SocketSink, onFailure: (cause: unknown) => void, maxQueueBytes = DEFAULT_QUEUE_BYTES) {\n\t\tthis.sink = sink;\n\t\tthis.onFailure = onFailure;\n\t\tthis.maxQueueBytes = maxQueueBytes;\n\t}\n\n\tenqueue(line: string, key?: string, onWritten?: () => void): void {\n\t\tif (this.closed) return;\n\t\tconst bytes = Buffer.byteLength(line);\n\t\tif (key !== undefined) {\n\t\t\tconst existing = this.queue.find((entry) => entry.key === key);\n\t\t\tif (existing) {\n\t\t\t\tthis.queuedBytes += bytes - existing.bytes;\n\t\t\t\texisting.line = line;\n\t\t\t\texisting.bytes = bytes;\n\t\t\t\tif (this.queuedBytes <= this.maxQueueBytes) return;\n\t\t\t}\n\t\t}\n\t\tif (this.queuedBytes + bytes > this.maxQueueBytes) {\n\t\t\tthis.closed = true;\n\t\t\tthis.queue.length = 0;\n\t\t\tthis.queuedBytes = 0;\n\t\t\ttry {\n\t\t\t\tthis.sink.writeRaw(`${JSON.stringify({ type: \"overflow\", error: \"overflow, resync required\" })}\\n`);\n\t\t\t} catch (cause) {\n\t\t\t\tthis.onFailure(cause);\n\t\t\t}\n\t\t\tthis.onFailure(new Error(\"socket event queue overflow\"));\n\t\t\treturn;\n\t\t}\n\t\tthis.queue.push({ line, bytes, key, onWritten });\n\t\tthis.queuedBytes += bytes;\n\t\tvoid this.drain();\n\t}\n\n\tasync flush(): Promise<void> {\n\t\twhile (this.draining) await this.draining;\n\t\tif (this.failure !== undefined) throw this.failure;\n\t}\n\n\tclose(): void {\n\t\tthis.closed = true;\n\t\tthis.queue.length = 0;\n\t\tthis.queuedBytes = 0;\n\t}\n\n\tprivate drain(): Promise<void> {\n\t\tif (this.draining) return this.draining;\n\t\tthis.draining = (async () => {\n\t\t\ttry {\n\t\t\t\twhile (!this.closed && this.queue.length > 0) {\n\t\t\t\t\tconst entry = this.queue.shift()!;\n\t\t\t\t\tthis.queuedBytes -= entry.bytes;\n\t\t\t\t\tthis.sink.writeRaw(entry.line);\n\t\t\t\t\tawait this.sink.waitForBackpressure();\n\t\t\t\t\tentry.onWritten?.();\n\t\t\t\t}\n\t\t\t} catch (cause) {\n\t\t\t\tthis.failure = cause;\n\t\t\t\tthis.closed = true;\n\t\t\t\tthis.queue.length = 0;\n\t\t\t\tthis.queuedBytes = 0;\n\t\t\t\tthis.onFailure(cause);\n\t\t\t}\n\t\t})().finally(() => {\n\t\t\tthis.draining = undefined;\n\t\t\t// An enqueue that lands between the loop's exit and this reaction sees the\n\t\t\t// stale settled promise and starts nothing; reschedule for it here.\n\t\t\tif (!this.closed && this.queue.length > 0) void this.drain();\n\t\t});\n\t\treturn this.draining;\n\t}\n}\n"]}
package/docs/rpc.md CHANGED
@@ -57,6 +57,19 @@ every open session, each tagged with its routing `sessionId`. Correlated respons
57
57
  only to the connection that issued the command. This lets a non-owner observe a foreign turn without requiring a
58
58
  separate subscription protocol.
59
59
 
60
+ ### Session auto-titling
61
+
62
+ Auto-generated session titles are on by default only for interactive launches. RPC hosts opt in with
63
+ `--auto-title-sessions`:
64
+
65
+ ```bash
66
+ senpi --mode rpc --multi-session --auto-title-sessions
67
+ ```
68
+
69
+ With the flag, every session the host opens (classic or multi-session) generates a title from its first user prompt and
70
+ publishes it to clients through the existing `session_info_changed` event; no new command or event is involved. Sessions
71
+ resumed with existing context messages are never retitled, with or without the flag.
72
+
60
73
  Startup: `senpi --mode rpc --multi-session` → NO default session is constructed (no default `AgentSessionRuntime`, no default extension/watcher load). Classic `senpi --mode rpc` is byte-identical to today. Mode is fixed at process start; there is no runtime transition.
61
74
 
62
75
  ### Interactive sessions and shared-host opt-out
@@ -831,9 +844,11 @@ Response:
831
844
  Load a different session file. Can be cancelled by a `session_before_switch` extension event handler.
832
845
 
833
846
  ```json
834
- {"type": "switch_session", "sessionPath": "/path/to/session.jsonl"}
847
+ {"type": "switch_session", "sessionPath": "/path/to/session.jsonl", "cwdOverride": "/path/to/project"}
835
848
  ```
836
849
 
850
+ `cwdOverride` is optional. When supplied, the replacement session and its cwd-bound settings and runtime state are rebuilt for that directory instead of using the session file's stored cwd. The host also resolves `projectTrusted` from its saved project trust store for the replacement cwd; an absent or false decision keeps project-scoped settings and resources disabled.
851
+
837
852
  Response:
838
853
  ```json
839
854
  {"type": "response", "command": "switch_session", "success": true, "data": {"cancelled": false}}
@@ -1237,6 +1252,30 @@ Emitted when an extension calls `pi.rpc.emit(name, data)` and the client adverti
1237
1252
  the specific event name before applying it. In multi-session mode the record also includes the
1238
1253
  routing `sessionId`; delivery preserves the owning session and per-session event order.
1239
1254
 
1255
+ The terminal builtin emits `terminal_monitor_state` on this path whenever the active monitor set
1256
+ changes. `data` is `{ activeCount, monitors }`, where each monitor entry has `id`, `description`,
1257
+ `paused`, and `startedAtMs`. The matching in-process `pi.events` channel is unchanged and is not
1258
+ forwarded. Clients receive the wire record only when they advertise the `extension_events`
1259
+ capability:
1260
+
1261
+ ```json
1262
+ {
1263
+ "type": "extension_event",
1264
+ "name": "terminal_monitor_state",
1265
+ "data": {
1266
+ "activeCount": 1,
1267
+ "monitors": [
1268
+ {
1269
+ "id": "bash_1",
1270
+ "description": "watch checks",
1271
+ "paused": false,
1272
+ "startedAtMs": 1710000000000
1273
+ }
1274
+ ]
1275
+ }
1276
+ }
1277
+ ```
1278
+
1240
1279
  ### agent_start
1241
1280
 
1242
1281
  Emitted when the agent begins processing a prompt.
package/docs/settings.md CHANGED
@@ -399,6 +399,8 @@ Windows paths in JSON must use forward slashes or escaped backslashes:
399
399
  | Setting | Type | Default | Description |
400
400
  |---------|------|---------|-------------|
401
401
  | `defaultTools` | string[] | - | Built-in tools enabled initially. When omitted, Pi uses its standard defaults |
402
+ | `experimental.bashEvalOnly` | boolean | `false` | Route `bash` and `powershell` through eval cells only; the tools leave the model's direct tool list |
403
+ | `experimental.workflowEvalOnly` | boolean | `false` | Route the workflow (dag) tool through eval cells only; the tool leaves the model direct tool list |
402
404
 
403
405
  `defaultTools` selects the built-in tools enabled at startup. Extension and SDK custom tools remain enabled. Available built-ins are `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`:
404
406
 
@@ -418,6 +420,38 @@ On Windows, select `powershell` instead of `bash`, or include both:
418
420
 
419
421
  An empty array starts with no built-in tools while preserving extension and SDK custom tools. `--tools` replaces this behavior with a strict allowlist for all tools, `--no-tools` disables all tools, and `--no-builtin-tools` disables the built-in defaults. `--exclude-tools` filters the resulting list. A project `defaultTools` array replaces the global array.
420
422
 
423
+ With `experimental.bashEvalOnly` enabled, the `bash` and `powershell` tools disappear from the model's direct tool list and run only inside eval cells:
424
+
425
+ ```json
426
+ {
427
+ "experimental": {
428
+ "bashEvalOnly": true
429
+ }
430
+ }
431
+ ```
432
+
433
+ ```js
434
+ const { output } = await tool.bash({ command: "ls -la" });
435
+ ```
436
+
437
+ Hooks and permission checks apply unchanged to shell commands run this way. If the model hallucinates a direct `bash` or `powershell` call anyway, the call returns a hint redirecting it to an eval cell. When the `eval` tool is unavailable (codemode not loaded), the policy auto-disables and both tools stay on the direct tool list, so shell access is never lost.
438
+
439
+ With `experimental.workflowEvalOnly` enabled, the `workflow` (dag) tool disappears from the model's direct tool list and runs only inside eval cells:
440
+
441
+ ```json
442
+ {
443
+ "experimental": {
444
+ "workflowEvalOnly": true
445
+ }
446
+ }
447
+ ```
448
+
449
+ ```js
450
+ const snapshot = await tool.workflow({ action: "snapshot", run_id });
451
+ ```
452
+
453
+ Hooks and permission checks apply unchanged to workflow operations run this way. When the `eval` tool is unavailable (codemode not loaded), the policy auto-disables and the workflow tool stays on the direct tool list, so workflow access is never lost.
454
+
421
455
  ### Sessions
422
456
 
423
457
  | Setting | Type | Default | Description |
package/docs/windows.md CHANGED
@@ -30,6 +30,8 @@ Or enable both while comparing behavior:
30
30
 
31
31
  The `!` and `!!` editor commands still use Bash.
32
32
 
33
+ With `experimental.bashEvalOnly` enabled, both `bash` and `powershell` run only inside eval cells; see [Settings](settings.md#tools).
34
+
33
35
  ## Custom Bash Path
34
36
 
35
37
  ```json
@@ -12,6 +12,68 @@
12
12
 
13
13
  ### Removed
14
14
 
15
+ ## [2026.8.30] - 2026-08-30
16
+
17
+ ### Breaking Changes
18
+
19
+ ### Added
20
+
21
+ ### Changed
22
+
23
+ - The eval prompt's dependency-graph section is now `<workflow>` and states its contract directly:
24
+ define the workflow spec in code, one node per logically distinct step, rather than hand-authoring
25
+ the graph as a single opaque call.
26
+
27
+ ### Fixed
28
+
29
+ - The JavaScript kernel persistence transform no longer truncates declarations whose multi-line
30
+ initializers contain interior `//` comments (previously emitted unparseable code such as
31
+ `globalThis["jobs"] = {;`, failing cells with `Unexpected token ';'. Expected a property name.`),
32
+ and no longer re-evaluates comment-bearing initializers when persisting bindings — such
33
+ declarations are kept verbatim and their bindings persisted by reference.
34
+ - Last-expression capture no longer inserts `return` before continuation lines (`else`/`catch`/`finally`
35
+ clauses and leading-`.`/operator method-chain lines), and now scans template literals (including
36
+ nested templates in interpolations), regexes, and comments with the same literal-aware scanner as
37
+ the persistence transform — fixing `return else …`, `return .replace(…)`, and mid-argument
38
+ `return )` corruption of valid cells.
39
+ - Last-expression capture now follows real ASI statement semantics: a parenthesized/bracketed/template
40
+ line after a closed block starts a new statement (echo restored), expressions split after a trailing
41
+ operator or `await` stay one statement, tagged templates split across lines invoke the tag, regexes
42
+ directly after a control-structure condition no longer desync the scanner, and labeled final
43
+ statements are left uncaptured instead of emitting invalid `return label: …`.
44
+ - Destructuring patterns carrying interior line comments now persist their bindings, and declarations
45
+ with a dangling trailing comma are left untransformed so the original syntax error surfaces instead
46
+ of being silently "repaired".
47
+ - Rewritten destructuring assignments are emitted with a leading defensive semicolon so they can no
48
+ longer ASI-merge into a preceding unterminated expression statement as a bogus call
49
+ (`foo()\n({…} = …)` previously became `foo()({…} = …)`).
50
+
51
+ ### Removed
52
+
53
+ ## [2026.8.29] - 2026-08-29
54
+
55
+ ### Breaking Changes
56
+
57
+ ### Added
58
+
59
+ ### Changed
60
+
61
+ ### Fixed
62
+
63
+ ### Removed
64
+
65
+ ## [2026.8.28-2] - 2026-08-28
66
+
67
+ ### Breaking Changes
68
+
69
+ ### Added
70
+
71
+ ### Changed
72
+
73
+ ### Fixed
74
+
75
+ ### Removed
76
+
15
77
  ## [2026.8.28] - 2026-08-28
16
78
 
17
79
  ### Breaking Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-codemode",
3
- "version": "2026.8.28",
3
+ "version": "2026.8.30",
4
4
  "private": true,
5
5
  "description": "Source-only senpi extension package for codemode evaluation tools",
6
6
  "type": "module",
@@ -31,14 +31,14 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@babel/parser": "8.0.4",
34
- "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.28",
34
+ "@earendil-works/pi-ai": "npm:@code-yeongyu/senpi-ai@2026.8.30",
35
35
  "typebox": "1.3.18"
36
36
  },
37
37
  "peerDependencies": {
38
- "@code-yeongyu/senpi": "2026.8.28"
38
+ "@code-yeongyu/senpi": "2026.8.30"
39
39
  },
40
40
  "devDependencies": {
41
- "@code-yeongyu/senpi": "2026.8.28"
41
+ "@code-yeongyu/senpi": "2026.8.30"
42
42
  },
43
43
  "keywords": [
44
44
  "senpi",