mixdog 0.9.96 → 0.9.97

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 (170) hide show
  1. package/README.md +0 -17
  2. package/package.json +8 -7
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/lead/01-general.md +1 -2
  9. package/src/rules/lead/lead-tool.md +0 -3
  10. package/src/rules/shared/01-tool.md +11 -10
  11. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  12. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  13. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  14. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  15. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  16. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  19. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  20. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
  21. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  23. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  24. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  25. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  27. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  28. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  29. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  30. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  33. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  34. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  35. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  36. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  37. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  38. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  39. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  41. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
  44. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  47. package/src/runtime/channels/lib/config.mjs +33 -33
  48. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  49. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  50. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  51. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  52. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  53. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  54. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  55. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  56. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  57. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  58. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  59. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  60. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  61. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  62. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  63. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  64. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  65. package/src/runtime/channels/tool-defs.mjs +1 -1
  66. package/src/runtime/memory/index.mjs +72 -211
  67. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  68. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  69. package/src/runtime/memory/lib/http-router.mjs +1 -6
  70. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  71. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  72. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  73. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  74. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  75. package/src/runtime/memory/lib/memory.mjs +1 -1
  76. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  77. package/src/runtime/search/index.mjs +2 -2
  78. package/src/runtime/shared/abort-race.mjs +70 -0
  79. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  80. package/src/runtime/shared/config.mjs +11 -24
  81. package/src/runtime/shared/llm/index.mjs +1 -1
  82. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  83. package/src/runtime/shared/resource-admission.mjs +6 -5
  84. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  85. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  86. package/src/runtime/shared/tool-surface.mjs +91 -22
  87. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  88. package/src/runtime/shared/turn-snapshot.mjs +17 -2
  89. package/src/session-runtime/channel-config-api.mjs +5 -5
  90. package/src/session-runtime/config-lifecycle.mjs +27 -27
  91. package/src/session-runtime/lifecycle-api.mjs +22 -3
  92. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  93. package/src/session-runtime/prewarm.mjs +1 -1
  94. package/src/session-runtime/provider-models.mjs +1 -1
  95. package/src/session-runtime/remote-control.mjs +6 -7
  96. package/src/session-runtime/runtime-core.mjs +52 -28
  97. package/src/session-runtime/self-update.mjs +1 -1
  98. package/src/session-runtime/session-lifecycle.mjs +36 -15
  99. package/src/session-runtime/session-title.mjs +1 -1
  100. package/src/session-runtime/session-turn-api.mjs +55 -15
  101. package/src/session-runtime/settings-api.mjs +7 -7
  102. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  103. package/src/standalone/channel-admin.mjs +42 -53
  104. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  105. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
  106. package/src/standalone/channel-worker.mjs +124 -647
  107. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
  108. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  109. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  110. package/src/standalone/session-protocol.mjs +8 -4
  111. package/src/standalone/session-runtime-pool.mjs +393 -0
  112. package/src/standalone/session-runtime-record.mjs +20 -0
  113. package/src/standalone/session-runtime-worker.mjs +203 -0
  114. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
  115. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
  116. package/src/standalone/session-wire.mjs +55 -0
  117. package/src/tui/App.jsx +4 -4
  118. package/src/tui/app/app-format.mjs +1 -1
  119. package/src/tui/app/channel-pickers.mjs +41 -41
  120. package/src/tui/app/input-parsers.mjs +2 -2
  121. package/src/tui/app/onboarding-steps.mjs +2 -2
  122. package/src/tui/app/project-picker.mjs +8 -8
  123. package/src/tui/app/prompt-submit.mjs +8 -8
  124. package/src/tui/app/settings-picker.mjs +25 -25
  125. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  126. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  127. package/src/tui/components/ToolExecution.jsx +1 -1
  128. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  129. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  130. package/src/tui/dist/index.mjs +154 -127
  131. package/src/tui/hooks/useSession.mjs +16 -0
  132. package/src/tui/index.jsx +11 -11
  133. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  134. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  135. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  136. package/src/tui/{engine → session}/context-state.mjs +3 -3
  137. package/src/tui/{engine → session}/labels.mjs +2 -2
  138. package/src/tui/{engine → session}/live-share.mjs +7 -7
  139. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  140. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  141. package/src/tui/session/oauth-flows.mjs +140 -0
  142. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  143. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  144. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  145. package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
  146. package/src/tui/{engine → session}/session-api.mjs +7 -60
  147. package/src/tui/{engine → session}/session-flow.mjs +1 -1
  148. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  149. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  150. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  151. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  152. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  153. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  154. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  155. package/src/tui/{engine → session}/turn.mjs +29 -107
  156. package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
  157. package/src/tui/session.mjs +22 -0
  158. package/src/ui/statusline-segments.mjs +1 -1
  159. package/src/ui/statusline.mjs +1 -1
  160. package/vendor/ink/build/components/App.js +10 -1
  161. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  162. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  163. package/src/tui/engine/turn-watchdog.mjs +0 -92
  164. package/src/tui/engine.mjs +0 -22
  165. package/src/tui/hooks/useEngine.mjs +0 -16
  166. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  167. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  168. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  169. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  170. /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
