mixdog 0.9.147 → 0.9.149

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 (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -15,6 +15,7 @@
15
15
  import http from 'node:http';
16
16
  import { randomUUID } from 'node:crypto';
17
17
  import { sendJson, sendError } from '../runtime/memory/lib/http-wire.mjs';
18
+ import { isPidAlive, parsePid } from '../runtime/shared/pid-liveness.mjs';
18
19
  import {
19
20
  compareRuntimeVersions,
20
21
  SESSION_CAPABILITY_FINGERPRINT,
@@ -99,17 +100,6 @@ function callSignature(name, args) {
99
100
  } catch { return null; }
100
101
  }
101
102
 
102
- function parsePid(value) {
103
- const n = Number(value);
104
- return Number.isInteger(n) && n > 0 ? n : null;
105
- }
106
-
107
- function isPidAlive(pid) {
108
- const n = parsePid(pid);
109
- if (!n) return false;
110
- try { process.kill(n, 0); return true; }
111
- catch (error) { return error?.code === 'EPERM'; }
112
- }
113
103
 
114
104
  // A stalled reader collapses the backlog to one frame per key. That is right
115
105
  // for SNAPSHOTS (the newest state is the whole truth) and wrong for a BYTE
@@ -313,17 +303,39 @@ export function createSessionTransport({
313
303
  return Math.min(limit, bytes);
314
304
  }
315
305
 
306
+ let callCachePressureLoggedAt = 0;
307
+
316
308
  function pruneCallCache() {
317
- while (callCache.size > CALL_CACHE_MAX || callCacheBytes > CALL_CACHE_MAX_BYTES) {
318
- let removed = false;
319
- for (const [key, record] of callCache) {
320
- // Never lose the dedup identity of an in-flight mutation.
321
- if (!record.settled) continue;
322
- deleteCallCacheEntry(key, record);
323
- removed = true;
324
- break;
325
- }
326
- if (!removed) return;
309
+ if (callCache.size <= CALL_CACHE_MAX && callCacheBytes <= CALL_CACHE_MAX_BYTES) return;
310
+ const now = nowMs();
311
+ // 1) Only an entry whose promised TTL has already elapsed may be forgotten
312
+ // completely (its own timer may not have fired yet).
313
+ for (const [key, record] of callCache) {
314
+ if (callCache.size <= CALL_CACHE_MAX && callCacheBytes <= CALL_CACHE_MAX_BYTES) return;
315
+ if (!record.settled || !record.settledAt) continue;
316
+ if (now - record.settledAt < CALL_CACHE_TTL_MS) continue;
317
+ deleteCallCacheEntry(key, record);
318
+ }
319
+ // 2) Byte pressure drops the retained RESULT of settled entries, oldest
320
+ // first, and KEEPS their dedup identity: evicting the identity inside
321
+ // the promised TTL is exactly what let a retried submit/abort/configure
322
+ // execute twice. A replay of a dropped result fails closed instead.
323
+ for (const record of callCache.values()) {
324
+ if (callCacheBytes <= CALL_CACHE_MAX_BYTES) break;
325
+ if (!record.settled || record.resultDropped) continue;
326
+ callCacheBytes = Math.max(0, callCacheBytes - (record.bytes || 0));
327
+ record.bytes = 0;
328
+ record.resultDropped = true;
329
+ record.promise = null;
330
+ }
331
+ // 3) Entry-count pressure never evicts a live dedup identity. What remains
332
+ // carries no payload and expires with its own TTL timer.
333
+ if (callCache.size > CALL_CACHE_MAX && now - callCachePressureLoggedAt > 60_000) {
334
+ callCachePressureLoggedAt = now;
335
+ log(
336
+ `session call dedup cache above its entry budget (${callCache.size}/${CALL_CACHE_MAX});`
337
+ + ' every remaining entry is still inside its retry TTL',
338
+ );
327
339
  }
328
340
  }
329
341
 
@@ -738,7 +750,14 @@ export function createSessionTransport({
738
750
  const cached = cacheKey ? callCache.get(cacheKey) : null;
739
751
  let dispatch;
740
752
  if (cached) {
741
- if (!signature || cached.signature !== signature) {
753
+ if (cached.resultDropped) {
754
+ // The identity survived memory pressure but its result did not.
755
+ // Fail closed: re-running a settled mutation is never correct.
756
+ dispatch = Promise.reject(Object.assign(
757
+ new Error(`callId '${callId}' already ran; its result is no longer retained`),
758
+ { code: 'ECALLRESULTDROPPED' },
759
+ ));
760
+ } else if (!signature || cached.signature !== signature) {
742
761
  // callId is an idempotency key, not a caller-selected overwrite
743
762
  // slot. Fail closed while the original keeps its cache identity;
744
763
  // dispatching here could execute two side-effecting mutations.
@@ -762,7 +781,13 @@ export function createSessionTransport({
762
781
  );
763
782
  if (cacheKey) {
764
783
  const record = {
765
- promise: dispatch, at: nowMs(), signature, settled: false, bytes: 0,
784
+ promise: dispatch,
785
+ at: nowMs(),
786
+ signature,
787
+ settled: false,
788
+ settledAt: 0,
789
+ resultDropped: false,
790
+ bytes: 0,
766
791
  };
767
792
  callCache.set(cacheKey, record);
768
793
  pruneCallCache();
@@ -773,6 +798,7 @@ export function createSessionTransport({
773
798
  callCacheBytes += record.bytes;
774
799
  }, () => {}).then(() => {
775
800
  record.settled = true;
801
+ record.settledAt = nowMs();
776
802
  pruneCallCache();
777
803
  const t = setTimeout(() => {
778
804
  deleteCallCacheEntry(cacheKey, record);
@@ -786,8 +812,13 @@ export function createSessionTransport({
786
812
  sendJson(res, { result });
787
813
  } catch (err) {
788
814
  // Session call errors travel as a 200 {error} envelope so the client can
789
- // tell a failed CALL from a dead TRANSPORT (which must re-attach).
790
- sendJson(res, { error: err?.message || String(err) }, 200);
815
+ // tell a failed CALL from a dead TRANSPORT (which must re-attach). The
816
+ // machine-readable `code` (ECALLIDCONFLICT / ECALLRESULTDROPPED) travels
817
+ // with it: a client must be able to branch on it, not parse prose.
818
+ sendJson(res, {
819
+ error: err?.message || String(err),
820
+ ...(err?.code ? { code: String(err.code) } : {}),
821
+ }, 200);
791
822
  }
792
823
  return;
793
824
  }
package/src/tui/App.jsx CHANGED
@@ -73,6 +73,8 @@ import {
73
73
  memoryCoreResultErrorText,
74
74
  } from './app/input-parsers.mjs';
75
75
  import { copyToClipboard } from './app/clipboard.mjs';
76
+ import { shouldSupersedePanelEpoch, supersedePanelEpoch } from './app/panel-epoch.mjs';
77
+ import { createPanelSurface } from './app/panel-surface.mjs';
76
78
  import { wrappedTextRows, promptContentRows, wrappedDetailRows, textEntryReservedRows, queuedBandRows } from './app/text-layout.mjs';
77
79
  import stringWidth from 'string-width';
78
80
  import { useMouseInput } from './app/use-mouse-input.mjs';
@@ -348,7 +350,17 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
348
350
  const setPicker = useCallback((next) => {
349
351
  // Synchronous ref update so out-of-band setPicker(null/other) is visible to
350
352
  // in-flight async guards immediately, before React commits the next render.
351
- livePickerRef.current = typeof next === 'function' ? next(livePickerRef.current) : next;
353
+ const previousPicker = livePickerRef.current;
354
+ livePickerRef.current = typeof next === 'function' ? next(previousPicker) : next;
355
+ // A real handover — closing the panel (open → null) OR replacing it with a
356
+ // different panel — gives the surface to what the user is looking at now:
357
+ // supersede every deferred paint issued before this point so a daemon write
358
+ // settling afterwards cannot resurrect the dismissed panel or clobber the
359
+ // panel that replaced it. Rebuilding the SAME panel keeps ownership (its
360
+ // own refresh must land), and a redundant null → null (or a first open) is
361
+ // not a handover and must not invalidate the in-flight write of a
362
+ // text-entry prompt that is currently on screen.
363
+ if (shouldSupersedePanelEpoch(previousPicker, livePickerRef.current)) supersedePanelEpoch();
352
364
  setPickerState((prev) => {
353
365
  const resolved = typeof next === 'function' ? next(prev) : next;
354
366
  if (resolved && typeof resolved === 'object' && pickerOpenedFromEnterRef.current) {
@@ -377,35 +389,40 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
377
389
  livePickerRef.current = picker;
378
390
  const [contextPanel, setContextPanel] = useState(null);
379
391
  const [usagePanel, setUsagePanel] = useState(null);
380
- const usageRequestRef = useRef(0);
392
+ // OWNING LAYER (app/panel-surface.mjs): the three sinks above are handed over
393
+ // exactly once and never leave that module. Every panel factory below gets
394
+ // `surface` instead, so painting or delegating without a claim that proves
395
+ // ownership is not expressible outside this file. Stable across renders — the
396
+ // usage generation lives inside it.
397
+ const surfaceRef = useRef(null);
398
+ if (!surfaceRef.current) {
399
+ surfaceRef.current = createPanelSurface({ setPicker, setContextPanel, setUsagePanel });
400
+ }
401
+ const surface = surfaceRef.current;
381
402
  // Cache of the last computed heavy settings-picker status objects (MCP,
382
403
  // hooks, plugins, skills, channel provider). ←/→ cycle/toggle handlers in
383
404
  // openSettingsPicker() pass { light: true } to reuse this cache instead of
384
405
  // re-querying these heavy getters on every keystroke; only a full open
385
406
  // (initial /config or Esc-return) recomputes them.
386
407
  const settingsHeavyCacheRef = useRef(null);
387
- const closeUsagePanel = useCallback(() => {
388
- usageRequestRef.current += 1;
389
- setUsagePanel(null);
390
- }, []);
408
+ // Settings build generation: every open/refresh takes a ticket and Esc bumps
409
+ // it, so a slow (daemon) snapshot from a superseded build cannot re-open the
410
+ // panel after it was closed.
411
+ const settingsRequestRef = useRef(0);
412
+ const closeUsagePanel = useCallback(() => surface.closeUsage(), [surface]);
391
413
  const [providerPrompt, setProviderPrompt] = useState(null);
392
414
  const oauthSubmitRef = useRef(false);
393
- const [channelPrompt, setChannelPrompt] = useState(null);
394
- const [hookPrompt, setHookPrompt] = useState(null);
395
415
  const [settingsPrompt, setSettingsPrompt] = useState(null);
396
- // Instantiate the project-picker cluster now that setPicker + every prompt
416
+ // Instantiate the project-picker cluster now that the surface + every prompt
397
417
  // setter and the usage-panel closer exist. projectPickerRef (declared above,
398
418
  // before the picker useState) is populated here so first-mount build and all
399
419
  // later callers resolve the same set of builders.
400
420
  const projectPicker = createProjectPicker({
401
421
  state,
402
422
  store,
403
- setPicker,
423
+ surface,
404
424
  setProviderPrompt,
405
- setChannelPrompt,
406
- setHookPrompt,
407
425
  setSettingsPrompt,
408
- setContextPanel,
409
426
  closeUsagePanel,
410
427
  projectNameFromPath,
411
428
  pickFolder,
@@ -609,12 +626,9 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
609
626
  } = createAppPickers({
610
627
  state,
611
628
  store,
612
- setPicker,
629
+ surface,
613
630
  setProviderPrompt,
614
- setChannelPrompt,
615
- setHookPrompt,
616
631
  setSettingsPrompt,
617
- setContextPanel,
618
632
  setOnboardingActive,
619
633
  closeUsagePanel,
620
634
  oauthSubmitRef,
@@ -625,6 +639,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
625
639
  modelPickerRequestRef,
626
640
  onboardingPrefetchSeqRef,
627
641
  settingsHeavyCacheRef,
642
+ settingsRequestRef,
628
643
  livePickerRef,
629
644
  disabledSkills,
630
645
  setDisabledSkills,
@@ -823,7 +838,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
823
838
  const { hasUserMessages, openMessageSelector } = useMessageSelector({
824
839
  store,
825
840
  state,
826
- setPicker,
841
+ surface,
827
842
  setPromptDraftOverride,
828
843
  syncPromptLayoutRows,
829
844
  showPromptHint,
@@ -849,7 +864,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
849
864
  setPastedImages,
850
865
  setPastedTexts,
851
866
  setPromptDraftOverride,
852
- setContextPanel,
867
+ surface,
853
868
  syncPromptLayoutRows,
854
869
  showPromptHint,
855
870
  clearPromptHint,
@@ -883,7 +898,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
883
898
  picker,
884
899
  usagePanel,
885
900
  contextPanel,
886
- setContextPanel,
901
+ surface,
887
902
  closeUsagePanel,
888
903
  isRawModeSupported,
889
904
  resizeState,
@@ -906,14 +921,9 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
906
921
  const { openUsagePanel, openContextPicker } = createUsageContextPanels({
907
922
  store,
908
923
  state,
909
- usageRequestRef,
910
- setPicker,
924
+ surface,
911
925
  setProviderPrompt,
912
- setChannelPrompt,
913
- setHookPrompt,
914
926
  setSettingsPrompt,
915
- setContextPanel,
916
- setUsagePanel,
917
927
  closeUsagePanel,
918
928
  });
919
929
 
@@ -960,17 +970,12 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
960
970
  store,
961
971
  state,
962
972
  providerPrompt,
963
- channelPrompt,
964
- hookPrompt,
965
973
  settingsPrompt,
966
974
  setProviderPrompt,
967
- setChannelPrompt,
968
- setHookPrompt,
969
975
  setSettingsPrompt,
970
976
  oauthSubmitRef,
971
977
  clearModelCaches,
972
978
  openProviderSetupPicker,
973
- openHooksPicker,
974
979
  openSettingsPicker,
975
980
  openProjectPicker,
976
981
  openAutoClearPicker,
@@ -988,7 +993,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
988
993
  pastedTextsRef,
989
994
  });
990
995
 
991
- const activeSlashQuery = providerPrompt || channelPrompt || hookPrompt || settingsPrompt || toolApproval || contextPanel || usagePanel ? null : slashQuery(promptDraft);
996
+ const activeSlashQuery = providerPrompt || settingsPrompt || toolApproval || contextPanel || usagePanel ? null : slashQuery(promptDraft);
992
997
  // "Slash mode" is live whenever a /token is being edited and no other
993
998
  // surface owns the floating area. The palette stays OPEN for the whole
994
999
  // slash session — including 0-match frames — so its 14-row layout never
@@ -1009,8 +1014,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1009
1014
  contextPanel: !!contextPanel,
1010
1015
  usagePanel: !!usagePanel,
1011
1016
  providerPrompt: !!providerPrompt,
1012
- channelPrompt: !!channelPrompt,
1013
- hookPrompt: !!hookPrompt,
1014
1017
  settingsPrompt: !!settingsPrompt,
1015
1018
  };
1016
1019
 
@@ -1023,8 +1026,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1023
1026
  const {
1024
1027
  onPromptDraftChange,
1025
1028
  cancelProviderPrompt,
1026
- cancelChannelPrompt,
1027
- cancelHookPrompt,
1028
1029
  cancelSettingsPrompt,
1029
1030
  acceptSlashPalette,
1030
1031
  completeSlashPalette,
@@ -1044,12 +1045,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1044
1045
  slashDismissedFor,
1045
1046
  setSlashDismissedFor,
1046
1047
  providerPrompt,
1047
- channelPrompt,
1048
- hookPrompt,
1049
1048
  settingsPrompt,
1050
1049
  setProviderPrompt,
1051
- setChannelPrompt,
1052
- setHookPrompt,
1053
1050
  setSettingsPrompt,
1054
1051
  oauthSubmitRef,
1055
1052
  openProjectPicker,
@@ -1070,8 +1067,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1070
1067
  // Transcript viewport + bottom-cluster row budget: app/shell-layout.mjs.
1071
1068
  const layout = computeShellLayout({
1072
1069
  providerPrompt,
1073
- channelPrompt,
1074
- hookPrompt,
1075
1070
  settingsPrompt,
1076
1071
  panelTransitionEpoch,
1077
1072
  panelInkMaskEpoch,
@@ -1194,7 +1189,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1194
1189
  setMeasuredRowsVersion,
1195
1190
  });
1196
1191
  const cycleWorkflowFromPrompt = useCallback(() => {
1197
- if (slashPaletteOpen || toolApproval || picker || settingsPrompt || providerPrompt || channelPrompt || hookPrompt || contextPanel || usagePanel) return true;
1192
+ if (slashPaletteOpen || toolApproval || picker || settingsPrompt || providerPrompt || contextPanel || usagePanel) return true;
1198
1193
  const repeatGuardMs = 300;
1199
1194
  const cycleGuard = workflowTabCycleRef.current;
1200
1195
  const now = Date.now();
@@ -1236,7 +1231,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1236
1231
  cycleGuard.lastAt = Date.now();
1237
1232
  });
1238
1233
  return true;
1239
- }, [slashPaletteOpen, toolApproval, picker, settingsPrompt, providerPrompt, channelPrompt, hookPrompt, contextPanel, usagePanel, state.commandBusy, state.workflow, store]);
1234
+ }, [slashPaletteOpen, toolApproval, picker, settingsPrompt, providerPrompt, contextPanel, usagePanel, state.commandBusy, state.workflow, store]);
1240
1235
  // The hardware/IME caret is parked by PromptInput from its OWN measured box
1241
1236
  // position (ink useCursor + useBoxMetrics) — correct now that the transcript
1242
1237
  // is a live column, so the live-frame line count ink relies on is accurate.
@@ -1247,12 +1242,9 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1247
1242
  activeSlashQuery,
1248
1243
  activeTools,
1249
1244
  agentRevision,
1250
- cancelChannelPrompt,
1251
- cancelHookPrompt,
1252
1245
  cancelProviderPrompt,
1253
1246
  cancelSettingsPrompt,
1254
1247
  cancelSlashPalette,
1255
- channelPrompt,
1256
1248
  clearPromptHint,
1257
1249
  completeSlashPalette,
1258
1250
  contextPanel,
@@ -1265,7 +1257,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1265
1257
  handlePromptInterrupt,
1266
1258
  handlePromptPaste,
1267
1259
  hasUserMessages,
1268
- hookPrompt,
1269
1260
  initialStatusLine,
1270
1261
  onPromptDraftChange,
1271
1262
  onSubmit,
@@ -1288,7 +1279,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1288
1279
  resizeEpoch,
1289
1280
  resizeState,
1290
1281
  restoreQueuedToPrompt,
1291
- setPicker,
1292
1282
  setSlashIndex,
1293
1283
  setTextEntryLayoutRows,
1294
1284
  settingsPrompt,
@@ -1298,6 +1288,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1298
1288
  state,
1299
1289
  statuslineStats,
1300
1290
  store,
1291
+ surface,
1301
1292
  toolApproval,
1302
1293
  toolOutputExpanded,
1303
1294
  transcriptMeasureRef,
@@ -18,9 +18,10 @@ import { SlashCommandPalette } from '../components/SlashCommandPalette.jsx';
18
18
  import { ContextPanel } from '../components/ContextPanel.jsx';
19
19
  import { UsagePanel } from '../components/UsagePanel.jsx';
20
20
  import { TextEntryPanel } from '../components/TextEntryPanel.jsx';
21
+ import { textEntryClearsByEmpty } from './text-entry-policy.mjs';
21
22
  import { Item } from '../components/TranscriptItem.jsx';
22
23
  export function renderAppView(ctx) {
23
- const { PANEL_MAX_VISIBLE, acceptSlashPalette, activeSlashQuery, activeTools, agentRevision, cancelChannelPrompt, cancelHookPrompt, cancelProviderPrompt, cancelSettingsPrompt, cancelSlashPalette, channelPrompt, clearPromptHint, completeSlashPalette, contextPanel, cycleWorkflowFromPrompt, exiting, expandedOptionPanel, floatingPanelRows, frameColumns, gridSelectionActiveRef, guardHintWidth, handlePromptEscape, handlePromptHistoryNavigate, handlePromptInterrupt, handlePromptPaste, hasUserMessages, hookPrompt, initialStatusLine, inputBoxHidden, inputHint, inputHintTone, liveSpinner, onPromptDraftChange, onSubmit, overlayHintAttachItemIndex, overlayHintBandRows, overlayHintFallbackRow, overlayHintOnLastItem, panelCloseMaskRows, panelInkMaskEpoch, panelTransitionClearRows, panelTransitionEpoch, picker, pickerOpenedFromEnterRef, pickerOpenedFromEnterTimerRef, pickerVisibleRows, promptBoxRectRef, promptBoxRows, promptDraft, promptDraftOverride, promptMetaVisible, promptMouseSelectionRef, promptSelectionRef, promptSpinnerColumns, promptValueRef, providerPrompt, queuedCompact, queuedVisible, renderedTranscriptItems, resizeEpoch, resizeState, restoreQueuedToPrompt, setPicker, setSlashIndex, setTextEntryLayoutRows, settingsPrompt, showWelcomeBanner, slashCommands, slashIndex, slashPaletteOpen, state, statuslineStats, store, toolApproval, toolOutputExpanded, transcriptContentHeight, transcriptGuardRows, transcriptMeasureRef, transcriptTailPinned, transcriptWindow, transientStatusWidth, tuiReady, usagePanel, viewportHeight, welcomePromptHintRows, welcomePromptHintText } = ctx; /* DESTRUCTURE */
24
+ const { PANEL_MAX_VISIBLE, acceptSlashPalette, activeSlashQuery, activeTools, agentRevision, cancelProviderPrompt, cancelSettingsPrompt, cancelSlashPalette, clearPromptHint, completeSlashPalette, contextPanel, cycleWorkflowFromPrompt, exiting, expandedOptionPanel, floatingPanelRows, frameColumns, gridSelectionActiveRef, guardHintWidth, handlePromptEscape, handlePromptHistoryNavigate, handlePromptInterrupt, handlePromptPaste, hasUserMessages, initialStatusLine, inputBoxHidden, inputHint, inputHintTone, liveSpinner, onPromptDraftChange, onSubmit, overlayHintAttachItemIndex, overlayHintBandRows, overlayHintFallbackRow, overlayHintOnLastItem, panelCloseMaskRows, panelInkMaskEpoch, panelTransitionClearRows, panelTransitionEpoch, picker, pickerOpenedFromEnterRef, pickerOpenedFromEnterTimerRef, pickerVisibleRows, promptBoxRectRef, promptBoxRows, promptDraft, promptDraftOverride, promptMetaVisible, promptMouseSelectionRef, promptSelectionRef, promptSpinnerColumns, promptValueRef, providerPrompt, queuedCompact, queuedVisible, renderedTranscriptItems, resizeEpoch, resizeState, restoreQueuedToPrompt, setSlashIndex, setTextEntryLayoutRows, settingsPrompt, showWelcomeBanner, slashCommands, slashIndex, slashPaletteOpen, state, statuslineStats, store, surface, toolApproval, toolOutputExpanded, transcriptContentHeight, transcriptGuardRows, transcriptMeasureRef, transcriptTailPinned, transcriptWindow, transientStatusWidth, tuiReady, usagePanel, viewportHeight, welcomePromptHintRows, welcomePromptHintText } = ctx; /* DESTRUCTURE */
24
25
  const promptInputControl = (
25
26
  <PromptInput
26
27
  onSubmit={onSubmit}
@@ -277,7 +278,9 @@ export function renderAppView(ctx) {
277
278
  onCancel={() => {
278
279
  if (picker.onCancel) picker.onCancel();
279
280
  else {
280
- setPicker(null);
281
+ // Esc with no owner-supplied handler: this keypress owns
282
+ // the surface it clears (app/panel-surface.mjs).
283
+ surface.claim().close();
281
284
  clearPromptHint();
282
285
  }
283
286
  }}
@@ -326,6 +329,9 @@ export function renderAppView(ctx) {
326
329
  />
327
330
  ) : providerPrompt ? (
328
331
  <TextEntryPanel
332
+ // Remount on a restore so a repeated failed save re-seeds the
333
+ // editor even when the restored text is byte-identical.
334
+ key={`provider-prompt:${providerPrompt.restoreEpoch || 0}`}
329
335
  title={providerPrompt.kind === 'api-key'
330
336
  ? `${providerPrompt.mode === 'replace' ? 'Replace' : 'Set'} API key · ${providerPrompt.label}`
331
337
  : providerPrompt.kind === 'oauth-code'
@@ -347,6 +353,9 @@ export function renderAppView(ctx) {
347
353
  detail={providerPrompt.detail || ''}
348
354
  mask={providerPrompt.kind === 'api-key' || providerPrompt.kind === 'openai-usage-session'}
349
355
  columns={frameColumns}
356
+ // Restored after a REJECTED daemon save so the entered secret
357
+ // is not lost with the failed round-trip (normally empty).
358
+ initialValue={providerPrompt.initialValue || ''}
350
359
  actionLabel={providerPrompt.kind === 'oauth-code' ? 'continue' : 'save'}
351
360
  promptLabel={providerPrompt.kind === 'api-key'
352
361
  ? 'API key > '
@@ -358,31 +367,15 @@ export function renderAppView(ctx) {
358
367
  onSubmit={onSubmit}
359
368
  onCancel={cancelProviderPrompt}
360
369
  />
361
- ) : channelPrompt ? (
362
- <TextEntryPanel
363
- title={channelPrompt.label}
364
- hint={channelPrompt.hint || 'Save channel setting.'}
365
- mask={channelPrompt.kind === 'webhook-token'}
366
- columns={frameColumns}
367
- promptLabel="Value > "
368
- onSubmit={onSubmit}
369
- onCancel={cancelChannelPrompt}
370
- />
371
- ) : hookPrompt ? (
372
- <TextEntryPanel
373
- title={hookPrompt.label}
374
- hint={hookPrompt.hint || 'Save hook setting.'}
375
- columns={frameColumns}
376
- promptLabel="Value > "
377
- onSubmit={onSubmit}
378
- onCancel={cancelHookPrompt}
379
- />
380
370
  ) : settingsPrompt ? (
381
371
  <TextEntryPanel
372
+ key={`settings-prompt:${settingsPrompt.kind}:${settingsPrompt.restoreEpoch || 0}`}
382
373
  title={settingsPrompt.label}
383
374
  hint={settingsPrompt.hint || 'Save setting.'}
384
375
  columns={frameColumns}
385
376
  initialValue={settingsPrompt.initialValue || ''}
377
+ // Reset/clear prompts document an empty submit as the action.
378
+ allowEmpty={textEntryClearsByEmpty(settingsPrompt.kind)}
386
379
  multiline={settingsPrompt.kind === 'core-add' || settingsPrompt.kind === 'core-edit'}
387
380
  maxContentRows={PANEL_MAX_VISIBLE}
388
381
  onContentRowsChange={setTextEntryLayoutRows}
@@ -2,16 +2,16 @@
2
2
  * core-memory-picker.mjs — the Core Memory picker + add/edit/delete flow.
3
3
  *
4
4
  * Extracted from App.jsx behavior-preservingly as a dependency-injection
5
- * factory: these openers drive setPicker + setSettingsPrompt and read live
6
- * store state, so they can't be pure. Every function body is the original App
7
- * logic verbatim, with closure identifiers threaded through the factory
5
+ * factory: these openers drive the panel surface + setSettingsPrompt and read
6
+ * live store state, so they can't be pure. Every function body is the original
7
+ * App logic verbatim, with closure identifiers threaded through the factory
8
8
  * argument. The Esc-return target is per-entry: the Settings row passes
9
9
  * { returnTo }, the standalone /memory command passes { returnTo: null },
10
10
  * and Esc either reopens the caller or simply closes the picker.
11
11
  */
12
12
  export function createCoreMemoryPicker({
13
13
  store,
14
- setPicker,
14
+ surface,
15
15
  setSettingsPrompt,
16
16
  parseMemoryCoreRows,
17
17
  }) {
@@ -22,13 +22,19 @@ export function createCoreMemoryPicker({
22
22
  let escReturnTo = null;
23
23
  const closeMemoryCorePicker = () => {
24
24
  if (escReturnTo) escReturnTo();
25
- else setPicker(null);
25
+ // Esc on the panel: this keypress owns the surface it clears.
26
+ else surface.claim().close();
26
27
  };
27
28
  const openMemoryCorePicker = (options = {}) => {
28
29
  if (options && Object.prototype.hasOwnProperty.call(options, 'returnTo')) {
29
30
  escReturnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
30
31
  }
31
- setPicker({
32
+ // Surface claim (panel-surface.mjs): every paint re-validates and re-arms
33
+ // it, so the loading frame can take the surface while a list (or failure
34
+ // close) landing after Esc cannot touch it.
35
+ const own = surface.claim();
36
+ const paintPanel = (panel) => own.paint(panel);
37
+ paintPanel({
32
38
  title: 'Memory',
33
39
  // Loading state lives in the header description row, not as a fake
34
40
  // selectable menu item.
@@ -53,7 +59,7 @@ export function createCoreMemoryPicker({
53
59
  _rows: coreRows,
54
60
  },
55
61
  ];
56
- setPicker({
62
+ paintPanel({
57
63
  title: 'Memory',
58
64
  description: 'User-curated core memories across projects.',
59
65
  items: rows.length ? rows : [{ value: 'empty', label: 'Memory', description: 'empty' }],
@@ -71,14 +77,17 @@ export function createCoreMemoryPicker({
71
77
  });
72
78
  })
73
79
  .catch((e) => {
74
- setPicker(null);
80
+ own.close();
75
81
  store.pushNotice(`core memory failed: ${e?.message || e}`, 'error');
76
82
  });
77
83
  };
78
84
 
79
85
  const openCoreMemoryListPicker = (rows = null) => {
86
+ // Same ownership rule as openMemoryCorePicker.
87
+ const own = surface.claim();
88
+ const paintPanel = (panel) => own.paint(panel);
80
89
  const renderList = (coreRows) => {
81
- setPicker({
90
+ paintPanel({
82
91
  title: 'Memory · List',
83
92
  description: coreRows.length
84
93
  ? 'Select a memory to edit or delete.'
@@ -101,7 +110,7 @@ export function createCoreMemoryPicker({
101
110
  return;
102
111
  }
103
112
 
104
- setPicker({
113
+ paintPanel({
105
114
  title: 'Memory · List',
106
115
  description: 'Loading memories…',
107
116
  items: [],
@@ -109,15 +118,19 @@ export function createCoreMemoryPicker({
109
118
  onCancel: () => openMemoryCorePicker(),
110
119
  });
111
120
  void store.memoryControl?.({ action: 'core', op: 'list', project_id: '*' }, { silent: true })
112
- .then((result) => renderList(parseMemoryCoreRows(result)))
121
+ .then((result) => {
122
+ renderList(parseMemoryCoreRows(result));
123
+ })
113
124
  .catch((e) => {
114
- setPicker(null);
125
+ own.close();
115
126
  store.pushNotice(`core memory failed: ${e?.message || e}`, 'error');
116
127
  });
117
128
  };
118
129
 
119
130
  const openCoreEntryActionsPicker = (entryItem) => {
120
- setPicker({
131
+ // Synchronous panel for an Enter on a list row: an ordinary claimed paint.
132
+ const own = surface.claim();
133
+ own.paint({
121
134
  title: `Memory · #${entryItem._id}`,
122
135
  description: entryItem._summary || entryItem._element || '',
123
136
  items: [
@@ -133,7 +146,7 @@ export function createCoreMemoryPicker({
133
146
  };
134
147
 
135
148
  const beginAddCoreMemory = () => {
136
- setPicker(null);
149
+ surface.claim().close();
137
150
  setSettingsPrompt({
138
151
  kind: 'core-add',
139
152
  label: 'Add memory',
@@ -142,7 +155,7 @@ export function createCoreMemoryPicker({
142
155
  };
143
156
 
144
157
  const beginEditCoreMemory = (entryItem) => {
145
- setPicker(null);
158
+ surface.claim().close();
146
159
  setSettingsPrompt({
147
160
  kind: 'core-edit',
148
161
  label: `Memory · Edit #${entryItem._id}`,
@@ -158,7 +171,7 @@ export function createCoreMemoryPicker({
158
171
  };
159
172
 
160
173
  const beginDeleteCoreMemory = (entryItem) => {
161
- setPicker(null);
174
+ surface.claim().close();
162
175
  setSettingsPrompt({
163
176
  kind: 'core-delete-confirm',
164
177
  label: `Memory · Delete #${entryItem._id}?`,