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.
- package/README.md +0 -17
- package/package.json +8 -7
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +17 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +52 -28
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +8 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +8 -8
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +154 -127
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +7 -7
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
- package/src/tui/{engine → session}/session-api.mjs +7 -60
- package/src/tui/{engine → session}/session-flow.mjs +1 -1
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /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 {
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
310
|
-
//
|
|
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
|
-
|
|
329
|
-
|
|
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
|
|
246
|
+
const kind = name.startsWith('ingest-') ? 'ingest' : 'entry'
|
|
366
247
|
try {
|
|
367
|
-
|
|
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
|
|
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
|
|
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
|
|
38
|
+
// the provider's worst-case per-attempt retries.
|
|
39
39
|
const SEND_ITEM_TIMEOUT_MS = 120_000;
|
|
40
|
-
// After the watchdog resets 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
|
|
407
|
-
// the forwarder stays
|
|
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
|
-
//
|
|
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
|
-
//
|
|
429
|
-
// On failure the
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
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
|
|
580
|
-
Promise.resolve(this.cb?.
|
|
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
|
|
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);
|