@@ -1,145 +1,37 @@
1
- import http from 'node:http'
2
1
  import fs from 'node:fs'
3
2
  import os from 'node:os'
4
3
  import path from 'node:path'
5
4
  import { fileURLToPath } from 'node:url'
6
- import { createStandaloneMemoryRuntime } from '../../../standalone/memory-runtime-proxy.mjs'
7
- import { readServicePort, markServiceUnreachable, isConnRefuseError } from '../../shared/service-discovery.mjs'
5
+ import { getStandaloneMemoryRuntime } from '../../../standalone/memory-runtime-proxy.mjs'
8
6
 
9
7
  const RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
10
8
  ? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
11
9
  : path.join(os.tmpdir(), 'mixdog')
12
- const ACTIVE_INSTANCE_FILE = path.join(RUNTIME_ROOT, 'active-instance.json')
13
-
14
- let _portCache = null // { port, mtime, ts }
15
-
16
- // Shared memory-runtime proxy handle for respawn. The channels worker is the
17
- // one process that can find the daemon dead (no port / ECONNREFUSED) while a
18
- // remote session is still alive. Rather than dead-lettering forever, it asks
19
- // the SAME proxy the host uses (createStandaloneMemoryRuntime.start()) to
20
- // re-ensure the singleton daemon. Owner-claim + fork logic is NOT duplicated
21
- // here — start() reuses the on-disk singleton owner file, so a live daemon is
22
- // reused and only a genuinely dead one is respawned.
10
+ let _memoryRuntimePromise = null
23
11
  const MEMORY_ENTRY = fileURLToPath(new URL('../../memory/index.mjs', import.meta.url))
24
- const MEMORY_DATA_DIR = process.env.MIXDOG_DATA_DIR
25
- ? path.resolve(process.env.MIXDOG_DATA_DIR)
26
- : RUNTIME_ROOT
27
- let _memoryProxy = null
28
- let _ensuringDaemon = null
29
- function getMemoryProxy() {
30
- if (!_memoryProxy) {
31
- _memoryProxy = createStandaloneMemoryRuntime({ entry: MEMORY_ENTRY, dataDir: MEMORY_DATA_DIR })
32
- }
33
- return _memoryProxy
34
- }
35
- // Ensure the daemon exists via the shared proxy. Reentrancy-guarded so a burst
36
- // of failed appends/drains coalesces into a single start(). On success the
37
- // port cache is invalidated so the next getMemoryPort() re-reads the freshly
38
- // published active-instance.json. Never throws — callers keep buffering on
39
- // failure, so a respawn miss degrades to the existing retry path, not a crash.
40
- async function ensureMemoryDaemon() {
41
- if (_ensuringDaemon) return _ensuringDaemon
42
- _ensuringDaemon = (async () => {
43
- try {
44
- const res = await getMemoryProxy().start()
45
- _portCache = null
46
- return res?.port ?? null
47
- } catch (e) {
48
- process.stderr.write(`[memory-client] ensureMemoryDaemon failed (${e.message})\n`)
49
- return null
50
- } finally {
51
- _ensuringDaemon = null
52
- }
53
- })()
54
- return _ensuringDaemon
55
- }
56
-
57
- function isConnRefusedLike(err) {
58
- const code = String(err?.code || '')
59
- const msg = String(err?.message || err || '')
60
- return code === 'ECONNREFUSED'
61
- || /ECONNREFUSED|missing memory_port|memory-service timeout/i.test(msg)
62
- }
63
12
 
