mixdog 0.9.51 → 0.9.53
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/package.json +5 -3
- package/scripts/abort-recovery-test.mjs +17 -1
- package/scripts/agent-model-liveness-test.mjs +79 -2
- package/scripts/anthropic-admission-retry-integration-test.mjs +119 -0
- package/scripts/anthropic-transport-policy-test.mjs +466 -0
- package/scripts/atomic-lock-tryonce-test.mjs +60 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/build-tui.mjs +13 -1
- package/scripts/channel-daemon-smoke.mjs +630 -10
- package/scripts/code-graph-aggregate-cwd-test.mjs +41 -37
- package/scripts/code-graph-disk-hit-test.mjs +11 -0
- package/scripts/code-graph-root-federation-test.mjs +273 -0
- package/scripts/compact-pressure-test.mjs +159 -1
- package/scripts/compact-smoke.mjs +80 -0
- package/scripts/context-mcp-metering-test.mjs +1350 -19
- package/scripts/deferred-tool-loading-test.mjs +17 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/gemini-provider-test.mjs +1053 -0
- package/scripts/interrupted-turn-history-test.mjs +371 -0
- package/scripts/lifecycle-api-test.mjs +137 -0
- package/scripts/max-output-recovery-test.mjs +86 -0
- package/scripts/mcp-grace-deferred-test.mjs +89 -13
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/memory-pg-recovery-test.mjs +59 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +387 -6
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +447 -0
- package/scripts/provider-admission-scheduler-test.mjs +582 -0
- package/scripts/provider-contract-test.mjs +525 -0
- package/scripts/provider-toolcall-test.mjs +492 -11
- package/scripts/reactive-compact-persist-smoke.mjs +59 -0
- package/scripts/resource-admission-test.mjs +789 -0
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/scripts/shell-jobs-windows-hide-test.mjs +1 -1
- package/scripts/steering-drain-buckets-test.mjs +18 -0
- package/scripts/toolcall-args-test.mjs +14 -6
- package/scripts/tui-transcript-perf-test.mjs +5 -7
- package/src/cli.mjs +15 -2
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +27 -13
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +4 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +331 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +114 -308
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +31 -21
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +136 -286
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +42 -5
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +554 -42
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +67 -18
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +84 -150
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -119
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +54 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +28 -74
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +10 -80
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +136 -20
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +94 -33
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +201 -123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +36 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +50 -45
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +78 -12
- package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +44 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +49 -8
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +229 -111
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +130 -63
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +99 -32
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +37 -224
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +81 -42
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +8 -28
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +42 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +220 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +20 -4
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +353 -108
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -12
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +42 -4
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +74 -37
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +223 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +317 -250
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +176 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +93 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +125 -85
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +46 -202
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/adapter.mjs +84 -10
- package/src/runtime/memory/lib/pg/process.mjs +91 -47
- package/src/runtime/memory/lib/pg/supervisor.mjs +50 -17
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +28 -150
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +436 -0
- package/src/runtime/shared/process-shutdown.mjs +33 -4
- package/src/runtime/shared/resource-admission.mjs +364 -0
- package/src/runtime/shared/staged-child-result.mjs +19 -0
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/context-status.mjs +38 -27
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +60 -13
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/provider-request-tools.mjs +72 -0
- package/src/session-runtime/runtime-core.mjs +49 -105
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/session-turn-api.mjs +5 -4
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +390 -104
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +29 -200
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/standalone/channel-daemon-client.mjs +200 -38
- package/src/standalone/channel-daemon-transport.mjs +136 -9
- package/src/standalone/channel-worker.mjs +79 -12
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +275 -343
- package/src/tui/engine/session-api-ext.mjs +50 -6
- package/src/tui/engine/session-api.mjs +1 -1
- package/src/tui/engine/turn.mjs +10 -6
- package/src/tui/engine.mjs +13 -4
- package/src/tui/index.jsx +5 -1
- package/src/tui/lib/voice-setup.mjs +21 -5
- package/scripts/_devtools-stub.mjs +0 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -288
|
@@ -90,6 +90,8 @@ import { createQueryHandlers } from './lib/query-handlers.mjs'
|
|
|
90
90
|
import { createSessionIngestRuntime } from './lib/session-ingest-runtime.mjs'
|
|
91
91
|
import { createMemoryActionHandlers } from './lib/memory-action-handlers.mjs'
|
|
92
92
|
import { createHttpRouter } from './lib/http-router.mjs'
|
|
93
|
+
import { createMemoryPortAdvertiser } from './lib/memory-port-advertiser.mjs'
|
|
94
|
+
import { createMemoryDaemonLifecycle } from './lib/memory-daemon-lifecycle.mjs'
|
|
93
95
|
import {
|
|
94
96
|
readMainConfig,
|
|
95
97
|
readRecapEnabled,
|
|
@@ -141,189 +143,31 @@ const RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
|
|
|
141
143
|
? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
|
|
142
144
|
: path.join(os.tmpdir(), 'mixdog')
|
|
143
145
|
|
|
144
|
-
let _periodicAdvertiseInstalled = false
|
|
145
|
-
let _periodicAdvertiseTimer = null
|
|
146
|
-
// Single module-level advertise retry chain. A newer advertiseMemoryPort call
|
|
147
|
-
// cancels the older chain so a delayed retry never replays a stale boundPort.
|
|
148
|
-
let _advertiseRetryTimer = null
|
|
149
|
-
let _advertiseGeneration = 0
|
|
150
|
-
// Track the most recently advertised port so the periodic tick re-reads it
|
|
151
|
-
// every interval. Without this the setInterval closure binds the FIRST port
|
|
152
|
-
// (the upstream we proxied to) and keeps re-advertising the dead upstream
|
|
153
|
-
// port after fork-proxy promotion swaps in our own locally-bound port.
|
|
154
|
-
let _currentAdvertisedPort = null
|
|
155
|
-
|
|
156
146
|
const MEMORY_SERVER_PID = parsePositivePid(process.env.MIXDOG_SERVER_PID) ?? process.pid
|
|
157
147
|
const _isPidAliveLocal = isPidAliveLocal
|
|
148
|
+
const _memoryPortAdvertiser = createMemoryPortAdvertiser({
|
|
149
|
+
readServiceAdvert: _readServiceAdvert,
|
|
150
|
+
writeServiceAdvert: _writeServiceAdvert,
|
|
151
|
+
parsePositivePid,
|
|
152
|
+
isPidAliveLocal: _isPidAliveLocal,
|
|
153
|
+
memoryServerPid: MEMORY_SERVER_PID,
|
|
154
|
+
log: __mixdogMemoryLog,
|
|
155
|
+
})
|
|
156
|
+
const { advertiseMemoryPort } = _memoryPortAdvertiser
|
|
158
157
|
const MEMORY_DAEMON_MODE = process.env.MIXDOG_MEMORY_DAEMON === '1'
|
|
159
158
|
const MEMORY_IDLE_TTL_MS = Math.max(0, Number(process.env.MIXDOG_MEMORY_IDLE_TTL_MS) || 10 * 60_000)
|
|
160
|
-
let _idleShutdownTimer = null
|
|
161
|
-
|
|
162
|
-
function touchDaemonIdleTimer(reason = 'activity') {
|
|
163
|
-
if (!MEMORY_DAEMON_MODE || MEMORY_IDLE_TTL_MS <= 0) return
|
|
164
|
-
if (_idleShutdownTimer) {
|
|
165
|
-
try { clearTimeout(_idleShutdownTimer) } catch {}
|
|
166
|
-
_idleShutdownTimer = null
|
|
167
|
-
}
|
|
168
|
-
_idleShutdownTimer = setTimeout(() => {
|
|
169
|
-
__mixdogMemoryLog(`[memory-service] daemon idle TTL elapsed after ${reason}; shutting down\n`)
|
|
170
|
-
stop()
|
|
171
|
-
.then(() => process.exit(0))
|
|
172
|
-
.catch((e) => {
|
|
173
|
-
__mixdogMemoryLog(`[memory-service] daemon idle shutdown failed: ${e?.message || e}\n`)
|
|
174
|
-
process.exit(1)
|
|
175
|
-
})
|
|
176
|
-
}, MEMORY_IDLE_TTL_MS)
|
|
177
|
-
_idleShutdownTimer.unref?.()
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ── Connected-client tracking + prompt shutdown ───────────────────────────
|
|
181
|
-
// The daemon is shared by multiple proxy clients (TUI host + channels worker,
|
|
182
|
-
// potentially several sessions). Each client registers/deregisters over HTTP
|
|
183
|
-
// (see /client/register, /client/deregister in lib/http-router.mjs). When the
|
|
184
|
-
// last client goes away we arm a short grace timer (default 10s) so a quick
|
|
185
|
-
// reconnect keeps the daemon warm, but an actually-closed CLI reaps the daemon
|
|
186
|
-
// in seconds instead of waiting out the 10-minute idle TTL (kept as backstop).
|
|
187
159
|
const MEMORY_CLIENT_GRACE_MS = Math.max(0, Number(process.env.MIXDOG_MEMORY_CLIENT_GRACE_MS) || 10_000)
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (!_isPidAliveLocal(pid)) _connectedClients.delete(pid)
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function cancelClientGrace() {
|
|
204
|
-
if (_clientGraceTimer) {
|
|
205
|
-
try { clearTimeout(_clientGraceTimer) } catch {}
|
|
206
|
-
_clientGraceTimer = null
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function armClientGrace(reason = 'last client gone') {
|
|
211
|
-
if (!_clientShutdownEnabled() || _clientGraceTimer) return
|
|
212
|
-
_clientGraceTimer = setTimeout(() => {
|
|
213
|
-
_clientGraceTimer = null
|
|
214
|
-
pruneDeadClients()
|
|
215
|
-
if (_connectedClients.size > 0) return
|
|
216
|
-
__mixdogMemoryLog(`[memory-service] daemon client grace elapsed (${reason}); shutting down\n`)
|
|
217
|
-
stop()
|
|
218
|
-
.then(() => process.exit(0))
|
|
219
|
-
.catch((e) => {
|
|
220
|
-
__mixdogMemoryLog(`[memory-service] daemon client-grace shutdown failed: ${e?.message || e}\n`)
|
|
221
|
-
process.exit(1)
|
|
222
|
-
})
|
|
223
|
-
}, MEMORY_CLIENT_GRACE_MS)
|
|
224
|
-
_clientGraceTimer.unref?.()
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function startClientSweep() {
|
|
228
|
-
if (_clientSweepTimer || !_clientShutdownEnabled()) return
|
|
229
|
-
// Reap clients that died without deregistering so a crashed CLI still frees
|
|
230
|
-
// the daemon in grace-scale time rather than waiting for the idle TTL.
|
|
231
|
-
const interval = Math.max(1000, Math.min(MEMORY_CLIENT_GRACE_MS, 5000))
|
|
232
|
-
_clientSweepTimer = setInterval(() => {
|
|
233
|
-
pruneDeadClients()
|
|
234
|
-
if (_everHadClient && _connectedClients.size === 0) armClientGrace('all clients gone (sweep)')
|
|
235
|
-
}, interval)
|
|
236
|
-
_clientSweepTimer.unref?.()
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function registerClient(clientPid) {
|
|
240
|
-
const pid = parsePositivePid(clientPid)
|
|
241
|
-
if (!pid) return true
|
|
242
|
-
// Reject registration once shutdown has begun. stop() sets _stopPromise
|
|
243
|
-
// synchronously (before its first await) and, in daemon mode, always ends
|
|
244
|
-
// in process.exit — so a daemon that is draining will never revive. Signal
|
|
245
|
-
// the proxy (via a distinct 503) to respawn a fresh daemon instead of
|
|
246
|
-
// binding to this dying one, which would fail the subsequent /api/tool.
|
|
247
|
-
if (_stopPromise) return false
|
|
248
|
-
_connectedClients.set(pid, Date.now())
|
|
249
|
-
_everHadClient = true
|
|
250
|
-
cancelClientGrace()
|
|
251
|
-
startClientSweep()
|
|
252
|
-
return true
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function deregisterClient(clientPid) {
|
|
256
|
-
const pid = parsePositivePid(clientPid)
|
|
257
|
-
if (pid) _connectedClients.delete(pid)
|
|
258
|
-
pruneDeadClients()
|
|
259
|
-
if (_everHadClient && _connectedClients.size === 0) armClientGrace('last client deregistered')
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function advertiseMemoryPort(boundPort, attempt = 0) {
|
|
263
|
-
if (!Number.isFinite(boundPort) || boundPort <= 0) return
|
|
264
|
-
// A fresh top-level advertise (attempt 0) supersedes any pending retry chain:
|
|
265
|
-
// last write wins, so a delayed retry never clobbers a newer boundPort.
|
|
266
|
-
if (attempt === 0) {
|
|
267
|
-
_currentAdvertisedPort = boundPort
|
|
268
|
-
_advertiseGeneration++
|
|
269
|
-
if (_advertiseRetryTimer) { try { clearTimeout(_advertiseRetryTimer) } catch {} ; _advertiseRetryTimer = null }
|
|
270
|
-
}
|
|
271
|
-
const generation = _advertiseGeneration
|
|
272
|
-
if (!_periodicAdvertiseInstalled) {
|
|
273
|
-
_periodicAdvertiseInstalled = true
|
|
274
|
-
_periodicAdvertiseTimer = setInterval(() => {
|
|
275
|
-
try {
|
|
276
|
-
if (_currentAdvertisedPort != null) {
|
|
277
|
-
advertiseMemoryPort(_currentAdvertisedPort)
|
|
278
|
-
}
|
|
279
|
-
} catch {}
|
|
280
|
-
}, 30_000)
|
|
281
|
-
_periodicAdvertiseTimer.unref?.()
|
|
282
|
-
}
|
|
283
|
-
try {
|
|
284
|
-
// Single-writer discovery file (discovery/memory.json), plain atomic rename
|
|
285
|
-
// with NO .lock: memory_port discovery can never be starved by the shared
|
|
286
|
-
// active-instance.json lock. Conflict guard preserved: a live OTHER memory
|
|
287
|
-
// owner advertising a different port is not clobbered.
|
|
288
|
-
const cur = _readServiceAdvert('memory')
|
|
289
|
-
const curMemPort = Number(cur?.port)
|
|
290
|
-
const curMemPid = parsePositivePid(cur?.pid)
|
|
291
|
-
const portConflict = Number.isFinite(curMemPort) && curMemPort > 0 && curMemPort !== boundPort
|
|
292
|
-
const otherOwnerAlive =
|
|
293
|
-
curMemPid != null &&
|
|
294
|
-
curMemPid !== MEMORY_SERVER_PID &&
|
|
295
|
-
_isPidAliveLocal(curMemPid)
|
|
296
|
-
if (portConflict && otherOwnerAlive) {
|
|
297
|
-
__mixdogMemoryLog(`[memory-service] skip memory_port advertise port=${boundPort} curMemPort=${curMemPort} curMemPid=${curMemPid} memoryServerPid=${MEMORY_SERVER_PID}\n`)
|
|
298
|
-
if (generation === _advertiseGeneration) _advertiseRetryTimer = null
|
|
299
|
-
return
|
|
300
|
-
}
|
|
301
|
-
_writeServiceAdvert('memory', {
|
|
302
|
-
port: boundPort,
|
|
303
|
-
...(MEMORY_SERVER_PID ? { pid: MEMORY_SERVER_PID } : {}),
|
|
304
|
-
})
|
|
305
|
-
if (generation === _advertiseGeneration) _advertiseRetryTimer = null
|
|
306
|
-
} catch (e) {
|
|
307
|
-
// Boot path must not serially block on the default 8s lock wait: use a short
|
|
308
|
-
// lock timeout and treat lock contention/timeout as transient so pg_port /
|
|
309
|
-
// memory_port still eventually publish via unref'd, backed-off bg retries.
|
|
310
|
-
const transient =
|
|
311
|
-
e?.code === 'EPERM' || e?.code === 'EBUSY' || e?.code === 'EACCES' ||
|
|
312
|
-
e?.code === 'ELOCKTIMEOUT' || e?.code === 'ELOCKCONTENDED'
|
|
313
|
-
if (transient && attempt < 5 && generation === _advertiseGeneration) {
|
|
314
|
-
const delay = Math.min(2000, 50 * 2 ** attempt)
|
|
315
|
-
// Fire-time generation re-check: even if clearTimeout was missed, a
|
|
316
|
-
// retry from a superseded chain must never republish an old boundPort.
|
|
317
|
-
_advertiseRetryTimer = setTimeout(() => {
|
|
318
|
-
if (generation !== _advertiseGeneration) return
|
|
319
|
-
advertiseMemoryPort(boundPort, attempt + 1)
|
|
320
|
-
}, delay)
|
|
321
|
-
_advertiseRetryTimer.unref?.()
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
__mixdogMemoryLog(`[memory-service] active-instance memory_port advertise failed: ${e?.message || e}\n`)
|
|
325
|
-
}
|
|
326
|
-
}
|
|
160
|
+
const _daemonLifecycle = createMemoryDaemonLifecycle({
|
|
161
|
+
daemonMode: MEMORY_DAEMON_MODE,
|
|
162
|
+
idleTtlMs: MEMORY_IDLE_TTL_MS,
|
|
163
|
+
clientGraceMs: MEMORY_CLIENT_GRACE_MS,
|
|
164
|
+
parsePositivePid,
|
|
165
|
+
isPidAlive: _isPidAliveLocal,
|
|
166
|
+
isStopping: () => _stopPromise != null,
|
|
167
|
+
stop,
|
|
168
|
+
log: __mixdogMemoryLog,
|
|
169
|
+
})
|
|
170
|
+
const { touchDaemonIdleTimer, registerClient, deregisterClient } = _daemonLifecycle
|
|
327
171
|
|
|
328
172
|
const LOCK_FILE = path.join(DATA_DIR, '.memory-service.lock')
|
|
329
173
|
// Owner-election lock. Separate from LOCK_FILE so single-instance mode keeps
|
|
@@ -853,27 +697,9 @@ export async function stop() {
|
|
|
853
697
|
if (_stopPromise) return _stopPromise
|
|
854
698
|
_stopPromise = (async () => {
|
|
855
699
|
_stopCycles()
|
|
856
|
-
|
|
857
|
-
try { clearInterval(_periodicAdvertiseTimer) } catch {}
|
|
858
|
-
_periodicAdvertiseTimer = null
|
|
859
|
-
}
|
|
860
|
-
if (_advertiseRetryTimer) {
|
|
861
|
-
try { clearTimeout(_advertiseRetryTimer) } catch {}
|
|
862
|
-
_advertiseRetryTimer = null
|
|
863
|
-
}
|
|
864
|
-
_advertiseGeneration++
|
|
865
|
-
_periodicAdvertiseInstalled = false
|
|
866
|
-
_currentAdvertisedPort = null
|
|
700
|
+
_memoryPortAdvertiser.reset()
|
|
867
701
|
_embeddingWarmup.reset()
|
|
868
|
-
|
|
869
|
-
try { clearTimeout(_idleShutdownTimer) } catch {}
|
|
870
|
-
_idleShutdownTimer = null
|
|
871
|
-
}
|
|
872
|
-
cancelClientGrace()
|
|
873
|
-
if (_clientSweepTimer) {
|
|
874
|
-
try { clearInterval(_clientSweepTimer) } catch {}
|
|
875
|
-
_clientSweepTimer = null
|
|
876
|
-
}
|
|
702
|
+
_daemonLifecycle.reset()
|
|
877
703
|
await stopLlmWorker()
|
|
878
704
|
resetHttpListenErrorHandler()
|
|
879
705
|
if (_httpBoundPort != null || _httpReadyPromise) {
|
|
@@ -1009,12 +835,14 @@ if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
|
|
|
1009
835
|
// removes port file) then exit(0). Prevents taskkill /F from bypassing
|
|
1010
836
|
// graceful shutdown and leaving pgdata in an inconsistent checkpoint state.
|
|
1011
837
|
let _stopInFlight = false
|
|
838
|
+
let _syncPgStopRequested = false
|
|
1012
839
|
const _workerSignalHandler = (sig) => {
|
|
1013
840
|
if (_stopInFlight) {
|
|
1014
841
|
__mixdogMemoryLog(`[memory-worker] ${sig} — stop already in flight, ignoring\n`)
|
|
1015
842
|
return
|
|
1016
843
|
}
|
|
1017
844
|
_stopInFlight = true
|
|
845
|
+
_syncPgStopRequested = true
|
|
1018
846
|
__mixdogMemoryLog(`[memory-worker] received ${sig} — calling stop() for clean shutdown\n`)
|
|
1019
847
|
const _exitTimer = setTimeout(() => {
|
|
1020
848
|
__mixdogMemoryLog(`[memory-worker] stop() timed out after 6000ms — forcing exit(2)\n`)
|
|
@@ -1037,7 +865,14 @@ if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
|
|
|
1037
865
|
// stop() path runs, orphaning PG mid-write. Best-effort sync pg_ctl stop on
|
|
1038
866
|
// exit (no-op after a completed graceful stop). Skip in secondary mode — we
|
|
1039
867
|
// do not own the shared PG there.
|
|
1040
|
-
if (!memorySecondaryMode()) process.on('exit', () => {
|
|
868
|
+
if (!memorySecondaryMode()) process.on('exit', () => {
|
|
869
|
+
// Do not stop shared PG on an unexpected Memory crash. The singleton proxy
|
|
870
|
+
// will respawn a daemon that can attach to the still-healthy cluster. The
|
|
871
|
+
// sync stop is only a last resort after an explicit shutdown signal began.
|
|
872
|
+
if (_syncPgStopRequested) {
|
|
873
|
+
try { stopPgForShutdownSync() } catch {}
|
|
874
|
+
}
|
|
875
|
+
})
|
|
1041
876
|
|
|
1042
877
|
// callId → AbortController for in-flight IPC calls (cancel handler uses this).
|
|
1043
878
|
const _inFlightCalls = new Map()
|
|
@@ -1112,9 +947,18 @@ if (IS_MEMORY_ENTRY && process.env.MIXDOG_WORKER_MODE !== '1') {
|
|
|
1112
947
|
;(async () => {
|
|
1113
948
|
acquireLock()
|
|
1114
949
|
process.on('exit', releaseLock)
|
|
1115
|
-
|
|
1116
|
-
process.on('
|
|
1117
|
-
|
|
950
|
+
let syncPgStopRequested = false
|
|
951
|
+
if (!memorySecondaryMode()) process.on('exit', () => {
|
|
952
|
+
if (syncPgStopRequested) {
|
|
953
|
+
try { stopPgForShutdownSync() } catch {}
|
|
954
|
+
}
|
|
955
|
+
})
|
|
956
|
+
const stopFromSignal = () => {
|
|
957
|
+
syncPgStopRequested = true
|
|
958
|
+
stop().finally(() => process.exit(0))
|
|
959
|
+
}
|
|
960
|
+
process.on('SIGINT', stopFromSignal)
|
|
961
|
+
process.on('SIGTERM', stopFromSignal)
|
|
1118
962
|
await init()
|
|
1119
963
|
const transport = new StdioServerTransport()
|
|
1120
964
|
await mcp.connect(transport)
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { resolveProjectId, resolveProjectScope } from './project-id-resolver.mjs'
|
|
39
39
|
import { resolvePluginData } from '../../shared/plugin-paths.mjs'
|
|
40
40
|
import { getMetaValue, isBootstrapComplete } from './memory.mjs'
|
|
41
|
+
import { createToolCallHandler } from './tool-call-handler.mjs'
|
|
41
42
|
|
|
42
43
|
export function createMemoryActionHandlers({
|
|
43
44
|
getDb,
|
|
@@ -879,59 +880,7 @@ export function createMemoryActionHandlers({
|
|
|
879
880
|
return { text: `unknown memory action: ${action}`, isError: true }
|
|
880
881
|
}
|
|
881
882
|
|
|
882
|
-
|
|
883
|
-
try {
|
|
884
|
-
if (name === 'search_memories') {
|
|
885
|
-
const result = await handleSearch(args || {}, signal)
|
|
886
|
-
return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
887
|
-
}
|
|
888
|
-
if (name === 'recall') {
|
|
889
|
-
// recall is aiWrapped in the unified build; in standalone mode map it to
|
|
890
|
-
// search_memories so the advertised tool name actually works. Forward
|
|
891
|
-
// every advertised arg so id/limit/offset/sort/includeArchived/
|
|
892
|
-
// includeMembers/includeRaw reach handleSearch instead of being dropped.
|
|
893
|
-
const a = args || {}
|
|
894
|
-
const hasQuery = Array.isArray(a.query)
|
|
895
|
-
? a.query.some((value) => String(value || '').trim())
|
|
896
|
-
: String(a.query ?? '').trim() !== ''
|
|
897
|
-
const recallIds = hasQuery
|
|
898
|
-
? []
|
|
899
|
-
: (Array.isArray(a.id) ? a.id : [a.id])
|
|
900
|
-
.map((value) => Number(value))
|
|
901
|
-
.filter((value) => Number.isInteger(value) && value > 0)
|
|
902
|
-
const searchArgs = {
|
|
903
|
-
...(a.query !== undefined ? { query: a.query } : {}),
|
|
904
|
-
...(recallIds.length > 0 ? { ids: recallIds } : {}),
|
|
905
|
-
...(a.period ? { period: a.period } : {}),
|
|
906
|
-
...(a.limit !== undefined ? { limit: a.limit } : {}),
|
|
907
|
-
...(a.offset !== undefined ? { offset: a.offset } : {}),
|
|
908
|
-
...(a.sort !== undefined ? { sort: a.sort } : {}),
|
|
909
|
-
...(a.category !== undefined ? { category: a.category } : {}),
|
|
910
|
-
...(a.includeArchived !== undefined ? { includeArchived: a.includeArchived } : {}),
|
|
911
|
-
...(a.includeMembers !== undefined ? { includeMembers: a.includeMembers } : {}),
|
|
912
|
-
...(a.includeRaw !== undefined ? { includeRaw: a.includeRaw } : {}),
|
|
913
|
-
...(a.cwd ? { cwd: a.cwd } : {}),
|
|
914
|
-
...(a.projectScope ? { projectScope: a.projectScope } : {}),
|
|
915
|
-
...(a.sessionId ? { sessionId: a.sessionId } : {}),
|
|
916
|
-
...(a.session_id ? { session_id: a.session_id } : {}),
|
|
917
|
-
...(a.currentSession !== undefined ? { currentSession: a.currentSession } : {}),
|
|
918
|
-
// Hint only — never a filter. Marks the caller's own session as
|
|
919
|
-
// "(current)" in the multi-session grouped browse output.
|
|
920
|
-
...(a.currentSessionId ? { currentSessionId: a.currentSessionId } : {}),
|
|
921
|
-
}
|
|
922
|
-
const result = await handleSearch(searchArgs, signal)
|
|
923
|
-
return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
924
|
-
}
|
|
925
|
-
if (name === 'memory') {
|
|
926
|
-
const result = await handleMemoryAction(args || {}, signal)
|
|
927
|
-
return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
928
|
-
}
|
|
929
|
-
return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true }
|
|
930
|
-
} catch (err) {
|
|
931
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
932
|
-
return { content: [{ type: 'text', text: `${name} failed: ${msg}` }], isError: true }
|
|
933
|
-
}
|
|
934
|
-
}
|
|
883
|
+
const handleToolCall = createToolCallHandler({ handleSearch, handleMemoryAction })
|
|
935
884
|
|
|
936
885
|
return { handleMemoryAction, handleToolCall }
|
|
937
886
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Idle and connected-client lifecycle for the shared memory daemon.
|
|
2
|
+
// Timer and client state live here; the facade injects shutdown ownership.
|
|
3
|
+
|
|
4
|
+
export function createMemoryDaemonLifecycle({
|
|
5
|
+
daemonMode,
|
|
6
|
+
idleTtlMs,
|
|
7
|
+
clientGraceMs,
|
|
8
|
+
parsePositivePid,
|
|
9
|
+
isPidAlive,
|
|
10
|
+
isStopping,
|
|
11
|
+
stop,
|
|
12
|
+
log,
|
|
13
|
+
}) {
|
|
14
|
+
let _idleShutdownTimer = null
|
|
15
|
+
const _connectedClients = new Map()
|
|
16
|
+
let _everHadClient = false
|
|
17
|
+
let _clientGraceTimer = null
|
|
18
|
+
let _clientSweepTimer = null
|
|
19
|
+
|
|
20
|
+
function touchDaemonIdleTimer(reason = 'activity') {
|
|
21
|
+
if (!daemonMode || idleTtlMs <= 0) return
|
|
22
|
+
if (_idleShutdownTimer) {
|
|
23
|
+
try { clearTimeout(_idleShutdownTimer) } catch {}
|
|
24
|
+
_idleShutdownTimer = null
|
|
25
|
+
}
|
|
26
|
+
_idleShutdownTimer = setTimeout(() => {
|
|
27
|
+
log(`[memory-service] daemon idle TTL elapsed after ${reason}; shutting down\n`)
|
|
28
|
+
stop()
|
|
29
|
+
.then(() => process.exit(0))
|
|
30
|
+
.catch((e) => {
|
|
31
|
+
log(`[memory-service] daemon idle shutdown failed: ${e?.message || e}\n`)
|
|
32
|
+
process.exit(1)
|
|
33
|
+
})
|
|
34
|
+
}, idleTtlMs)
|
|
35
|
+
_idleShutdownTimer.unref?.()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clientShutdownEnabled() {
|
|
39
|
+
return daemonMode && clientGraceMs > 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function pruneDeadClients() {
|
|
43
|
+
for (const pid of [..._connectedClients.keys()]) {
|
|
44
|
+
if (!isPidAlive(pid)) _connectedClients.delete(pid)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cancelClientGrace() {
|
|
49
|
+
if (_clientGraceTimer) {
|
|
50
|
+
try { clearTimeout(_clientGraceTimer) } catch {}
|
|
51
|
+
_clientGraceTimer = null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function armClientGrace(reason = 'last client gone') {
|
|
56
|
+
if (!clientShutdownEnabled() || _clientGraceTimer) return
|
|
57
|
+
_clientGraceTimer = setTimeout(() => {
|
|
58
|
+
_clientGraceTimer = null
|
|
59
|
+
pruneDeadClients()
|
|
60
|
+
if (_connectedClients.size > 0) return
|
|
61
|
+
log(`[memory-service] daemon client grace elapsed (${reason}); shutting down\n`)
|
|
62
|
+
stop()
|
|
63
|
+
.then(() => process.exit(0))
|
|
64
|
+
.catch((e) => {
|
|
65
|
+
log(`[memory-service] daemon client-grace shutdown failed: ${e?.message || e}\n`)
|
|
66
|
+
process.exit(1)
|
|
67
|
+
})
|
|
68
|
+
}, clientGraceMs)
|
|
69
|
+
_clientGraceTimer.unref?.()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function startClientSweep() {
|
|
73
|
+
if (_clientSweepTimer || !clientShutdownEnabled()) return
|
|
74
|
+
const interval = Math.max(1000, Math.min(clientGraceMs, 5000))
|
|
75
|
+
_clientSweepTimer = setInterval(() => {
|
|
76
|
+
pruneDeadClients()
|
|
77
|
+
if (_everHadClient && _connectedClients.size === 0) armClientGrace('all clients gone (sweep)')
|
|
78
|
+
}, interval)
|
|
79
|
+
_clientSweepTimer.unref?.()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function registerClient(clientPid) {
|
|
83
|
+
const pid = parsePositivePid(clientPid)
|
|
84
|
+
if (!pid) return true
|
|
85
|
+
if (isStopping()) return false
|
|
86
|
+
_connectedClients.set(pid, Date.now())
|
|
87
|
+
_everHadClient = true
|
|
88
|
+
cancelClientGrace()
|
|
89
|
+
startClientSweep()
|
|
90
|
+
return true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function deregisterClient(clientPid) {
|
|
94
|
+
const pid = parsePositivePid(clientPid)
|
|
95
|
+
if (pid) _connectedClients.delete(pid)
|
|
96
|
+
pruneDeadClients()
|
|
97
|
+
if (_everHadClient && _connectedClients.size === 0) armClientGrace('last client deregistered')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function reset() {
|
|
101
|
+
if (_idleShutdownTimer) {
|
|
102
|
+
try { clearTimeout(_idleShutdownTimer) } catch {}
|
|
103
|
+
_idleShutdownTimer = null
|
|
104
|
+
}
|
|
105
|
+
cancelClientGrace()
|
|
106
|
+
if (_clientSweepTimer) {
|
|
107
|
+
try { clearInterval(_clientSweepTimer) } catch {}
|
|
108
|
+
_clientSweepTimer = null
|
|
109
|
+
}
|
|
110
|
+
_connectedClients.clear()
|
|
111
|
+
_everHadClient = false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { touchDaemonIdleTimer, registerClient, deregisterClient, reset }
|
|
115
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Memory service discovery advertisement lifecycle.
|
|
2
|
+
// Owns the retry/periodic timer state so there is exactly one advertise chain.
|
|
3
|
+
|
|
4
|
+
export function createMemoryPortAdvertiser({
|
|
5
|
+
readServiceAdvert,
|
|
6
|
+
writeServiceAdvert,
|
|
7
|
+
parsePositivePid,
|
|
8
|
+
isPidAliveLocal,
|
|
9
|
+
memoryServerPid,
|
|
10
|
+
log,
|
|
11
|
+
}) {
|
|
12
|
+
let _periodicAdvertiseInstalled = false
|
|
13
|
+
let _periodicAdvertiseTimer = null
|
|
14
|
+
// Single module-level advertise retry chain. A newer advertiseMemoryPort call
|
|
15
|
+
// cancels the older chain so a delayed retry never replays a stale boundPort.
|
|
16
|
+
let _advertiseRetryTimer = null
|
|
17
|
+
let _advertiseGeneration = 0
|
|
18
|
+
// Track the most recently advertised port so the periodic tick re-reads it
|
|
19
|
+
// every interval. Without this the setInterval closure binds the FIRST port
|
|
20
|
+
// (the upstream we proxied to) and keeps re-advertising the dead upstream
|
|
21
|
+
// port after fork-proxy promotion swaps in our own locally-bound port.
|
|
22
|
+
let _currentAdvertisedPort = null
|
|
23
|
+
|
|
24
|
+
function advertiseMemoryPort(boundPort, attempt = 0) {
|
|
25
|
+
if (!Number.isFinite(boundPort) || boundPort <= 0) return
|
|
26
|
+
// A fresh top-level advertise (attempt 0) supersedes any pending retry chain:
|
|
27
|
+
// last write wins, so a delayed retry never clobbers a newer boundPort.
|
|
28
|
+
if (attempt === 0) {
|
|
29
|
+
_currentAdvertisedPort = boundPort
|
|
30
|
+
_advertiseGeneration++
|
|
31
|
+
if (_advertiseRetryTimer) { try { clearTimeout(_advertiseRetryTimer) } catch {} ; _advertiseRetryTimer = null }
|
|
32
|
+
}
|
|
33
|
+
const generation = _advertiseGeneration
|
|
34
|
+
if (!_periodicAdvertiseInstalled) {
|
|
35
|
+
_periodicAdvertiseInstalled = true
|
|
36
|
+
_periodicAdvertiseTimer = setInterval(() => {
|
|
37
|
+
try {
|
|
38
|
+
if (_currentAdvertisedPort != null) {
|
|
39
|
+
advertiseMemoryPort(_currentAdvertisedPort)
|
|
40
|
+
}
|
|
41
|
+
} catch {}
|
|
42
|
+
}, 30_000)
|
|
43
|
+
_periodicAdvertiseTimer.unref?.()
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
// Single-writer discovery file (discovery/memory.json), plain atomic rename
|
|
47
|
+
// with NO .lock: memory_port discovery can never be starved by the shared
|
|
48
|
+
// active-instance.json lock. Conflict guard preserved: a live OTHER memory
|
|
49
|
+
// owner advertising a different port is not clobbered.
|
|
50
|
+
const cur = readServiceAdvert('memory')
|
|
51
|
+
const curMemPort = Number(cur?.port)
|
|
52
|
+
const curMemPid = parsePositivePid(cur?.pid)
|
|
53
|
+
const portConflict = Number.isFinite(curMemPort) && curMemPort > 0 && curMemPort !== boundPort
|
|
54
|
+
const otherOwnerAlive =
|
|
55
|
+
curMemPid != null &&
|
|
56
|
+
curMemPid !== memoryServerPid &&
|
|
57
|
+
isPidAliveLocal(curMemPid)
|
|
58
|
+
if (portConflict && otherOwnerAlive) {
|
|
59
|
+
log(`[memory-service] skip memory_port advertise port=${boundPort} curMemPort=${curMemPort} curMemPid=${curMemPid} memoryServerPid=${memoryServerPid}\n`)
|
|
60
|
+
if (generation === _advertiseGeneration) _advertiseRetryTimer = null
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
writeServiceAdvert('memory', {
|
|
64
|
+
port: boundPort,
|
|
65
|
+
...(memoryServerPid ? { pid: memoryServerPid } : {}),
|
|
66
|
+
})
|
|
67
|
+
if (generation === _advertiseGeneration) _advertiseRetryTimer = null
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// Boot path must not serially block on the default 8s lock wait: use a short
|
|
70
|
+
// lock timeout and treat lock contention/timeout as transient so pg_port /
|
|
71
|
+
// memory_port still eventually publish via unref'd, backed-off bg retries.
|
|
72
|
+
const transient =
|
|
73
|
+
e?.code === 'EPERM' || e?.code === 'EBUSY' || e?.code === 'EACCES' ||
|
|
74
|
+
e?.code === 'ELOCKTIMEOUT' || e?.code === 'ELOCKCONTENDED'
|
|
75
|
+
if (transient && attempt < 5 && generation === _advertiseGeneration) {
|
|
76
|
+
const delay = Math.min(2000, 50 * 2 ** attempt)
|
|
77
|
+
// Fire-time generation re-check: even if clearTimeout was missed, a
|
|
78
|
+
// retry from a superseded chain must never republish an old boundPort.
|
|
79
|
+
_advertiseRetryTimer = setTimeout(() => {
|
|
80
|
+
if (generation !== _advertiseGeneration) return
|
|
81
|
+
advertiseMemoryPort(boundPort, attempt + 1)
|
|
82
|
+
}, delay)
|
|
83
|
+
_advertiseRetryTimer.unref?.()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
log(`[memory-service] active-instance memory_port advertise failed: ${e?.message || e}\n`)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function reset() {
|
|
91
|
+
if (_periodicAdvertiseTimer) {
|
|
92
|
+
try { clearInterval(_periodicAdvertiseTimer) } catch {}
|
|
93
|
+
_periodicAdvertiseTimer = null
|
|
94
|
+
}
|
|
95
|
+
if (_advertiseRetryTimer) {
|
|
96
|
+
try { clearTimeout(_advertiseRetryTimer) } catch {}
|
|
97
|
+
_advertiseRetryTimer = null
|
|
98
|
+
}
|
|
99
|
+
_advertiseGeneration++
|
|
100
|
+
_periodicAdvertiseInstalled = false
|
|
101
|
+
_currentAdvertisedPort = null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { advertiseMemoryPort, reset }
|
|
105
|
+
}
|