mixdog 0.9.135 → 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/help.mjs +0 -2
- package/src/rules/lead/01-general.md +0 -4
- package/src/rules/shared/01-tool.md +10 -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/builtin-tools.mjs +0 -5
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +17 -4
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +40 -0
- 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 +34 -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 +124 -18
- 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/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/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +78 -12
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +34 -3
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -5
- 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/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 +1 -19
- 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 +2 -7
- package/src/session-runtime/prewarm.test.mjs +0 -1
- package/src/session-runtime/runtime-core.mjs +6 -185
- 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/standalone/channel-admin.mjs +8 -150
- package/src/standalone/daemon.mjs +5 -48
- 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
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import { readFileSync, mkdirSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { DiscordProvider } from "../providers/discord.mjs";
|
|
4
|
-
import { TelegramProvider } from "../providers/telegram.mjs";
|
|
5
3
|
import {
|
|
6
4
|
readSection,
|
|
7
5
|
updateSection,
|
|
8
6
|
CONFIG_PATH as MIXDOG_CONFIG_PATH,
|
|
9
|
-
SECRET_ACCOUNTS,
|
|
10
|
-
getDiscordToken,
|
|
11
|
-
getTelegramToken,
|
|
12
|
-
diagnoseDiscordTokenValue,
|
|
13
|
-
invalidateSecretReadCache,
|
|
14
7
|
} from "../../shared/config.mjs";
|
|
15
8
|
import { listSchedules } from "../../shared/schedules-db.mjs";
|
|
16
9
|
import { resolvePluginData } from "../../shared/plugin-paths.mjs";
|
|
@@ -22,9 +15,6 @@ const DEFAULT_ACCESS = {
|
|
|
22
15
|
channels: {}
|
|
23
16
|
};
|
|
24
17
|
const DEFAULT_CONFIG = {
|
|
25
|
-
provider: "discord",
|
|
26
|
-
discord: { token: "" },
|
|
27
|
-
telegram: { token: "" },
|
|
28
18
|
access: DEFAULT_ACCESS,
|
|
29
19
|
channel: {}
|
|
30
20
|
};
|
|
@@ -40,27 +30,8 @@ function applyDefaults(config) {
|
|
|
40
30
|
return out;
|
|
41
31
|
}
|
|
42
32
|
|
|
43
|
-
function
|
|
44
|
-
if (provider === "telegram") {
|
|
45
|
-
return String(entry?.telegramChatId || "");
|
|
46
|
-
}
|
|
47
|
-
return String(entry?.discordChannelId || "");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Resolve the single active-provider channel id from the config's `channel`
|
|
51
|
-
// section. Disk config carries a single `channel` object only.
|
|
52
|
-
function resolveChannelId(raw = {}, provider = "discord") {
|
|
53
|
-
const channel = raw.channel && typeof raw.channel === "object" ? raw.channel : null;
|
|
54
|
-
if (channel) return channelIdForProvider(channel, provider);
|
|
55
|
-
return "";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function loadConfig({ freshSecrets = false } = {}) {
|
|
33
|
+
async function loadConfig() {
|
|
59
34
|
try {
|
|
60
|
-
if (freshSecrets) {
|
|
61
|
-
invalidateSecretReadCache(SECRET_ACCOUNTS.discordToken);
|
|
62
|
-
invalidateSecretReadCache(SECRET_ACCOUNTS.telegramToken);
|
|
63
|
-
}
|
|
64
35
|
let raw = readSection("channels");
|
|
65
36
|
raw = raw && typeof raw === "object" ? raw : {};
|
|
66
37
|
// Schedules are the PG `scheduler.schedules` table (single source of
|
|
@@ -83,27 +54,11 @@ async function loadConfig({ freshSecrets = false } = {}) {
|
|
|
83
54
|
const v = readSection("voice");
|
|
84
55
|
if (v && typeof v === "object") voice = v;
|
|
85
56
|
} catch { /* missing section is fine */ }
|
|
86
|
-
|
|
87
|
-
const discordTokenProblem = diagnoseDiscordTokenValue(discordToken, raw);
|
|
88
|
-
if (discordTokenProblem) {
|
|
89
|
-
process.stderr.write(`mixdog: discord token ignored: ${discordTokenProblem}\n`);
|
|
90
|
-
}
|
|
91
|
-
// Single-provider select: config.provider picks ONE of discord|telegram.
|
|
92
|
-
// Anything else falls back to the discord default.
|
|
93
|
-
const provider = raw.provider === "telegram" ? "telegram" : "discord";
|
|
94
|
-
const telegramToken = getTelegramToken();
|
|
95
|
-
const channelId = resolveChannelId(raw, provider);
|
|
96
|
-
// The runtime reads only the resolved `channelId`; disk carries a single
|
|
97
|
-
// `channel` object, so spread `raw` directly.
|
|
57
|
+
// Messaging is retired: no provider select, no tokens, no channel id.
|
|
98
58
|
return applyDefaults({
|
|
99
59
|
...DEFAULT_CONFIG,
|
|
100
60
|
...raw,
|
|
101
|
-
|
|
102
|
-
channelId,
|
|
103
|
-
discord: { ...DEFAULT_CONFIG.discord, ...(({ token: _, ...rest }) => rest)(raw.discord || {}), ...(discordToken && !discordTokenProblem ? { token: discordToken } : {}) },
|
|
104
|
-
// Merge the keychain-resolved telegram token (harmless when provider is
|
|
105
|
-
// discord; the secret never lands in the on-disk config either way).
|
|
106
|
-
telegram: { ...DEFAULT_CONFIG.telegram, ...(({ token: _t, ...rest }) => rest)(raw.telegram || {}), ...(telegramToken ? { token: telegramToken } : {}) },
|
|
61
|
+
channelId: "",
|
|
107
62
|
access: {
|
|
108
63
|
...DEFAULT_ACCESS,
|
|
109
64
|
...(raw.access || {}),
|
|
@@ -116,9 +71,7 @@ async function loadConfig({ freshSecrets = false } = {}) {
|
|
|
116
71
|
mkdirSync(DATA_DIR, { recursive: true });
|
|
117
72
|
updateSection("channels", () => DEFAULT_CONFIG);
|
|
118
73
|
process.stderr.write(
|
|
119
|
-
`mixdog: default channels config created in ${MIXDOG_CONFIG_PATH}
|
|
120
|
-
edit the active provider channel id to connect.
|
|
121
|
-
`
|
|
74
|
+
`mixdog: default channels config created in ${MIXDOG_CONFIG_PATH}\n`
|
|
122
75
|
);
|
|
123
76
|
return applyDefaults(DEFAULT_CONFIG);
|
|
124
77
|
}
|
|
@@ -154,56 +107,18 @@ const HEADLESS_PROVIDER = {
|
|
|
154
107
|
return Buffer.alloc(0);
|
|
155
108
|
},
|
|
156
109
|
on() {
|
|
110
|
+
},
|
|
111
|
+
startTyping() {
|
|
112
|
+
},
|
|
113
|
+
stopTyping() {
|
|
157
114
|
}
|
|
158
115
|
};
|
|
159
|
-
function createProvider(
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return HEADLESS_PROVIDER;
|
|
166
|
-
}
|
|
167
|
-
// Single-provider select: exactly one provider is constructed based on
|
|
168
|
-
// config.provider (discord|telegram). The two are mutually exclusive.
|
|
169
|
-
if (config.provider === "telegram") {
|
|
170
|
-
const telegramToken = getTelegramToken();
|
|
171
|
-
if (!telegramToken) {
|
|
172
|
-
process.stderr.write("mixdog: telegram bot not configured; channel runtime running in headless mode\n");
|
|
173
|
-
return HEADLESS_PROVIDER;
|
|
174
|
-
}
|
|
175
|
-
const tgStateDir = config.telegram?.stateDir ?? join(DATA_DIR, "telegram");
|
|
176
|
-
mkdirSync(tgStateDir, { recursive: true });
|
|
177
|
-
return new TelegramProvider({
|
|
178
|
-
...config.telegram,
|
|
179
|
-
configPath: CONFIG_FILE,
|
|
180
|
-
access: config.access,
|
|
181
|
-
// Single-source channel setup: the main chat is auto-allowed inside
|
|
182
|
-
// TelegramProvider.loadAccess() so the configured Telegram target is
|
|
183
|
-
// enough for both inbound gating and outbound.
|
|
184
|
-
mainChannelId: config.channelId
|
|
185
|
-
}, tgStateDir);
|
|
186
|
-
}
|
|
187
|
-
const discordToken = getDiscordToken();
|
|
188
|
-
const discordTokenProblem = diagnoseDiscordTokenValue(discordToken, config);
|
|
189
|
-
if (discordTokenProblem) {
|
|
190
|
-
process.stderr.write(`mixdog: discord token ignored: ${discordTokenProblem}\n`);
|
|
191
|
-
}
|
|
192
|
-
if (config.provider !== "discord" || !discordToken || discordTokenProblem) {
|
|
193
|
-
process.stderr.write("mixdog: discord bot not configured; channel runtime running in headless mode\n");
|
|
194
|
-
return HEADLESS_PROVIDER;
|
|
195
|
-
}
|
|
196
|
-
const stateDir = config.discord.stateDir ?? join(DATA_DIR, "discord");
|
|
197
|
-
mkdirSync(stateDir, { recursive: true });
|
|
198
|
-
return new DiscordProvider({
|
|
199
|
-
...config.discord,
|
|
200
|
-
configPath: CONFIG_FILE,
|
|
201
|
-
access: config.access,
|
|
202
|
-
// Single-source channel setup: the main channel is auto-allowed inside
|
|
203
|
-
// DiscordProvider.loadAccess() so the configured Discord target is enough
|
|
204
|
-
// for both inbound and outbound — no separate access.channels entry.
|
|
205
|
-
mainChannelId: config.channelId
|
|
206
|
-
}, stateDir);
|
|
116
|
+
function createProvider() {
|
|
117
|
+
// Discord/Telegram messaging is retired (user decision: the PWA replaces
|
|
118
|
+
// it). The channel runtime always runs headless: automation
|
|
119
|
+
// (scheduler/webhooks) and voice transcription keep the worker alive while
|
|
120
|
+
// every messaging call is a no-op.
|
|
121
|
+
return HEADLESS_PROVIDER;
|
|
207
122
|
}
|
|
208
123
|
const PROFILE_FILE = join(DATA_DIR, "profile.json");
|
|
209
124
|
function loadProfileConfig() {
|
|
@@ -216,8 +131,6 @@ function loadProfileConfig() {
|
|
|
216
131
|
export {
|
|
217
132
|
DATA_DIR,
|
|
218
133
|
createProvider,
|
|
219
|
-
getDiscordToken,
|
|
220
|
-
getTelegramToken,
|
|
221
134
|
loadConfig,
|
|
222
135
|
loadProfileConfig
|
|
223
136
|
};
|
|
@@ -2,11 +2,8 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { DATA_DIR } from "./config.mjs";
|
|
4
4
|
|
|
5
|
-
// Drop-trace instrumentation for channels/index.mjs
|
|
6
|
-
//
|
|
7
|
-
// (which the output-forwarder uses): this instance is owned by index.mjs and
|
|
8
|
-
// keeps its own buffered writer + `preview` helper so the two trace streams
|
|
9
|
-
// stay byte-identical to the pre-split behavior.
|
|
5
|
+
// Drop-trace instrumentation for channels/index.mjs: buffered writer +
|
|
6
|
+
// `preview` helper owned by index.mjs.
|
|
10
7
|
const _dropTraceLog = path.join(DATA_DIR, "drop-trace.log");
|
|
11
8
|
const DROP_TRACE_ENABLED =
|
|
12
9
|
process.env.MIXDOG_DROP_TRACE === "1" ||
|
|
@@ -31,24 +31,17 @@ export function createOwnedRuntime({
|
|
|
31
31
|
getChannelBridgeActive,
|
|
32
32
|
instanceId,
|
|
33
33
|
TERMINAL_LEAD_PID,
|
|
34
|
-
forwarder,
|
|
35
34
|
sendNotifyToParent,
|
|
36
35
|
scheduler,
|
|
37
36
|
statusState,
|
|
38
37
|
logOwnership,
|
|
39
38
|
currentOwnerState,
|
|
40
|
-
bindPersistedTranscriptIfAny,
|
|
41
|
-
cancelPendingTranscriptRearm,
|
|
42
|
-
schedulePendingTranscriptRearm,
|
|
43
|
-
stopServerTyping,
|
|
44
39
|
wireWebhookHandlers,
|
|
45
40
|
wireEventQueueHandlers,
|
|
46
|
-
memoryDrainBuffer,
|
|
47
41
|
}) {
|
|
48
42
|
let bridgeRuntimeStarting = false;
|
|
49
43
|
let _ownedRuntimeStopRequested = false;
|
|
50
44
|
let bridgeOwnershipRefreshInFlight = null;
|
|
51
|
-
let _memoryDrainTimer = null;
|
|
52
45
|
// Automation (scheduler + webhook server + relay tunnel) can outlive a
|
|
53
46
|
// failed/absent messaging provider: it is tracked separately so teardown and
|
|
54
47
|
// reload know it is running even while bridgeRuntimeConnected stays false.
|
|
@@ -174,27 +167,16 @@ let _ownedRuntimeSelfHealing = false;
|
|
|
174
167
|
// reconnect window can reset a healthy reconnect loop. Keep this for explicit
|
|
175
168
|
// activation/reload recovery only.
|
|
176
169
|
async function selfHealOwnedRuntime(options = {}) {
|
|
177
|
-
const shouldRestoreBinding = options.restoreBinding === true;
|
|
178
170
|
const shouldResetProvider = options.resetProvider === true;
|
|
179
|
-
if (!
|
|
171
|
+
if (!shouldResetProvider) return;
|
|
180
172
|
if (_ownedRuntimeSelfHealing) return;
|
|
181
173
|
_ownedRuntimeSelfHealing = true;
|
|
182
174
|
try {
|
|
183
|
-
if (
|
|
184
|
-
await bindPersistedTranscriptIfAny().catch((e) => {
|
|
185
|
-
process.stderr.write(`mixdog: self-heal bindPersistedTranscriptIfAny failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
if (shouldResetProvider && typeof getProvider()?._resetClient === "function") {
|
|
175
|
+
if (typeof getProvider()?._resetClient === "function") {
|
|
189
176
|
await getProvider()._resetClient().catch((e) => {
|
|
190
177
|
process.stderr.write(`mixdog: self-heal getProvider() reset failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
191
178
|
});
|
|
192
179
|
}
|
|
193
|
-
// Do NOT nudge forwardNewText() here. bindPersistedTranscriptIfAny() above
|
|
194
|
-
// already forwards the same-session catch-up from the persisted cursor; an
|
|
195
|
-
// extra drain risks surfacing the old tail of a freshly (re)bound transcript
|
|
196
|
-
// on connect/change/rebind. Only outputs created after the new binding
|
|
197
|
-
// should be forwarded, and the normal watch/poll path handles those.
|
|
198
180
|
} finally {
|
|
199
181
|
_ownedRuntimeSelfHealing = false;
|
|
200
182
|
}
|
|
@@ -237,13 +219,6 @@ async function startOwnedRuntime(options = {}) {
|
|
|
237
219
|
process.stderr.write(`mixdog: pre-connect metadata advert aborted (${e instanceof Error ? e.message : String(e)})\n`);
|
|
238
220
|
return;
|
|
239
221
|
}
|
|
240
|
-
// Periodic buffer drain: replays memory-buffer/entry-*/ingest-*.json once the
|
|
241
|
-
// daemon-hosted memory runtime is ready. Idempotent + reentrancy-guarded inside
|
|
242
|
-
// drainBuffer(); unref'd so it never holds the worker open.
|
|
243
|
-
if (!_memoryDrainTimer) {
|
|
244
|
-
_memoryDrainTimer = setInterval(() => { void memoryDrainBuffer().catch(() => {}); }, 5e3);
|
|
245
|
-
_memoryDrainTimer.unref?.();
|
|
246
|
-
}
|
|
247
222
|
// Re-check after each post-connect await so a stopOwnedRuntime() landing
|
|
248
223
|
// mid-start cannot be overridden by the resuming start (scheduler/snapshot/
|
|
249
224
|
// webhook/binding launches below would revive owner state after stop).
|
|
@@ -260,12 +235,6 @@ async function startOwnedRuntime(options = {}) {
|
|
|
260
235
|
_ownedRuntimeStopRequested = false;
|
|
261
236
|
return true;
|
|
262
237
|
};
|
|
263
|
-
const restoreBinding = options.restoreBinding !== false;
|
|
264
|
-
const bindPersistedTranscriptTask = restoreBinding
|
|
265
|
-
? bindPersistedTranscriptIfAny().catch((e) => {
|
|
266
|
-
process.stderr.write(`mixdog: bindPersistedTranscriptIfAny failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
267
|
-
})
|
|
268
|
-
: null;
|
|
269
238
|
// Await getProvider().connect() so callers (and bindingReady) only resolve after
|
|
270
239
|
// the Discord binding is real. Previously this was fire-and-forget and
|
|
271
240
|
// refreshBridgeOwnership returned immediately, letting bindingReady fire
|
|
@@ -273,9 +242,6 @@ async function startOwnedRuntime(options = {}) {
|
|
|
273
242
|
try {
|
|
274
243
|
await startingProvider.connect();
|
|
275
244
|
if (await bailIfStopRequested()) {
|
|
276
|
-
cancelPendingTranscriptRearm();
|
|
277
|
-
try { forwarder.stopWatch(); } catch {}
|
|
278
|
-
if (bindPersistedTranscriptTask) await bindPersistedTranscriptTask;
|
|
279
245
|
return;
|
|
280
246
|
}
|
|
281
247
|
// Advertise provider readiness (metadata advert).
|
|
@@ -301,9 +267,6 @@ async function startOwnedRuntime(options = {}) {
|
|
|
301
267
|
process.stderr.write(`mixdog: initProviders failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
302
268
|
}
|
|
303
269
|
if (await bailIfStopRequested()) {
|
|
304
|
-
cancelPendingTranscriptRearm();
|
|
305
|
-
try { forwarder.stopWatch(); } catch {}
|
|
306
|
-
if (bindPersistedTranscriptTask) await bindPersistedTranscriptTask;
|
|
307
270
|
return;
|
|
308
271
|
}
|
|
309
272
|
// Reconnect after a degraded (automation-only) start must not double-arm
|
|
@@ -314,26 +277,10 @@ async function startOwnedRuntime(options = {}) {
|
|
|
314
277
|
}
|
|
315
278
|
syncOwnedWebhookAndEventRuntime();
|
|
316
279
|
automationRunning = true;
|
|
317
|
-
if (restoreBinding) {
|
|
318
|
-
if (bindPersistedTranscriptTask) await bindPersistedTranscriptTask;
|
|
319
|
-
const pendingTranscriptPath = forwarder.transcriptPath;
|
|
320
|
-
if (pendingTranscriptPath && !fs.existsSync(pendingTranscriptPath)) {
|
|
321
|
-
// Pre-connect bind may have armed rearm while !getBridgeRuntimeConnected();
|
|
322
|
-
// the first tick then exits without rescheduling. Re-arm now that we own.
|
|
323
|
-
schedulePendingTranscriptRearm(statusState.read().channelId, pendingTranscriptPath);
|
|
324
|
-
} else {
|
|
325
|
-
void forwarder.forwardNewText().catch((err) => {
|
|
326
|
-
process.stderr.write(`mixdog: post-connect forwardNewText failed (non-fatal): ${err instanceof Error ? err.message : String(err)}\n`);
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
280
|
process.stderr.write(`mixdog: running with ${getProvider().name} getProvider()\n`);
|
|
331
281
|
logOwnership(`active owner lead=${TERMINAL_LEAD_PID} pid=${process.pid}`);
|
|
332
282
|
} catch (e) {
|
|
333
283
|
process.stderr.write(`mixdog: getProvider() connect failed (non-fatal, cycle1/MCP still up): ${e instanceof Error ? e.message : String(e)}\n`);
|
|
334
|
-
cancelPendingTranscriptRearm();
|
|
335
|
-
try { forwarder.stopWatch(); } catch {}
|
|
336
|
-
if (bindPersistedTranscriptTask) await bindPersistedTranscriptTask;
|
|
337
284
|
// Roll back partial owner-side state advertised before connect() ran:
|
|
338
285
|
// disconnect the provider WE started (a post-connect startup step may have
|
|
339
286
|
// thrown while the gateway is live), then release the channel locks + clear
|
|
@@ -341,7 +288,6 @@ async function startOwnedRuntime(options = {}) {
|
|
|
341
288
|
try { await startingProvider.disconnect(); } catch {}
|
|
342
289
|
try { releaseOwnedChannelLocks(instanceId); } catch {}
|
|
343
290
|
try { clearActiveInstance(instanceId); } catch {}
|
|
344
|
-
if (_memoryDrainTimer) { clearInterval(_memoryDrainTimer); _memoryDrainTimer = null; }
|
|
345
291
|
// DEGRADED MODE (automation decoupling): a messaging connect failure —
|
|
346
292
|
// packaged runtime without discord.js, bad token, gateway outage — must
|
|
347
293
|
// not take schedules/webhooks down with it. Start the automation runtime
|
|
@@ -364,11 +310,6 @@ async function startOwnedRuntime(options = {}) {
|
|
|
364
310
|
}
|
|
365
311
|
}
|
|
366
312
|
async function stopOwnedRuntime(reason) {
|
|
367
|
-
// Cancel any pending transcript re-arm poll BEFORE the connected/starting
|
|
368
|
-
// early-return below. Otherwise a poll armed against a not-yet-existing
|
|
369
|
-
// transcript could fire after teardown and reinstall the fs.watch handle
|
|
370
|
-
// (startWatch is not owner-gated), leaking a live watcher past shutdown.
|
|
371
|
-
cancelPendingTranscriptRearm();
|
|
372
313
|
// startOwnedRuntime() advertises owner HTTP/heartbeat/active-instance and
|
|
373
314
|
// claims channel locks BEFORE awaiting getProvider().connect(). If shutdown lands
|
|
374
315
|
// during that window (bridgeRuntimeStarting=true, getBridgeRuntimeConnected()
|
|
@@ -384,13 +325,6 @@ async function stopOwnedRuntime(reason) {
|
|
|
384
325
|
try { await automationStartPromise; } catch {}
|
|
385
326
|
}
|
|
386
327
|
const wasConnected = getBridgeRuntimeConnected();
|
|
387
|
-
stopServerTyping();
|
|
388
|
-
// Release the transcript fs.watch handle plus the forwarder's debounce/retry
|
|
389
|
-
// timers on standby. Without this the watcher keeps firing scheduleWatchFlush
|
|
390
|
-
// and the drain/retry timers stay live after ownership is dropped, leaking a
|
|
391
|
-
// file handle + timers for the rest of the process lifetime.
|
|
392
|
-
try { forwarder.stopWatch(); } catch {}
|
|
393
|
-
if (_memoryDrainTimer) { clearInterval(_memoryDrainTimer); _memoryDrainTimer = null; }
|
|
394
328
|
scheduler.stop();
|
|
395
329
|
stopSnapshotWriter();
|
|
396
330
|
await stopWebhookAndEventRuntime();
|
|
@@ -499,19 +433,9 @@ async function reloadRuntimeConfig() {
|
|
|
499
433
|
// the restart and the NEW provider would never connect (lost-restart race).
|
|
500
434
|
if (_inFlightStart) { try { await _inFlightStart; } catch {} }
|
|
501
435
|
setProvider(nextProvider);
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
// would 400 "chat not found". There is no id mapping between platforms, so
|
|
505
|
-
// CLEAR the stale binding: drop the forwarder's context + watcher and wipe
|
|
506
|
-
// status.channelId/transcriptPath. The next inbound from the new getProvider()
|
|
507
|
-
// rebinds the correct chat via applyTranscriptBinding(). Only done on
|
|
508
|
-
// providerChanged — same-getProvider() reloads keep their binding untouched.
|
|
509
|
-
// (active-instance is cleared by stopOwnedRuntime on the restart path; we
|
|
510
|
-
// don't re-advertise here to avoid resurrecting a just-cleared entry.)
|
|
436
|
+
// Provider-type change wipes the persisted routing ids; messaging is
|
|
437
|
+
// retired, so only the status snapshot needs clearing.
|
|
511
438
|
if (providerTypeChanged) {
|
|
512
|
-
try { forwarder.stopWatch(); } catch {}
|
|
513
|
-
forwarder.channelId = "";
|
|
514
|
-
forwarder.transcriptPath = "";
|
|
515
439
|
try {
|
|
516
440
|
statusState.update((state) => {
|
|
517
441
|
state.channelId = "";
|
|
@@ -6,7 +6,17 @@ import { DATA_DIR } from "./config.mjs";
|
|
|
6
6
|
import { ensureNopluginDir } from "./executor.mjs";
|
|
7
7
|
import { withFileLockSync } from "../../shared/atomic-file.mjs";
|
|
8
8
|
import { resolveRuntimeRoot } from "../../shared/runtime-root.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
advanceScheduleCursor,
|
|
11
|
+
claimScheduleRun,
|
|
12
|
+
markFired,
|
|
13
|
+
markDone,
|
|
14
|
+
markScheduleFailure,
|
|
15
|
+
markScheduleSuccess,
|
|
16
|
+
setDeferred,
|
|
17
|
+
setNextFire,
|
|
18
|
+
setSkippedUntil,
|
|
19
|
+
} from "../../shared/schedules-db.mjs";
|
|
10
20
|
import { runScheduleSession } from "../../shared/schedule-session-run.mjs";
|
|
11
21
|
|
|
12
22
|
const SCHEDULE_LOG = join(DATA_DIR, "schedule.log");
|
|
@@ -107,6 +117,9 @@ class Scheduler {
|
|
|
107
117
|
this.interactive = interactive.filter((s) => s.enabled !== false);
|
|
108
118
|
this.channelId = channelId ?? "";
|
|
109
119
|
this.promptsDir = join(DATA_DIR, "prompts");
|
|
120
|
+
for (const s of [...this.nonInteractive, ...this.interactive]) {
|
|
121
|
+
if (s.lastFiredAt) this.lastFired.set(s.name, new Date(s.lastFiredAt).toISOString());
|
|
122
|
+
}
|
|
110
123
|
this.refreshSkipCache();
|
|
111
124
|
}
|
|
112
125
|
setInjectHandler(fn) {
|
|
@@ -341,11 +354,18 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
341
354
|
continue;
|
|
342
355
|
}
|
|
343
356
|
try {
|
|
344
|
-
const task = cron.schedule(s.whenCron, () => this.onCronFire(s, type), {
|
|
357
|
+
const task = cron.schedule(s.whenCron, (ctx) => this.onCronFire(s, type, ctx?.date), {
|
|
345
358
|
timezone: s.timezone || undefined,
|
|
346
359
|
name: s.name,
|
|
347
360
|
});
|
|
348
361
|
this.cronJobs.set(s.name, task);
|
|
362
|
+
const next = typeof task.getNextRun === "function" ? task.getNextRun() : null;
|
|
363
|
+
if (next) {
|
|
364
|
+
s.nextFireAt = next.toISOString();
|
|
365
|
+
void setNextFire(s.name, next).catch((err) => {
|
|
366
|
+
process.stderr.write(`mixdog scheduler: ${s.name} setNextFire failed: ${err}\n`);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
349
369
|
logSchedule(`registered cron "${s.name}" = "${s.whenCron}"${s.timezone ? ` tz=${s.timezone}` : ""}\n`);
|
|
350
370
|
} catch (err) {
|
|
351
371
|
process.stderr.write(`mixdog scheduler: failed to register cron "${s.name}" (${s.whenCron}): ${err}\n`);
|
|
@@ -357,20 +377,44 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
357
377
|
}
|
|
358
378
|
/** Fire path for a cron-triggered entry. node-cron's dow field covers the
|
|
359
379
|
* day guard, so there is no separate days filter. Persists last_fired_at. */
|
|
360
|
-
async onCronFire(schedule, type) {
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
try { await
|
|
370
|
-
catch (err) { process.stderr.write(`mixdog scheduler: ${schedule.name}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
380
|
+
async onCronFire(schedule, type, scheduledDate = null) {
|
|
381
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
382
|
+
const scheduledAt = scheduledDate instanceof Date && Number.isFinite(scheduledDate.getTime())
|
|
383
|
+
? scheduledDate
|
|
384
|
+
: startedAt;
|
|
385
|
+
const task = this.cronJobs.get(schedule.name);
|
|
386
|
+
const nextFireAt = typeof task?.getNextRun === "function" ? task.getNextRun() : null;
|
|
387
|
+
schedule.nextFireAt = nextFireAt?.toISOString?.() ?? null;
|
|
388
|
+
if (this.shouldSkip(schedule.name)) {
|
|
389
|
+
try { await advanceScheduleCursor(schedule.name, scheduledAt, nextFireAt); }
|
|
390
|
+
catch (err) { process.stderr.write(`mixdog scheduler: ${schedule.name} cursor advance failed: ${err}\n`); }
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const claimed = await claimScheduleRun(schedule.name, scheduledAt, startedAt, nextFireAt);
|
|
395
|
+
if (!claimed) {
|
|
396
|
+
logSchedule(`${schedule.name}: duplicate/stale cron slot skipped\n`);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
schedule.lastScheduledAt = scheduledAt.toISOString();
|
|
400
|
+
schedule.lastStartedAt = startedAt.toISOString();
|
|
401
|
+
schedule.lastFiredAt = startedAt.toISOString();
|
|
402
|
+
this.lastFired.set(schedule.name, startedAt.toISOString());
|
|
403
|
+
const fired = await this.fireTimed(schedule, type, { awaitDispatch: true });
|
|
404
|
+
if (!fired) {
|
|
405
|
+
schedule.lastFailedAt = new Date().toISOString();
|
|
406
|
+
await markScheduleFailure(schedule.name, new Date());
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const completedAt = new Date();
|
|
410
|
+
schedule.lastSuccessAt = completedAt.toISOString();
|
|
411
|
+
await markScheduleSuccess(schedule.name, completedAt);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
schedule.lastFailedAt = new Date().toISOString();
|
|
414
|
+
try { await markScheduleFailure(schedule.name, new Date()); } catch {}
|
|
415
|
+
process.stderr.write(`mixdog scheduler: ${schedule.name} failed: ${err}\n`);
|
|
416
|
+
this.notifyFailure(schedule, `run failed: ${err?.message || err}`);
|
|
417
|
+
}
|
|
374
418
|
}
|
|
375
419
|
stop() {
|
|
376
420
|
if (this.tickTimer) {
|
|
@@ -419,6 +463,8 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
419
463
|
}
|
|
420
464
|
const timer = setTimeout(() => this.fireOneShot(schedule, type), Math.max(0, delay));
|
|
421
465
|
this.oneShotTimers.set(schedule.name, timer);
|
|
466
|
+
schedule.nextFireAt = new Date(fireAt).toISOString();
|
|
467
|
+
void setNextFire(schedule.name, new Date(fireAt)).catch(() => {});
|
|
422
468
|
logSchedule(`armed one-shot "${schedule.name}" at ${new Date(fireAt).toISOString()}${delay <= 0 ? " (misfire → immediate)" : ""}\n`);
|
|
423
469
|
}
|
|
424
470
|
/** markDone with bounded retry. A one-shot that fired but failed to persist
|
|
@@ -462,6 +508,8 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
462
508
|
logSchedule(`one-shot "${schedule.name}" deferred/skipped — re-arming for ${new Date(target).toISOString()}\n`);
|
|
463
509
|
const timer = setTimeout(() => this.fireOneShot(schedule, type), delay);
|
|
464
510
|
this.oneShotTimers.set(schedule.name, timer);
|
|
511
|
+
schedule.nextFireAt = new Date(target).toISOString();
|
|
512
|
+
void setNextFire(schedule.name, new Date(target)).catch(() => {});
|
|
465
513
|
return;
|
|
466
514
|
}
|
|
467
515
|
// Mark done ONLY after a successful fire. A false/throwing fireTimed
|
|
@@ -474,6 +522,8 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
474
522
|
// rejects. Only a resolved dispatch counts as a real fire here.
|
|
475
523
|
const fired = await this.fireTimed(schedule, type, { awaitDispatch: true });
|
|
476
524
|
if (!fired) {
|
|
525
|
+
schedule.lastFailedAt = new Date().toISOString();
|
|
526
|
+
try { await markScheduleFailure(schedule.name, new Date()); } catch {}
|
|
477
527
|
logSchedule(`one-shot "${schedule.name}" did not fire (skipped/guarded) — leaving pending for retry\n`);
|
|
478
528
|
return;
|
|
479
529
|
}
|
|
@@ -481,8 +531,12 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
481
531
|
schedule.lastFiredAt = now.toISOString();
|
|
482
532
|
try { await markFired(schedule.name, now); }
|
|
483
533
|
catch (err) { process.stderr.write(`mixdog scheduler: ${schedule.name} markFired failed: ${err}\n`); }
|
|
534
|
+
schedule.lastSuccessAt = new Date().toISOString();
|
|
535
|
+
try { await markScheduleSuccess(schedule.name, new Date()); } catch {}
|
|
484
536
|
await this.markDoneWithRetry(schedule.name);
|
|
485
537
|
} catch (err) {
|
|
538
|
+
schedule.lastFailedAt = new Date().toISOString();
|
|
539
|
+
try { await markScheduleFailure(schedule.name, new Date()); } catch {}
|
|
486
540
|
process.stderr.write(`mixdog scheduler: ${schedule.name} one-shot failed: ${err} — leaving pending for retry\n`);
|
|
487
541
|
this.notifyFailure(schedule, `one-shot failed: ${err?.message || err} — left pending for retry`);
|
|
488
542
|
}
|
|
@@ -526,8 +580,11 @@ ${Scheduler.INSTANCE_UUID}`;
|
|
|
526
580
|
name: s.name,
|
|
527
581
|
time: s.whenCron ?? s.whenAt ?? null,
|
|
528
582
|
type,
|
|
529
|
-
running:
|
|
530
|
-
lastFired: this.lastFired.get(s.name) ?? null,
|
|
583
|
+
running: this.running.has(s.name),
|
|
584
|
+
lastFired: this.lastFired.get(s.name) ?? s.lastFiredAt ?? null,
|
|
585
|
+
lastSuccess: s.lastSuccessAt ?? null,
|
|
586
|
+
lastFailed: s.lastFailedAt ?? null,
|
|
587
|
+
nextFire: s.nextFireAt ?? null,
|
|
531
588
|
}));
|
|
532
589
|
}
|
|
533
590
|
async triggerManual(name) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Writes <DATA_DIR>/channels/status-snapshot.json every 10 seconds so that
|
|
5
5
|
* setup-server can read cross-process state (cron next-fire, deferred count,
|
|
6
|
-
*
|
|
6
|
+
* relay hook URL) without IPC.
|
|
7
7
|
*
|
|
8
8
|
* Atomic write: tmp → rename so readers never see a partial file.
|
|
9
9
|
*
|
|
@@ -31,60 +31,12 @@ function stableSnapshotJson(snapshot) {
|
|
|
31
31
|
return JSON.stringify(stable, null, 2);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// ── In-memory Discord unread tracking ────────────────────────────────────────
|
|
35
|
-
// Key: channelId Value: { label, latestSeenId, unseenCount }
|
|
36
|
-
// No persistence across restarts — clean start is fine for v1.
|
|
37
|
-
const _discordUnread = new Map();
|
|
38
|
-
const DISCORD_UNREAD_MAX_CHANNELS = 500;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Called whenever the provider observes messages for a channelId.
|
|
42
|
-
* `messages` is the array returned by provider.fetchMessages().
|
|
43
|
-
* We record the most-recently-seen message id and count messages
|
|
44
|
-
* received since the last call as "unread since last fetch".
|
|
45
|
-
* Pass { markRead: true } for live gateway messages that were already
|
|
46
|
-
* delivered into the bridge.
|
|
47
|
-
*/
|
|
48
|
-
export function recordFetchedMessages(channelId, channelLabel, messages, options = {}) {
|
|
49
|
-
if (!Array.isArray(messages) || messages.length === 0) return;
|
|
50
|
-
const prev = _discordUnread.get(channelId);
|
|
51
|
-
const prevLatestId = prev?.latestSeenId ?? null;
|
|
52
|
-
const newLatestId = messages[messages.length - 1]?.id ?? null;
|
|
53
|
-
|
|
54
|
-
// Count messages newer than the last seen id (BigInt-compare Discord snowflakes).
|
|
55
|
-
let unseenCount = 0;
|
|
56
|
-
if (options.markRead === true) {
|
|
57
|
-
unseenCount = 0;
|
|
58
|
-
} else if (prevLatestId) {
|
|
59
|
-
for (const m of messages) {
|
|
60
|
-
try {
|
|
61
|
-
if (BigInt(m.id) > BigInt(prevLatestId)) unseenCount++;
|
|
62
|
-
} catch { unseenCount++; }
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
// First call: zero unread (baseline, not retroactive).
|
|
66
|
-
|
|
67
|
-
// Retain the most recently observed channels only. Channel ids originate from
|
|
68
|
-
// inbound traffic and ad-hoc fetches, so an unbounded process-wide registry
|
|
69
|
-
// would otherwise grow for the worker lifetime.
|
|
70
|
-
if (_discordUnread.has(channelId)) {
|
|
71
|
-
_discordUnread.delete(channelId);
|
|
72
|
-
} else if (_discordUnread.size >= DISCORD_UNREAD_MAX_CHANNELS) {
|
|
73
|
-
_discordUnread.delete(_discordUnread.keys().next().value);
|
|
74
|
-
}
|
|
75
|
-
_discordUnread.set(channelId, {
|
|
76
|
-
label: channelLabel ?? channelId,
|
|
77
|
-
latestSeenId: newLatestId,
|
|
78
|
-
unseenCount,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
34
|
// ── Snapshot computation ─────────────────────────────────────────────────────
|
|
83
35
|
// The legacy HH:MM / everyNm / hourly next-fire fallback was removed: the
|
|
84
36
|
// scheduler accepts cron expressions exclusively (scheduler.mjs:68), so the
|
|
85
37
|
// fallback could only produce stale next-fire timestamps for entries that
|
|
86
38
|
// never actually fire under the cron-only scheduler.
|
|
87
|
-
async function computeSnapshot(scheduler) {
|
|
39
|
+
export async function computeSnapshot(scheduler) {
|
|
88
40
|
const now = Date.now();
|
|
89
41
|
|
|
90
42
|
// ── Schedules ──────────────────────────────────────────────────────────────
|
|
@@ -97,9 +49,10 @@ async function computeSnapshot(scheduler) {
|
|
|
97
49
|
for (const [name, task] of scheduler.cronJobs) {
|
|
98
50
|
if (scheduler.shouldSkip && scheduler.shouldSkip(name)) continue;
|
|
99
51
|
try {
|
|
100
|
-
// node-cron
|
|
101
|
-
//
|
|
52
|
+
// node-cron v4 exposes getNextRun(); retain older aliases for
|
|
53
|
+
// compatibility with persisted installations on an earlier runtime.
|
|
102
54
|
const nd =
|
|
55
|
+
(typeof task.getNextRun === 'function' ? task.getNextRun() : null) ??
|
|
103
56
|
(typeof task.nextDate === 'function' ? task.nextDate() : null) ??
|
|
104
57
|
(typeof task.getNextDate === 'function' ? task.getNextDate() : null);
|
|
105
58
|
if (!nd) continue;
|
|
@@ -136,18 +89,6 @@ async function computeSnapshot(scheduler) {
|
|
|
136
89
|
}
|
|
137
90
|
}
|
|
138
91
|
|
|
139
|
-
// ── Discord unread ─────────────────────────────────────────────────────────
|
|
140
|
-
const unreadList = [];
|
|
141
|
-
let totalUnread = 0;
|
|
142
|
-
for (const [channelId, entry] of _discordUnread) {
|
|
143
|
-
unreadList.push({
|
|
144
|
-
channelId,
|
|
145
|
-
channelLabel: entry.label,
|
|
146
|
-
count: entry.unseenCount,
|
|
147
|
-
});
|
|
148
|
-
totalUnread += entry.unseenCount;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
92
|
// ── Relay hook URL (identity file read; assigned on first tunnel start) ────
|
|
152
93
|
const hookPublicUrl = readHookPublicBase();
|
|
153
94
|
|
|
@@ -160,10 +101,6 @@ async function computeSnapshot(scheduler) {
|
|
|
160
101
|
deferred,
|
|
161
102
|
deferredCount: deferred.length,
|
|
162
103
|
},
|
|
163
|
-
discord: {
|
|
164
|
-
unread: unreadList,
|
|
165
|
-
totalUnread,
|
|
166
|
-
},
|
|
167
104
|
hook: {
|
|
168
105
|
publicUrl: hookPublicUrl,
|
|
169
106
|
},
|