mixdog 0.9.134 → 0.9.136
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 +3 -1
- package/scripts/tool-stress.mjs +8 -1
- package/src/agents/maintainer/agent.json +1 -1
- package/src/defaults/skills/setup/SKILL.md +8 -43
- package/src/headless-exec.mjs +26 -9
- package/src/headless-exec.test.mjs +55 -2
- package/src/help.mjs +0 -2
- package/src/rules/lead/01-general.md +0 -4
- package/src/rules/shared/01-tool.md +12 -4
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +30 -4
- package/src/runtime/agent/orchestrator/agent-trace-timing.test.mjs +57 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +15 -6
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +25 -4
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +30 -7
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +150 -41
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +60 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +18 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +10 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +138 -19
- package/src/runtime/agent/orchestrator/tools/builtin/search-glob-top-k.test.mjs +39 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +80 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +82 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +3 -19
- package/src/runtime/agent/orchestrator/tools/env-scrub.test.mjs +25 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +165 -17
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +36 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/channels/lib/config.mjs +14 -101
- package/src/runtime/channels/lib/index-drop-trace.mjs +2 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +4 -80
- package/src/runtime/channels/lib/scheduler.mjs +75 -18
- package/src/runtime/channels/lib/status-snapshot.mjs +5 -68
- package/src/runtime/channels/lib/tool-dispatch.mjs +12 -55
- package/src/runtime/channels/lib/worker-ipc.mjs +1 -16
- package/src/runtime/channels/lib/worker-main.mjs +22 -307
- package/src/runtime/media/renditions.mjs +68 -5
- package/src/runtime/media/renditions.test.mjs +45 -0
- package/src/runtime/media/store.mjs +1 -0
- package/src/runtime/memory/index.mjs +4 -3
- package/src/runtime/memory/lib/cycle-scheduler.mjs +17 -6
- package/src/runtime/memory/lib/memory-cycle-packets.test.mjs +7 -1
- package/src/runtime/memory/lib/memory-cycle-requests.mjs +13 -0
- package/src/runtime/memory/lib/query-handlers.mjs +1 -0
- package/src/runtime/shared/config.mjs +8 -49
- package/src/runtime/shared/schedule-time.mjs +25 -0
- package/src/runtime/shared/schedule-time.test.mjs +29 -0
- package/src/runtime/shared/schedules-db.mjs +73 -1
- package/src/runtime/shared/tool-card-model.mjs +1 -1
- package/src/runtime/shared/transcript-writer.mjs +10 -17
- package/src/runtime/shared/update-checker.mjs +7 -2
- package/src/runtime/shared/update-checker.test.mjs +33 -0
- package/src/session-runtime/channel-config-api.mjs +3 -20
- package/src/session-runtime/config-lifecycle.mjs +1 -42
- package/src/session-runtime/lifecycle-api.mjs +10 -21
- package/src/session-runtime/model-route-api.mjs +0 -2
- package/src/session-runtime/model-settings-persist.test.mjs +0 -1
- package/src/session-runtime/prewarm.mjs +11 -7
- package/src/session-runtime/prewarm.test.mjs +0 -1
- package/src/session-runtime/runtime-core.mjs +20 -186
- package/src/session-runtime/session-lifecycle.mjs +4 -17
- package/src/session-runtime/session-transcript.mjs +54 -0
- package/src/session-runtime/session-turn-api.mjs +4 -44
- package/src/session-runtime/settings-api.mjs +0 -45
- package/src/session-runtime/tool-catalog-schema.mjs +1 -1
- package/src/session-runtime/tool-policy-surface.test.mjs +4 -0
- package/src/standalone/channel-admin.mjs +8 -150
- package/src/standalone/channel-transport.mjs +13 -0
- package/src/standalone/daemon.mjs +19 -49
- package/src/standalone/seeds.mjs +18 -1
- package/src/standalone/session-protocol.mjs +0 -8
- package/src/standalone/session-service.mjs +1 -3
- package/src/tui/App.jsx +0 -5
- package/src/tui/app/app-view.jsx +1 -2
- package/src/tui/app/create-app-pickers.mjs +0 -20
- package/src/tui/app/doctor.mjs +2 -6
- package/src/tui/app/prompt-submit.mjs +2 -49
- package/src/tui/app/settings-picker.mjs +22 -96
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +0 -17
- package/src/tui/app/use-prompt-handlers.mjs +4 -1
- package/src/tui/components/StatusLine.jsx +4 -4
- package/src/tui/dist/index.mjs +1003 -1461
- package/src/tui/session/context-state.mjs +0 -1
- package/src/tui/session/queue-helpers.mjs +1 -1
- package/src/tui/session/queue-helpers.test.mjs +10 -1
- package/src/tui/session/session-api-ext.mjs +0 -89
- package/src/tui/session/session-api.mjs +0 -2
- package/src/tui/session/tool-card-results.mjs +1 -1
- package/src/tui/session/tool-result-status.mjs +3 -3
- package/src/tui/session/tool-result-status.test.mjs +3 -3
- package/src/tui/session-local.mjs +0 -26
- package/src/workflows/default/WORKFLOW.md +13 -15
- package/src/workflows/solo/WORKFLOW.md +6 -7
- package/src/runtime/channels/lib/config-lock.mjs +0 -13
- package/src/runtime/channels/lib/drop-trace.mjs +0 -71
- package/src/runtime/channels/lib/format.mjs +0 -190
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -387
- package/src/runtime/channels/lib/inbound-routing.mjs +0 -81
- package/src/runtime/channels/lib/interaction-handlers.mjs +0 -70
- package/src/runtime/channels/lib/interaction-workflows.mjs +0 -76
- package/src/runtime/channels/lib/memory-client.mjs +0 -289
- package/src/runtime/channels/lib/output-forwarder.mjs +0 -934
- package/src/runtime/channels/lib/session-control.mjs +0 -36
- package/src/runtime/channels/lib/session-discovery.mjs +0 -217
- package/src/runtime/channels/lib/session-discovery.test.mjs +0 -32
- package/src/runtime/channels/lib/telegram-format.mjs +0 -280
- package/src/runtime/channels/lib/tool-format.mjs +0 -125
- package/src/runtime/channels/lib/transcript-binding.mjs +0 -336
- package/src/runtime/channels/lib/transcript-discovery.mjs +0 -208
- package/src/runtime/channels/providers/discord-access.mjs +0 -30
- package/src/runtime/channels/providers/discord-attachments.mjs +0 -69
- package/src/runtime/channels/providers/discord-gateway.mjs +0 -230
- package/src/runtime/channels/providers/discord.mjs +0 -730
- package/src/runtime/channels/providers/telegram.mjs +0 -598
- package/src/runtime/channels/providers/telegram.test.mjs +0 -75
- package/src/session-runtime/remote-control.mjs +0 -126
- package/src/session-runtime/remote-transcript.mjs +0 -134
- package/src/session-runtime/remote-transition-queue.mjs +0 -14
- package/src/tui/app/channel-pickers.mjs +0 -384
|
@@ -12,29 +12,17 @@ import { captureOriginalUserCwd, readLastSessionCwd } from "../../shared/user-cw
|
|
|
12
12
|
import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from "../../shared/runtime-root.mjs";
|
|
13
13
|
import { initProviders } from "../../agent/orchestrator/providers/registry.mjs";
|
|
14
14
|
import { Scheduler } from "./scheduler.mjs";
|
|
15
|
-
import { startSnapshotWriter, stopSnapshotWriter
|
|
15
|
+
import { startSnapshotWriter, stopSnapshotWriter } from "./status-snapshot.mjs";
|
|
16
16
|
import { hasPending as dispatchHasPending } from "../../agent/orchestrator/dispatch-persist.mjs";
|
|
17
17
|
import { setListener as setActivityBusListener } from "../../agent/orchestrator/activity-bus.mjs";
|
|
18
18
|
import { stripSoftWarns } from "../../agent/orchestrator/tool-loop-guard.mjs";
|
|
19
19
|
import { WebhookServer } from "./webhook.mjs";
|
|
20
20
|
import { EventPipeline } from "./event-pipeline.mjs";
|
|
21
21
|
import { startCliWorker } from "./cli-worker-host.mjs";
|
|
22
|
-
import {
|
|
23
|
-
OutputForwarder,
|
|
24
|
-
discoverSessionBoundTranscript,
|
|
25
|
-
findLatestTranscriptByMtime,
|
|
26
|
-
sameResolvedPath
|
|
27
|
-
} from "./output-forwarder.mjs";
|
|
28
|
-
import { controlClaudeSession } from "./session-control.mjs";
|
|
29
22
|
import { JsonStateFile, ensureDir, removeFileIfExists, writeTextFile } from "./state-file.mjs";
|
|
30
|
-
import {
|
|
31
|
-
buildModalRequestSpec,
|
|
32
|
-
PendingInteractionStore
|
|
33
|
-
} from "./interaction-workflows.mjs";
|
|
34
23
|
import {
|
|
35
24
|
ensureRuntimeDirs,
|
|
36
25
|
makeInstanceId,
|
|
37
|
-
getTurnEndPath,
|
|
38
26
|
getStatusPath,
|
|
39
27
|
getChannelOwnerPath,
|
|
40
28
|
getActiveOwnerPid,
|
|
@@ -61,24 +49,13 @@ import {
|
|
|
61
49
|
BENIGN_CRASH_STREAK_WINDOW_MS,
|
|
62
50
|
} from "./crash-log.mjs";
|
|
63
51
|
import { dropTrace, preview, _dtIdxFlush } from "./index-drop-trace.mjs";
|
|
64
|
-
import { createVoiceTranscription } from "./voice-transcription.mjs";
|
|
65
52
|
import { createParentBridge } from "./parent-bridge.mjs";
|
|
66
|
-
import { createInboundRouting } from "./inbound-routing.mjs";
|
|
67
53
|
import { createToolDispatch } from "./tool-dispatch.mjs";
|
|
68
54
|
import { createOwnerHeartbeat } from "./owner-heartbeat.mjs";
|
|
69
|
-
import { createTranscriptBinding } from "./transcript-binding.mjs";
|
|
70
55
|
import { isNetworkError, retryOnNetwork } from "./network-retry.mjs";
|
|
71
56
|
import { runWorkerIpc } from "./worker-ipc.mjs";
|
|
72
|
-
import { createInteractionHandlers } from "./interaction-handlers.mjs";
|
|
73
|
-
import { createInboundHandler } from "./inbound-handler.mjs";
|
|
74
57
|
import { createOwnedRuntime } from "./owned-runtime.mjs";
|
|
75
58
|
import { runWorkerBootstrap } from "./worker-bootstrap.mjs";
|
|
76
|
-
const memoryClientModulePath = new URL("./memory-client.mjs", import.meta.url).href;
|
|
77
|
-
const {
|
|
78
|
-
appendEntry: memoryAppendEntry,
|
|
79
|
-
ingestTranscript: memoryIngestTranscript,
|
|
80
|
-
drainBuffer: memoryDrainBuffer,
|
|
81
|
-
} = await import(memoryClientModulePath);
|
|
82
59
|
// Zombie-Lead repro (2026-07-02): logCrash-then-survive left a worker alive
|
|
83
60
|
// after an unhandled rejection whose async state was already corrupted
|
|
84
61
|
// (observed: EPERM on active-instance.json rename retry), so it spun
|
|
@@ -170,113 +147,12 @@ function writeBridgeState(active) {
|
|
|
170
147
|
function isChannelBridgeActive() {
|
|
171
148
|
return channelBridgeActive;
|
|
172
149
|
}
|
|
173
|
-
let typingChannelId = null;
|
|
174
|
-
const pendingSetup = new PendingInteractionStore();
|
|
175
|
-
function startServerTyping(channelId) {
|
|
176
|
-
if (typingChannelId && typingChannelId !== channelId) {
|
|
177
|
-
provider.stopTyping(typingChannelId);
|
|
178
|
-
}
|
|
179
|
-
typingChannelId = channelId;
|
|
180
|
-
provider.startTyping(channelId);
|
|
181
|
-
}
|
|
182
|
-
function stopServerTyping() {
|
|
183
|
-
if (typingChannelId) {
|
|
184
|
-
provider.stopTyping(typingChannelId);
|
|
185
|
-
typingChannelId = null;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
const TURN_END_FILE = getTurnEndPath(INSTANCE_ID);
|
|
189
|
-
const TURN_END_BASENAME = path.basename(TURN_END_FILE);
|
|
190
|
-
const TURN_END_DIR = path.dirname(TURN_END_FILE);
|
|
191
|
-
let turnEndWatcher = null;
|
|
192
150
|
// Config hot-reload watcher (installed by start(); torn down by stop()).
|
|
193
151
|
let _configWatcher = null;
|
|
194
152
|
let _reloadDebounce = null;
|
|
195
|
-
if (!_isWorkerMode) {
|
|
196
|
-
removeFileIfExists(TURN_END_FILE);
|
|
197
|
-
turnEndWatcher = fs.watch(TURN_END_DIR, async (_event, filename) => {
|
|
198
|
-
if (filename !== TURN_END_BASENAME) return;
|
|
199
|
-
try {
|
|
200
|
-
const stat = fs.statSync(TURN_END_FILE);
|
|
201
|
-
if (stat.size > 0) {
|
|
202
|
-
stopServerTyping();
|
|
203
|
-
await forwarder.forwardFinalText();
|
|
204
|
-
removeFileIfExists(TURN_END_FILE);
|
|
205
|
-
}
|
|
206
|
-
} catch {
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
153
|
const STATUS_FILE = getStatusPath(INSTANCE_ID);
|
|
211
154
|
const statusState = new JsonStateFile(STATUS_FILE, {});
|
|
212
155
|
statusState.ensure();
|
|
213
|
-
const forwarder = new OutputForwarder({
|
|
214
|
-
send: async (ch, text, opts) => {
|
|
215
|
-
if (!channelBridgeActive) {
|
|
216
|
-
throw new Error("send() called while channel bridge is inactive");
|
|
217
|
-
}
|
|
218
|
-
await provider.sendMessage(ch, text, opts);
|
|
219
|
-
},
|
|
220
|
-
formatOutgoing: (text) => provider.formatOutgoing ? provider.formatOutgoing(text) : text,
|
|
221
|
-
recordAssistantTurn: async () => {
|
|
222
|
-
},
|
|
223
|
-
react: (ch, mid, emoji) => {
|
|
224
|
-
if (!channelBridgeActive) return Promise.resolve();
|
|
225
|
-
return provider.react(ch, mid, emoji);
|
|
226
|
-
},
|
|
227
|
-
removeReaction: (ch, mid, emoji) => {
|
|
228
|
-
if (!channelBridgeActive) return Promise.resolve();
|
|
229
|
-
return provider.removeReaction(ch, mid, emoji);
|
|
230
|
-
},
|
|
231
|
-
// Watchdog backstop: force the provider to tear down + rebuild a wedged
|
|
232
|
-
// client so an over-budget send fails fast and the queue releases.
|
|
233
|
-
resetProvider: async () => {
|
|
234
|
-
if (typeof provider?._resetClient === "function") await provider._resetClient();
|
|
235
|
-
}
|
|
236
|
-
}, statusState);
|
|
237
|
-
forwarder.setOnIdle(() => {
|
|
238
|
-
stopServerTyping();
|
|
239
|
-
void forwarder.forwardFinalText();
|
|
240
|
-
});
|
|
241
|
-
// Wire the forwarder ownership probe unconditionally. wireEventQueueHandlers()
|
|
242
|
-
// also sets this, but that path only runs when the event pipeline starts
|
|
243
|
-
// (webhook enabled or event rules present). Without an event pipeline the
|
|
244
|
-
// forwarder's ownerGetter stayed null and _isOwner() failed open, letting a
|
|
245
|
-
// non-owner process forward transcript output (duplicate Discord sends).
|
|
246
|
-
// The closure reads bridgeRuntimeConnected at call time as a fast-path AND;
|
|
247
|
-
// bridgeRuntimeConnected alone can go stale (e.g. this process lost the seat
|
|
248
|
-
// but has not yet observed it), so currentOwnerState().owned is re-read at
|
|
249
|
-
// probe time as the source of truth for ownership.
|
|
250
|
-
forwarder.setOwnerGetter(() => bridgeRuntimeConnected && currentOwnerState().owned);
|
|
251
|
-
// ── Transcript binding cluster ──────────────────────────────────────────────
|
|
252
|
-
// Extracted → lib/transcript-binding.mjs. Bound to live config/identity/owner
|
|
253
|
-
// getters so file-level reference semantics (runtime reloads, ownership flips)
|
|
254
|
-
// are preserved.
|
|
255
|
-
const {
|
|
256
|
-
sessionIdFromTranscriptPath,
|
|
257
|
-
getPersistedTranscriptPath,
|
|
258
|
-
pickUsableTranscriptPath,
|
|
259
|
-
applyTranscriptBinding,
|
|
260
|
-
cancelPendingTranscriptRearm,
|
|
261
|
-
schedulePendingTranscriptRearm,
|
|
262
|
-
rebindTranscriptContext,
|
|
263
|
-
bindPersistedTranscriptIfAny,
|
|
264
|
-
} = createTranscriptBinding({
|
|
265
|
-
forwarder,
|
|
266
|
-
statusState,
|
|
267
|
-
readActiveInstance,
|
|
268
|
-
refreshActiveInstance,
|
|
269
|
-
instanceId: INSTANCE_ID,
|
|
270
|
-
memoryIngestTranscript,
|
|
271
|
-
memoryDrainBuffer,
|
|
272
|
-
dropTrace,
|
|
273
|
-
discoverSessionBoundTranscript,
|
|
274
|
-
sameResolvedPath,
|
|
275
|
-
getConfig: () => config,
|
|
276
|
-
getChannelBridgeActive: () => channelBridgeActive,
|
|
277
|
-
getBridgeRuntimeConnected: () => bridgeRuntimeConnected,
|
|
278
|
-
RUNTIME_ROOT,
|
|
279
|
-
});
|
|
280
156
|
const scheduler = new Scheduler(
|
|
281
157
|
config.nonInteractive ?? [],
|
|
282
158
|
config.interactive ?? [],
|
|
@@ -305,15 +181,6 @@ let bridgeRuntimeConnected = false;
|
|
|
305
181
|
// in-flight start does not revive owner state after the stop already tore
|
|
306
182
|
// the partial-start state down.
|
|
307
183
|
const ACTIVE_OWNER_STALE_MS = 1e4;
|
|
308
|
-
// Owner gating here is multi-process runtime coordination: only the active
|
|
309
|
-
// bindingReady gates all send paths until the boot-time refreshBridgeOwnership
|
|
310
|
-
// ({ restoreBinding: true }) call completes. Without this, scheduler/webhook
|
|
311
|
-
// emissions fired within the first ~few hundred ms after restart drop because
|
|
312
|
-
// the Discord provider binding has not yet been established.
|
|
313
|
-
let bindingReadyStatus = "pending";
|
|
314
|
-
let _bindingReadyResolve;
|
|
315
|
-
const bindingReady = new Promise((r) => { _bindingReadyResolve = r; });
|
|
316
|
-
dropTrace("bindingReady.create", { status: bindingReadyStatus });
|
|
317
184
|
// ── Bridge ownership snapshot + owner heartbeat ─────────────────────────────
|
|
318
185
|
// Extracted → lib/owner-heartbeat.mjs. Owns its own heartbeat timer + last-note
|
|
319
186
|
// dedup; bound to live identity + active-instance primitives.
|
|
@@ -350,19 +217,13 @@ const {
|
|
|
350
217
|
getChannelBridgeActive: () => channelBridgeActive,
|
|
351
218
|
instanceId: INSTANCE_ID,
|
|
352
219
|
TERMINAL_LEAD_PID,
|
|
353
|
-
forwarder,
|
|
354
220
|
sendNotifyToParent,
|
|
355
221
|
scheduler,
|
|
356
222
|
statusState,
|
|
357
223
|
logOwnership,
|
|
358
224
|
currentOwnerState,
|
|
359
|
-
bindPersistedTranscriptIfAny,
|
|
360
|
-
cancelPendingTranscriptRearm,
|
|
361
|
-
schedulePendingTranscriptRearm,
|
|
362
|
-
stopServerTyping,
|
|
363
225
|
wireWebhookHandlers,
|
|
364
226
|
wireEventQueueHandlers,
|
|
365
|
-
memoryDrainBuffer,
|
|
366
227
|
});
|
|
367
228
|
function injectAndRecord(channelId, name, content, options) {
|
|
368
229
|
// Strip soft-warn marker blocks (Tool-loop / Repeated-input / legacy
|
|
@@ -372,7 +233,7 @@ function injectAndRecord(channelId, name, content, options) {
|
|
|
372
233
|
// intentionally prepended onto tool RESULTS upstream (tool-loop-guard.mjs
|
|
373
234
|
// build*Warn) so the model
|
|
374
235
|
// self-corrects, but agent roles commonly echo them and we don't want them
|
|
375
|
-
// surfacing in
|
|
236
|
+
// surfacing in the Lead channel push.
|
|
376
237
|
if (typeof content === 'string') content = stripSoftWarns(content);
|
|
377
238
|
const ts = new Date().toISOString();
|
|
378
239
|
const now = new Date();
|
|
@@ -382,39 +243,10 @@ function injectAndRecord(channelId, name, content, options) {
|
|
|
382
243
|
if (options?.instruction) meta.instruction = options.instruction;
|
|
383
244
|
if (options?.type) meta.type = options.type;
|
|
384
245
|
// `silent_to_agent` — lifecycle status pings (worker/iter/started echoes)
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// can recognise and drop it. Default is false → legacy behaviour preserved.
|
|
390
|
-
if (options?.silent_to_agent) meta.silent_to_agent = true;
|
|
391
|
-
const silent = options?.silent_to_agent === true;
|
|
392
|
-
if (!silent) {
|
|
393
|
-
sendNotifyToParent("notifications/claude/channel", { content, meta });
|
|
394
|
-
} else {
|
|
395
|
-
forwardLifecycleToDiscord(channelId, content);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// Best-effort direct Discord emission for silent-to-agent lifecycle pings.
|
|
400
|
-
// Only used when the parent-notify hop is skipped, so the user still sees
|
|
401
|
-
// the status on Discord even though Lead will never echo it through the
|
|
402
|
-
// transcript-tail forwarder. Falls back to a no-op when no channel is
|
|
403
|
-
// resolvable — lifecycle pings are non-critical.
|
|
404
|
-
function forwardLifecycleToDiscord(channelId, content) {
|
|
405
|
-
try {
|
|
406
|
-
// Skip rather than guess: lifecycle callers pass the channelId they own;
|
|
407
|
-
// falling back to statusState.channelId can route to a stale/unrelated
|
|
408
|
-
// channel when the caller did not supply one intentionally.
|
|
409
|
-
const target = channelId || null;
|
|
410
|
-
dropTrace("send.lifecycle.entry", { channelId: target || "(none)", bindingReadyStatus, providerPresent: !!provider?.sendMessage, preview: preview(content) });
|
|
411
|
-
if (!target || !provider?.sendMessage) return;
|
|
412
|
-
void bindingReady.then(() =>
|
|
413
|
-
provider.sendMessage(target, content)
|
|
414
|
-
.then(() => dropTrace("send.lifecycle.ok", { channelId: target }))
|
|
415
|
-
.catch((err) => dropTrace("send.lifecycle.err", { channelId: target, err: String(err) }))
|
|
416
|
-
).catch(() => {});
|
|
417
|
-
} catch { /* best-effort */ }
|
|
246
|
+
// must NOT land in Lead's context window. The channel relay that used to
|
|
247
|
+
// surface them is retired, so silent pings are dropped entirely.
|
|
248
|
+
if (options?.silent_to_agent === true) return;
|
|
249
|
+
sendNotifyToParent("notifications/claude/channel", { content, meta });
|
|
418
250
|
}
|
|
419
251
|
scheduler.setInjectHandler((channelId, name, content, options) => {
|
|
420
252
|
injectAndRecord(channelId, name, content, options);
|
|
@@ -423,24 +255,15 @@ scheduler.setInjectHandler((channelId, name, content, options) => {
|
|
|
423
255
|
// actually holds the live bridge seat; otherwise the scheduler falls back to
|
|
424
256
|
// the visible-session run so the fire is never lost.
|
|
425
257
|
scheduler.setInjectReadyCheck(() => bridgeRuntimeConnected && currentOwnerState().owned);
|
|
426
|
-
scheduler.setSendHandler(async (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
dropTrace("send.scheduler.ready", { channelId });
|
|
430
|
-
try {
|
|
431
|
-
await provider.sendMessage(channelId, text);
|
|
432
|
-
dropTrace("send.scheduler.ok", { channelId });
|
|
433
|
-
} catch (err) {
|
|
434
|
-
dropTrace("send.scheduler.err", { channelId, err: String(err) });
|
|
435
|
-
throw err;
|
|
436
|
-
}
|
|
258
|
+
scheduler.setSendHandler(async () => {
|
|
259
|
+
// Channel delivery is retired: schedule results surface in the app session
|
|
260
|
+
// (parent notify / Automations row) only.
|
|
437
261
|
});
|
|
438
262
|
function wireWebhookHandlers() {
|
|
439
263
|
if (!webhookServer) return;
|
|
440
264
|
webhookServer.setEventPipeline(eventPipeline);
|
|
441
|
-
// Webhook fires run as sessions (schedules parity)
|
|
442
|
-
//
|
|
443
|
-
// 'channel'/'both' → the run result also relays to the main channel.
|
|
265
|
+
// Webhook fires run as sessions (schedules parity); the Automations
|
|
266
|
+
// session row is the only surface (channel relay retired).
|
|
444
267
|
webhookServer.setBridgeDispatch(async ({ prompt, model, cwd, workflow, attachments, delivery, context }) => {
|
|
445
268
|
const { runWebhookSession } = await import("../../shared/webhook-session-run.mjs");
|
|
446
269
|
const run = await runWebhookSession({
|
|
@@ -452,14 +275,6 @@ function wireWebhookHandlers() {
|
|
|
452
275
|
delivery: delivery || null,
|
|
453
276
|
prompt,
|
|
454
277
|
});
|
|
455
|
-
const mode = String(delivery || "app");
|
|
456
|
-
if ((mode === "channel" || mode === "both") && run?.result && provider?.sendMessage) {
|
|
457
|
-
const target = scheduler.resolveChannel("");
|
|
458
|
-
if (target) {
|
|
459
|
-
void bindingReady.then(() => provider.sendMessage(target, run.result))
|
|
460
|
-
.catch((err) => dropTrace("send.webhook.err", { channelId: target, err: String(err) }));
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
278
|
return run;
|
|
464
279
|
});
|
|
465
280
|
}
|
|
@@ -474,127 +289,37 @@ function wireEventQueueHandlers(eventQueue) {
|
|
|
474
289
|
// bridgeRuntimeConnected is a fast-path AND; currentOwnerState().owned is
|
|
475
290
|
// re-read at probe time so a stale-connected flag cannot mask a lost seat.
|
|
476
291
|
eventQueue.setOwnerGetter(() => bridgeRuntimeConnected && currentOwnerState().owned);
|
|
477
|
-
forwarder.setOwnerGetter(() => bridgeRuntimeConnected && currentOwnerState().owned);
|
|
478
292
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
instanceId: INSTANCE_ID,
|
|
483
|
-
getBridgeOwnershipSnapshot,
|
|
484
|
-
refreshBridgeOwnershipSafe,
|
|
485
|
-
pendingSetup,
|
|
486
|
-
buildModalRequestSpec,
|
|
487
|
-
loadProfileConfig,
|
|
488
|
-
sendNotifyToParent,
|
|
489
|
-
scheduler,
|
|
490
|
-
controlClaudeSession,
|
|
491
|
-
writeTextFile,
|
|
492
|
-
TURN_END_FILE,
|
|
493
|
-
});
|
|
494
|
-
const { isVoiceAttachment, transcribeVoice } = createVoiceTranscription({
|
|
495
|
-
getConfig: () => config,
|
|
496
|
-
dataDir: DATA_DIR,
|
|
497
|
-
});
|
|
293
|
+
// Inbound messaging, provider interaction handlers, and channel voice-message
|
|
294
|
+
// transcription are DELETED with Discord/Telegram (user decision: PWA replaces
|
|
295
|
+
// channels). The headless provider emits no events, so nothing wires here.
|
|
498
296
|
import { TOOL_DEFS } from '../tool-defs.mjs';
|
|
499
297
|
// Tool dispatch in worker mode goes through the IPC `call` handler at the
|
|
500
298
|
// bottom of this file (parent's `callWorker` → `handleToolCall`). There is no
|
|
501
299
|
// orphan worker-level MCP Server: the parent (server.mjs) owns the single
|
|
502
300
|
// connected transport and routes CallTool through the IPC `call` path.
|
|
503
|
-
// ── Inbound routing / dedup / ownership helpers ─────────────────────────────
|
|
504
|
-
// Extracted → lib/inbound-routing.mjs. Bound to live config/identity getters.
|
|
505
|
-
const {
|
|
506
|
-
writeChannelOwner,
|
|
507
|
-
shouldDropDuplicateInbound,
|
|
508
|
-
resolveInboundRoute,
|
|
509
|
-
} = createInboundRouting({
|
|
510
|
-
getConfig: () => config,
|
|
511
|
-
getInstanceId: () => INSTANCE_ID,
|
|
512
|
-
getChannelOwnerPath,
|
|
513
|
-
});
|
|
514
301
|
// ── Worker/HTTP tool-call dispatch ──────────────────────────────────────────
|
|
515
|
-
//
|
|
516
|
-
//
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
// the module reads live file-level references at call time (original closure
|
|
520
|
-
// semantics preserved). Used by the HTTP MCP CallTool path and the worker IPC
|
|
521
|
-
// `call` handler at the bottom of this file.
|
|
302
|
+
// Runtime lifecycle dispatch only (activate_channel_bridge / reload_config):
|
|
303
|
+
// threaded as a lifecycle bag of lazy getters so the module reads live
|
|
304
|
+
// file-level references at call time. Used by the HTTP MCP CallTool path and
|
|
305
|
+
// the worker IPC `call` handler at the bottom of this file.
|
|
522
306
|
const {
|
|
523
307
|
handleToolCall,
|
|
524
308
|
handleToolCallWithBridgeRetry,
|
|
525
309
|
} = createToolDispatch({
|
|
526
|
-
getForwarder: () => forwarder,
|
|
527
310
|
isChannelsDegraded,
|
|
528
311
|
lifecycle: {
|
|
529
|
-
getBridgeRuntimeConnected: () => bridgeRuntimeConnected,
|
|
530
312
|
getChannelBridgeActive: () => channelBridgeActive,
|
|
531
313
|
getOwned: () => getBridgeOwnershipSnapshot().owned,
|
|
532
314
|
setChannelBridgeActive: (v) => { channelBridgeActive = v; },
|
|
533
315
|
writeBridgeState,
|
|
534
|
-
stopServerTyping,
|
|
535
316
|
notifyRemoteAcquired,
|
|
536
317
|
refreshBridgeOwnership,
|
|
537
|
-
bindPersistedTranscriptIfAny,
|
|
538
|
-
// Lead-pushed repoint: bind the exact transcript the lead just created
|
|
539
|
-
// (auto-acquire / newSession / resume / clear) instead of waiting for the
|
|
540
|
-
// next inbound parent-chain steal. Idempotent + best-effort: same binding
|
|
541
|
-
// path as the inbound steal (rebindTranscriptContext -> applyTranscriptBinding).
|
|
542
|
-
rebindCurrentTranscript: async (transcriptPath) => {
|
|
543
|
-
const cleanPath = typeof transcriptPath === "string" ? transcriptPath.trim() : "";
|
|
544
|
-
if (!cleanPath) return;
|
|
545
|
-
const channelId = statusState.read().channelId || config.channelId;
|
|
546
|
-
if (!channelId) return;
|
|
547
|
-
// Fail-closed: a malformed / not-yet-on-disk path must NEVER fall through
|
|
548
|
-
// to rebindTranscriptContext's discovery loop (which would mutate the
|
|
549
|
-
// binding onto a different session). Only an existing regular file binds;
|
|
550
|
-
// otherwise log + return so the current binding is left untouched.
|
|
551
|
-
let exists = false;
|
|
552
|
-
try { exists = fs.statSync(cleanPath).isFile(); } catch { exists = false; }
|
|
553
|
-
if (!exists) {
|
|
554
|
-
process.stderr.write(`mixdog: rebind_current_transcript: ignoring non-existent path ${cleanPath}\n`);
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
// Idempotent: already bound to this exact transcript => no-op. In
|
|
558
|
-
// particular do NOT re-run recoverUnsyncedTail, which is only meaningful
|
|
559
|
-
// when the binding actually changes.
|
|
560
|
-
if (forwarder.hasBinding() && sameResolvedPath(forwarder.transcriptPath, cleanPath)) return;
|
|
561
|
-
// Binding changed: same path as the inbound steal's applyTranscriptBinding.
|
|
562
|
-
applyTranscriptBinding(channelId, cleanPath, {
|
|
563
|
-
persistStatus: true,
|
|
564
|
-
recoverUnsyncedTail: true,
|
|
565
|
-
});
|
|
566
|
-
},
|
|
567
318
|
startChannelBridge: () => start({ messaging: true }),
|
|
568
319
|
stopOwnedRuntime,
|
|
569
320
|
reloadRuntimeConfig,
|
|
570
321
|
},
|
|
571
322
|
});
|
|
572
|
-
createInboundHandler({
|
|
573
|
-
getProvider: () => provider,
|
|
574
|
-
getConfig: () => config,
|
|
575
|
-
getBridgeRuntimeConnected: () => bridgeRuntimeConnected,
|
|
576
|
-
getChannelBridgeActive: () => channelBridgeActive,
|
|
577
|
-
instanceId: INSTANCE_ID,
|
|
578
|
-
forwarder,
|
|
579
|
-
scheduler,
|
|
580
|
-
statusState,
|
|
581
|
-
getBridgeOwnershipSnapshot,
|
|
582
|
-
refreshBridgeOwnershipSafe,
|
|
583
|
-
writeChannelOwner,
|
|
584
|
-
shouldDropDuplicateInbound,
|
|
585
|
-
resolveInboundRoute,
|
|
586
|
-
isVoiceAttachment,
|
|
587
|
-
transcribeVoice,
|
|
588
|
-
getPersistedTranscriptPath,
|
|
589
|
-
sessionIdFromTranscriptPath,
|
|
590
|
-
pickUsableTranscriptPath,
|
|
591
|
-
applyTranscriptBinding,
|
|
592
|
-
rebindTranscriptContext,
|
|
593
|
-
sendNotifyToParent,
|
|
594
|
-
memoryAppendEntry,
|
|
595
|
-
startServerTyping,
|
|
596
|
-
stopServerTyping,
|
|
597
|
-
});
|
|
598
323
|
async function init(_sharedMcp) {
|
|
599
324
|
// _sharedMcp is no longer used. Notifications now flow via IPC to the parent
|
|
600
325
|
// (sendNotifyToParent above). The parameter is retained for backward
|
|
@@ -626,17 +351,11 @@ async function start(options = {}) {
|
|
|
626
351
|
// Daemon model: this runtime is the machine-global singleton bridge owner
|
|
627
352
|
// (enforced by the standalone daemon's singleton-owner lock), so there is no
|
|
628
353
|
// seat to claim and no contender to make-before-break against. Just connect
|
|
629
|
-
// the owned runtime
|
|
630
|
-
const _bindingReadyStart = Date.now();
|
|
354
|
+
// the owned runtime.
|
|
631
355
|
try {
|
|
632
|
-
await startOwnedRuntime(
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
_bindingReadyResolve(true);
|
|
636
|
-
} catch (e) {
|
|
637
|
-
bindingReadyStatus = "rejected";
|
|
638
|
-
dropTrace("bindingReady.reject", { elapsedMs: Date.now() - _bindingReadyStart, status: bindingReadyStatus, err: String(e) });
|
|
639
|
-
_bindingReadyResolve(e);
|
|
356
|
+
await startOwnedRuntime();
|
|
357
|
+
} catch {
|
|
358
|
+
// Non-fatal: owned-runtime start errors degrade to automation-only mode.
|
|
640
359
|
}
|
|
641
360
|
// No-op under the daemon model (kept for call-site stability): there is no
|
|
642
361
|
// ownership timer — the singleton daemon guarantees exactly one owner.
|
|
@@ -668,10 +387,6 @@ async function stop() {
|
|
|
668
387
|
clearBridgeOwnershipTimer();
|
|
669
388
|
if (_reloadDebounce) { clearTimeout(_reloadDebounce); _reloadDebounce = null; }
|
|
670
389
|
if (_configWatcher) { try { _configWatcher.close(); } catch {} _configWatcher = null; }
|
|
671
|
-
if (turnEndWatcher) {
|
|
672
|
-
try { turnEndWatcher.close(); } catch {}
|
|
673
|
-
turnEndWatcher = null;
|
|
674
|
-
}
|
|
675
390
|
}
|
|
676
391
|
// ── IPC worker mode ──────────────────────────────────────────────
|
|
677
392
|
// Skipped under the machine-global host (MIXDOG_DAEMON_HOST=1): the daemon
|
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
* what made the remote gallery slow.
|
|
15
15
|
*/
|
|
16
16
|
import { spawn } from 'child_process';
|
|
17
|
-
import {
|
|
17
|
+
import { randomBytes } from 'crypto';
|
|
18
|
+
import {
|
|
19
|
+
existsSync,
|
|
20
|
+
mkdirSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
renameSync,
|
|
23
|
+
statSync,
|
|
24
|
+
unlinkSync,
|
|
25
|
+
writeFileSync,
|
|
26
|
+
} from 'fs';
|
|
18
27
|
import { dirname, join } from 'path';
|
|
19
28
|
|
|
20
29
|
import { resolvePluginData } from '../shared/plugin-paths.mjs';
|
|
@@ -35,6 +44,12 @@ const POSTER_TIMEOUT_MS = 20_000;
|
|
|
35
44
|
const MAX_POSTER_BYTES = 32 * 1024 * 1024;
|
|
36
45
|
const FAILED_RENDITION_COOLDOWN_MS = 30_000;
|
|
37
46
|
const MAX_CACHED_RENDITION_BYTES = 4 * 1024 * 1024;
|
|
47
|
+
const DEFAULT_RENDITION_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
48
|
+
const configuredRenditionCacheMaxBytes = Number(process.env.MIXDOG_RENDITION_CACHE_MAX_BYTES);
|
|
49
|
+
export const RENDITION_CACHE_MAX_BYTES = Number.isFinite(configuredRenditionCacheMaxBytes)
|
|
50
|
+
&& configuredRenditionCacheMaxBytes >= MAX_CACHED_RENDITION_BYTES
|
|
51
|
+
? Math.floor(configuredRenditionCacheMaxBytes)
|
|
52
|
+
: DEFAULT_RENDITION_CACHE_MAX_BYTES;
|
|
38
53
|
|
|
39
54
|
const MEDIA_VARIANTS = Object.keys(SPECS);
|
|
40
55
|
|
|
@@ -89,14 +104,62 @@ export function removeRenditions(cacheDir, id) {
|
|
|
89
104
|
}
|
|
90
105
|
}
|
|
91
106
|
|
|
107
|
+
/** Bound the rebuildable rendition tree by evicting the oldest files first. */
|
|
108
|
+
export function pruneRenditionCache(cacheDir, {
|
|
109
|
+
maxBytes = RENDITION_CACHE_MAX_BYTES,
|
|
110
|
+
protectedPath = '',
|
|
111
|
+
} = {}) {
|
|
112
|
+
const budget = Math.max(0, Math.floor(Number(maxBytes) || 0));
|
|
113
|
+
const rows = [];
|
|
114
|
+
let totalBytes = 0;
|
|
115
|
+
for (const variant of MEDIA_VARIANTS) {
|
|
116
|
+
const dir = join(cacheDir, variant);
|
|
117
|
+
let names;
|
|
118
|
+
try { names = readdirSync(dir); } catch { continue; }
|
|
119
|
+
for (const name of names) {
|
|
120
|
+
if (!Object.keys(EXTENSION_MIME).some((extension) => name.endsWith(extension))) continue;
|
|
121
|
+
const path = join(dir, name);
|
|
122
|
+
try {
|
|
123
|
+
const stat = statSync(path);
|
|
124
|
+
if (!stat.isFile()) continue;
|
|
125
|
+
rows.push({ path, bytes: stat.size, mtimeMs: stat.mtimeMs });
|
|
126
|
+
totalBytes += stat.size;
|
|
127
|
+
} catch { /* raced with another writer/pruner */ }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
rows.sort((a, b) => (a.mtimeMs - b.mtimeMs) || a.path.localeCompare(b.path));
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
if (totalBytes <= budget) break;
|
|
133
|
+
if (row.path === protectedPath) continue;
|
|
134
|
+
try {
|
|
135
|
+
unlinkSync(row.path);
|
|
136
|
+
totalBytes -= row.bytes;
|
|
137
|
+
} catch { /* another process already removed it */ }
|
|
138
|
+
}
|
|
139
|
+
return { bytes: Math.max(0, totalBytes), entries: rows.length };
|
|
140
|
+
}
|
|
141
|
+
|
|
92
142
|
function writeRendition(cacheDir, variant, id, extension, buffer) {
|
|
143
|
+
if (!Buffer.isBuffer(buffer) || buffer.length === 0 || buffer.length > MAX_CACHED_RENDITION_BYTES) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
93
146
|
const path = renditionPath(cacheDir, variant, id, extension);
|
|
94
147
|
mkdirSync(dirname(path), { recursive: true });
|
|
95
148
|
// Stage-then-rename: a reader must never observe a half-written cache file.
|
|
96
|
-
const staging = `${path}.${process.pid}.tmp`;
|
|
149
|
+
const staging = `${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
97
150
|
writeFileSync(staging, buffer);
|
|
98
|
-
|
|
99
|
-
|
|
151
|
+
try {
|
|
152
|
+
renameSync(staging, path);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
// Another process may have won the same cache publication race.
|
|
155
|
+
if (!existsSync(path)) throw error;
|
|
156
|
+
} finally {
|
|
157
|
+
try { unlinkSync(staging); } catch { /* renamed or already removed */ }
|
|
158
|
+
}
|
|
159
|
+
let bytes = buffer.length;
|
|
160
|
+
try { bytes = statSync(path).size; } catch { /* use written size */ }
|
|
161
|
+
pruneRenditionCache(cacheDir, { protectedPath: path });
|
|
162
|
+
return { path, mime: EXTENSION_MIME[extension], bytes };
|
|
100
163
|
}
|
|
101
164
|
|
|
102
165
|
export function createPriorityScheduler(limit = 2) {
|
|
@@ -246,7 +309,7 @@ async function generate({ id, kind, sourcePath, variant, spec, cacheDir }) {
|
|
|
246
309
|
const frame = await grabVideoFrame(sourcePath, spec);
|
|
247
310
|
if (!frame) return null;
|
|
248
311
|
const written = writeRendition(cacheDir, variant, id, '.jpg', frame.buffer);
|
|
249
|
-
return { ...written, durationSeconds: frame.durationSeconds };
|
|
312
|
+
return written ? { ...written, durationSeconds: frame.durationSeconds } : null;
|
|
250
313
|
}
|
|
251
314
|
const encoded = await encodeStill(sourcePath, spec);
|
|
252
315
|
return encoded ? writeRendition(cacheDir, variant, id, encoded.extension, encoded.buffer) : null;
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
rmSync,
|
|
6
|
+
utimesSync,
|
|
7
|
+
} from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
2
10
|
import { test } from 'node:test';
|
|
3
11
|
|
|
4
12
|
import {
|
|
13
|
+
cacheRendition,
|
|
5
14
|
createPriorityScheduler,
|
|
15
|
+
pruneRenditionCache,
|
|
6
16
|
videoPosterArguments,
|
|
7
17
|
} from './renditions.mjs';
|
|
8
18
|
|
|
@@ -52,3 +62,38 @@ test('rendition scheduler bounds work and prioritizes visible requests over queu
|
|
|
52
62
|
await Promise.all([first, second, third]);
|
|
53
63
|
assert.equal(peak, 2);
|
|
54
64
|
});
|
|
65
|
+
|
|
66
|
+
test('rendition files reject oversized entries and obey a total disk budget', () => {
|
|
67
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-rendition-cache-'));
|
|
68
|
+
try {
|
|
69
|
+
const first = cacheRendition({
|
|
70
|
+
id: 'first',
|
|
71
|
+
variant: 'thumb',
|
|
72
|
+
mime: 'image/jpeg',
|
|
73
|
+
buffer: Buffer.alloc(10, 1),
|
|
74
|
+
cacheDir: root,
|
|
75
|
+
});
|
|
76
|
+
const second = cacheRendition({
|
|
77
|
+
id: 'second',
|
|
78
|
+
variant: 'thumb',
|
|
79
|
+
mime: 'image/jpeg',
|
|
80
|
+
buffer: Buffer.alloc(10, 2),
|
|
81
|
+
cacheDir: root,
|
|
82
|
+
});
|
|
83
|
+
assert.ok(first && second);
|
|
84
|
+
utimesSync(first.path, new Date(1_000), new Date(1_000));
|
|
85
|
+
const pruned = pruneRenditionCache(root, { maxBytes: 10 });
|
|
86
|
+
assert.equal(pruned.bytes, 10);
|
|
87
|
+
assert.equal(existsSync(first.path), false);
|
|
88
|
+
assert.equal(existsSync(second.path), true);
|
|
89
|
+
assert.equal(cacheRendition({
|
|
90
|
+
id: 'oversized',
|
|
91
|
+
variant: 'thumb',
|
|
92
|
+
mime: 'image/jpeg',
|
|
93
|
+
buffer: Buffer.alloc(4 * 1024 * 1024 + 1),
|
|
94
|
+
cacheDir: root,
|
|
95
|
+
}), null);
|
|
96
|
+
} finally {
|
|
97
|
+
rmSync(root, { recursive: true, force: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
@@ -217,6 +217,7 @@ export function saveMediaAsset({ kind, lane, model, prompt, options = {}, mime,
|
|
|
217
217
|
for (const dropped of assets.slice(MAX_INDEX_ENTRIES)) {
|
|
218
218
|
const droppedPath = storedAssetPath(dropped.file);
|
|
219
219
|
try { if (droppedPath) unlinkSync(droppedPath); } catch {}
|
|
220
|
+
removeRenditions(renditionsDir(), dropped.id);
|
|
220
221
|
}
|
|
221
222
|
writeIndex(kept);
|
|
222
223
|
});
|
|
@@ -58,7 +58,7 @@ import { startLlmWorker, stopLlmWorker } from './lib/llm-worker-host.mjs'
|
|
|
58
58
|
import { runCycle1, runCycle2, runCycle3, runUnifiedGate, parseInterval, syncRootEmbedding, flushRawEmbeddings, applySimpleStatus, applyUpdate, applyMerge, CYCLE2_ACTIVE_TARGET_CAP } from './lib/memory-cycle.mjs'
|
|
59
59
|
import { callAgentDispatch } from './lib/agent-ipc.mjs'
|
|
60
60
|
import { getInFlightCycle1 } from './lib/memory-cycle1.mjs'
|
|
61
|
-
import { claimAndMarkScheduledCycle, markCycleRequest, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
61
|
+
import { cancelCoalescedCycleRetries, claimAndMarkScheduledCycle, markCycleRequest, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
62
62
|
import { searchRelevantHybrid } from './lib/memory-recall-store.mjs'
|
|
63
63
|
import { fetchEntriesByIdsScoped } from './lib/memory-recall-id-patch.mjs'
|
|
64
64
|
import { retrieveEntries } from './lib/memory-retrievers.mjs'
|
|
@@ -420,6 +420,7 @@ const _cycleScheduler = createCycleScheduler({
|
|
|
420
420
|
markCycleRequest,
|
|
421
421
|
resolveCoalesceMaxRetries,
|
|
422
422
|
scheduleCoalescedCycleRetry,
|
|
423
|
+
cancelCoalescedCycleRetries,
|
|
423
424
|
scheduledCycle1Signature,
|
|
424
425
|
scheduledCycle2Signature,
|
|
425
426
|
scheduledCycle3Signature,
|
|
@@ -643,8 +644,8 @@ export async function init() {
|
|
|
643
644
|
await runtimeReady
|
|
644
645
|
} catch (e) {
|
|
645
646
|
// Runtime init failed AFTER we advertised the HTTP port. Leaving the
|
|
646
|
-
// listener up would answer discovery with a live 503, which
|
|
647
|
-
//
|
|
647
|
+
// listener up would answer discovery with a live 503, which HTTP
|
|
648
|
+
// ingest callers treat as a delivered (non-buffered) write — silently
|
|
648
649
|
// dropping entries that would otherwise be buffered when no port is
|
|
649
650
|
// advertised. stop() withdraws the advert (clears _currentAdvertisedPort
|
|
650
651
|
// + cancels the periodic re-advertise) and closes the HTTP server, so
|