64
- // A discovery advert validates only the owner pid, which can be a recycled pid
65
- // living on an unrelated process while its advertised port is dead. This client
66
- // has no separate /health probe, so on a connect-level failure we distrust the
67
- // port: mark it unreachable (readServicePort then skips it → legacy fallback)
68
- // and drop the port cache so the next getMemoryPort re-resolves.
69
- function _distrustMemoryPort(port, err) {
70
- // Connection-level failures ONLY. A 'memory-service timeout' means the daemon
71
- // is slow-but-alive — distrusting it would false-route to legacy/buffer.
72
- if (port && isConnRefuseError(err)) {
73
- markServiceUnreachable('memory', port)
74
- _portCache = null
75
- }
76
- }
77
-
78
- async function getMemoryPort() {
79
- const now = Date.now()
80
- if (_portCache && (now - _portCache.ts) < 5_000) return _portCache.port
81
- // Prefer the single-writer discovery advert (discovery/memory.json), which
82
- // validates the owner pid is alive. Fall back to the legacy
83
- // active-instance.json memory_port field for cross-version compat.
84
- const advertPort = readServicePort('memory', { requirePid: false })
85
- if (advertPort) { _portCache = { port: advertPort, mtime: 0, ts: now }; return advertPort }
13
+ async function memoryRuntime() {
14
+ _memoryRuntimePromise ??= Promise.resolve(getStandaloneMemoryRuntime({
15
+ entry: MEMORY_ENTRY,
16
+ dataDir: process.env.MIXDOG_DATA_DIR,
17
+ }))
86
18
  try {
87
- const stat = await fs.promises.stat(ACTIVE_INSTANCE_FILE)
88
- const mtime = stat.mtimeMs
89
- if (_portCache && _portCache.mtime === mtime) {
90
- _portCache.ts = now
91
- return _portCache.port
92
- }
93
- const raw = await fs.promises.readFile(ACTIVE_INSTANCE_FILE, 'utf8')
94
- const active = JSON.parse(raw)
95
- const port = Number(active && active.memory_port)
96
- if (!Number.isFinite(port) || port <= 0) return null
97
- _portCache = { port, mtime, ts: now }
98
- return port
99
- } catch {
100
- return null
19
+ const runtime = await _memoryRuntimePromise
20
+ await runtime.init()
21
+ return runtime
22
+ } catch (error) {
23
+ _memoryRuntimePromise = null
24
+ throw error
101
25
  }
102
26
  }
103
27
 
104
- async function memoryFetch(method, endpoint, body = null, timeoutMs = 10_000, { throwOnError = false } = {}) {
105
- const port = await getMemoryPort()
106
- return new Promise((resolve, reject) => {
107
- if (!port) { reject(new Error('active-instance.json missing memory_port')); return }
108
- const payload = body ? JSON.stringify(body) : null
109
- const req = http.request({
110
- hostname: '127.0.0.1',
111
- port,
112
- path: endpoint,
113
- method,
114
- headers: payload
115
- ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
116
- : {},
117
- timeout: timeoutMs,
118
- }, res => {
119
- let data = ''
120
- res.on('data', chunk => { data += chunk })
121
- res.on('end', () => {
122
- const status = res.statusCode || 0
123
- let parsed
124
- try { parsed = JSON.parse(data) }
125
- catch { parsed = { raw: data } }
126
- // Live callers keep lenient semantics (resolve on any response).
127
- // Drain replay passes throwOnError so a non-2xx status or an
128
- // {error} body is treated as a FAILED replay — the buffer file is
129
- // then kept for retry instead of being unlinked (data-loss guard).
130
- if (throwOnError && (status < 200 || status >= 300 || (parsed && parsed.error != null))) {
131
- const detail = parsed && parsed.error != null ? String(parsed.error) : `HTTP ${status}`
132
- reject(new Error(`memory replay rejected: ${detail}`))
133
- return
134
- }
135
- resolve(parsed)
136
- })
137
- })
138
- req.on('error', err => { _distrustMemoryPort(port, err); reject(err) })
139
- req.on('timeout', () => { req.destroy(); const err = new Error('memory-service timeout'); _distrustMemoryPort(port, err); reject(err) })
140
- if (payload) req.write(payload)
141
- req.end()
142
- })
28
+ async function replayBuffered(kind, payload) {
29
+ const module = await memoryRuntime()
30
+ const result = kind === 'ingest'
31
+ ? await module.ingestTranscript(payload.filePath, { cwd: payload.cwd })
32
+ : await module.appendEntry(payload)
33
+ if (result?.error) throw new Error(`memory replay rejected: ${result.error}`)
34
+ return result
143
35
  }
144
36
 
145
37
  const BUFFER_DIR = path.join(RUNTIME_ROOT, 'memory-buffer')
@@ -166,18 +58,12 @@ export async function appendEntry(data) {
166
58
  cwd: data.cwd ?? null,
167
59
  }
168
60
  // Bounded fast attempt. On failure, buffer to disk immediately and let
169
- // the periodic drainer ship buffered entries when the service is back.
170
- // Caller is fire-and-forget (channels worker), so capping the tail at
171
- // ~3s prevents promises from lingering on minute-long timeouts.
61
+ // the periodic drainer retry after the in-process runtime recovers.
172
62
  try {
173
- return await memoryFetch('POST', '/entry', payload, 3_000)
63
+ const module = await memoryRuntime()
64
+ return await module.appendEntry(payload)
174
65
  } catch (e) {
175
66
  process.stderr.write(`[memory-client] appendEntry failed (${e.message}) — buffering\n`)
176
- // No-port / connection-refused means the daemon is likely dead. Ask the
177
- // shared proxy to re-ensure the singleton (reentrancy-guarded, non-throwing)
178
- // so buffered entries have a live target on the next periodic drain instead
179
- // of being retried against nothing until quarantine.
180
- if (isConnRefusedLike(e)) void ensureMemoryDaemon()
181
67
  const bufferPath = bufferToDisk('entry', payload)
182
68
  return bufferPath ? { ok: false, buffered: true, path: bufferPath } : { ok: false }
183
69
  }
@@ -185,7 +71,8 @@ export async function appendEntry(data) {
185
71
 
186
72
  export async function ingestTranscript(filePath, { cwd } = {}) {
187
73
  try {
188
- return await memoryFetch('POST', '/ingest-transcript', { filePath, ...(cwd ? { cwd } : {}) })
74
+ const module = await memoryRuntime()
75
+ return await module.ingestTranscript(filePath, { cwd })
189
76
  } catch (e) {
190
77
  process.stderr.write(`[memory-client] ingestTranscript failed (${e.message}) — buffering\n`)
191
78
  // Dedupe by transcriptPath: replace any already-buffered ingest for the
@@ -306,8 +193,8 @@ function enforceBufferCap() {
306
193
 
307
194
  // Replay buffered entry-*/ingest-* files once the memory port is live.
308
195
  // Oldest-first (filename carries a ms timestamp), dedupe-safe (each file is
309
- // deleted only after a 2xx replay — memoryFetch(throwOnError) rejects on
310
- // non-2xx/{error}, so a rejected replay keeps the file for retry, no data
196
+ // deleted only after a successful in-process replay; a rejected replay keeps
197
+ // the file for retry, so no data
311
198
  // loss). Retry count is PERSISTED in the filename suffix (`.rN`) so process
312
199
  // restarts don't reset the poison cap; after MAX_DRAIN_ATTEMPTS the file is
313
200
  // MOVED to memory-buffer/dead/ (not deleted, not left blocking the queue).
@@ -325,14 +212,8 @@ function retryName(name, n) {
325
212
  }
326
213
  export async function drainBuffer() {
327
214
  if (_draining) return { ok: true, skipped: 'in-progress' }
328
- const port = await getMemoryPort()
329
- if (!port) {
330
- // No published port: the daemon is down while buffered work is waiting.
331
- // Respawn it via the shared proxy (reused singleton — no fork dup) and
332
- // let the next tick drain against the freshly published port.
333
- void ensureMemoryDaemon()
334
- return { ok: false, reason: 'no-port' }
335
- }
215
+ try { await memoryRuntime() }
216
+ catch (error) { return { ok: false, reason: error?.message || 'memory runtime unavailable' } }
336
217
  _draining = true
337
218
  let drained = 0
338
219
  let failed = 0
@@ -362,11 +243,9 @@ export async function drainBuffer() {
362
243
  if (!moveToDead(name, 'unparseable buffer file')) skipThisPass.add(name)
363
244
  continue
364
245
  }
365
- const endpoint = name.startsWith('ingest-') ? '/ingest-transcript' : '/entry'
246
+ const kind = name.startsWith('ingest-') ? 'ingest' : 'entry'
366
247
  try {
367
- // throwOnError: non-2xx status or an {error} body REJECTS, so the
368
- // file is kept/aged for retry instead of unlinked (HIGH: data loss).
369
- await memoryFetch('POST', endpoint, payload, 10_000, { throwOnError: true })
248
+ await replayBuffered(kind, payload)
370
249
  try {
371
250
  fs.unlinkSync(bufferPath)
372
251
  dropDedupeIndexForPath(bufferPath)
@@ -375,10 +254,6 @@ export async function drainBuffer() {
375
254
  } catch (e) {
376
255
  const attempts = parseRetry(name) + 1
377
256
  failed++
378
- // Connection-refused mid-drain: the daemon died between the port read
379
- // and this POST. Re-ensure it via the shared proxy so the next tick
380
- // has a live target (the file is aged/kept below, never dropped here).
381
- if (isConnRefusedLike(e)) void ensureMemoryDaemon()
382
257
  if (attempts >= MAX_DRAIN_ATTEMPTS) {
383
258
  // Quarantine, don't drop: move to dead/ so a poison record neither
384
259
  // wedges the queue nor silently vanishes (recoverable for triage).
@@ -413,4 +288,3 @@ export async function drainBuffer() {
413
288
  }
414
289
  return { ok: failed === 0, drained, failed }
415
290
  }
416
-
@@ -29,15 +29,15 @@ import {
29
29
  buildToolLine
30
30
  } from "./tool-format.mjs";
31
31
 
32
- // Per-item send watchdog. drainQueue awaits ONE backend send at a time; a hung
32
+ // Per-item send watchdog. drainQueue awaits ONE provider send at a time; a hung
33
33
  // send would otherwise never settle, so drainQueue never returns and `sending`
34
34
  // stays true forever — wedging ALL outbound behind the queue head. The watchdog
35
35
  // does NOT abandon the send (racing a still-running send against a retry would
36
- // double-deliver): it only pokes the backend to reset its wedged client so the
36
+ // double-deliver): it only pokes the provider to reset its wedged client so the
37
37
  // AWAITED send fails fast and `sending` releases on real settlement. Sized above
38
- // the backend's worst-case per-attempt retries.
38
+ // the provider's worst-case per-attempt retries.
39
39
  const SEND_ITEM_TIMEOUT_MS = 120_000;
40
- // After the watchdog resets the backend, wait at most this long for the
40
+ // After the watchdog resets the provider, wait at most this long for the
41
41
  // abandoned send to actually settle before releasing the queue.
42
42
  const SEND_SETTLE_CAP_MS = 15_000;
43
43
  const TAIL_RECOVERY_BYTES = 256 * 1024;
@@ -403,8 +403,8 @@ class OutputForwarder {
403
403
  this.commitReadProgress(item.nextFileSize);
404
404
  return;
405
405
  }
406
- // Formatting is routed through the active backend via the send-callback so
407
- // the forwarder stays backend-agnostic (Discord/Telegram/etc). Preformatted
406
+ // Formatting is routed through the active provider via the send-callback so
407
+ // the forwarder stays provider-agnostic (Discord/Telegram/etc). Preformatted
408
408
  // items (tool logs) are sent as-is. Fallback to raw text when no hook.
409
409
  const formatted = item.preformatted
410
410
  ? item.text
@@ -419,18 +419,18 @@ class OutputForwarder {
419
419
  return;
420
420
  }
421
421
  // The active transcript/turn owns Discord continuation spacing. A daemon
422
- // backend is shared by every client and scheduler, so backend-global send
422
+ // provider is shared by every client and scheduler, so provider-global send
423
423
  // counts cannot decide whether THIS session's item is its first response.
424
424
  // Freeze the decision on the queue item so retries remain byte-identical.
425
425
  if (typeof item._continuation !== "boolean") {
426
426
  item._continuation = this.sentCount > 0;
427
427
  }
428
- // Backend owns chunking + send retry: pass the whole (unchunked) text once.
429
- // On failure the backend has already exhausted its per-chunk retries and
428
+ // Provider owns chunking + send retry: pass the whole (unchunked) text once.
429
+ // On failure the provider has already exhausted its per-chunk retries and
430
430
  // throws; we re-throw so drainQueue/scheduleRetry requeues the whole item.
431
431
  try {
432
432
  // Hand back any opaque resume token stored from a prior partial-send
433
- // failure so the backend resumes at the failed chunk instead of
433
+ // failure so the provider resumes at the failed chunk instead of
434
434
  // re-sending chunks that already landed. The token is opaque here —
435
435
  // the forwarder only stores/passes/clears it, never interprets it.
436
436
  await this.cb.send(targetChannelId, formatted, {
@@ -462,7 +462,7 @@ class OutputForwarder {
462
462
  ${_bt.trim()}` : _bt.trim();
463
463
  }
464
464
  // sentCount now tracks delivered items (the forwarder no longer knows the
465
- // backend's chunk count). Increment by 1 per delivered item.
465
+ // provider's chunk count). Increment by 1 per delivered item.
466
466
  this.sentCount += 1;
467
467
  this.commitReadProgress(item.nextFileSize);
468
468
  }
@@ -507,7 +507,7 @@ ${_bt.trim()}` : _bt.trim();
507
507
  const ftLen = this.finalLane.length;
508
508
  // A partially-delivered item carries an opaque _resumeToken pinned (by
509
509
  // hash) to its exact text. Mutating its text here would break that hash on
510
- // the next retry, forcing the backend to full-resend from chunk 0 and
510
+ // the next retry, forcing the provider to full-resend from chunk 0 and
511
511
  // duplicate the chunks already delivered. Treat any tokenized item as
512
512
  // sealed: never coalesce/cap-merge into it; new text goes behind it as a
513
513
  // separate item.
@@ -569,15 +569,15 @@ ${_bt.trim()}` : _bt.trim();
569
569
  /** Supervise a per-item send. The underlying promise is AWAITED to real
570
570
  * settlement so `sending` is never released early (no unsupervised send
571
571
  * racing a retry → duplicate). The watchdog's only job is to reset the
572
- * backend's wedged client so the awaited send fails fast. */
572
+ * provider's wedged client so the awaited send fails fast. */
573
573
  _sendSupervised(promise, label) {
574
574
  const settled = Promise.resolve(promise);
575
575
  settled.catch(() => {}); // never leak an unhandled rejection on late settle
576
576
  return new Promise((resolve, reject) => {
577
577
  let capTimer = null;
578
578
  const watchdog = setTimeout(() => {
579
- process.stderr.write(`[output-forwarder] ${label} watchdog fired after ${SEND_ITEM_TIMEOUT_MS}ms; resetting backend\n`);
580
- Promise.resolve(this.cb?.resetBackend?.()).catch(() => {});
579
+ process.stderr.write(`[output-forwarder] ${label} watchdog fired after ${SEND_ITEM_TIMEOUT_MS}ms; resetting provider\n`);
580
+ Promise.resolve(this.cb?.resetProvider?.()).catch(() => {});
581
581
  // Bound the post-reset settlement wait; past the cap, invalidate the
582
582
  // in-flight send (gen bump → zombie can't double-advance) and release.
583
583
  capTimer = setTimeout(() => {
@@ -705,7 +705,7 @@ ${_bt.trim()}` : _bt.trim();
705
705
  // Transient send failure: extractNewText already advanced the read
706
706
  // cursor past these bytes, so dropping the item here would lose the
707
707
  // final text. Requeue the WHOLE item and let drainQueue retry
708
- // instead of silently discarding. The backend owns chunking+retry,
708
+ // instead of silently discarding. The provider owns chunking+retry,
709
709
  // so a requeued send restarts from chunk 0. pendingFinalFlush stays
710
710
  // set so a process restart can also resume the flush.
711
711
  this.finalLane.push(finalItem);