talon-agent 5.18.2 → 5.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/package.json +1 -1
- package/prompts/system/agent-brief.md +20 -3
- package/src/app.ts +13 -0
- package/src/backend/claude-sdk/handler.ts +4 -4
- package/src/backend/claude-sdk/mcp-ready.ts +16 -2
- package/src/backend/claude-sdk/stream.ts +2 -2
- package/src/backend/codex/auth.ts +1 -1
- package/src/backend/codex/handler/message.ts +11 -11
- package/src/backend/codex/init.ts +4 -9
- package/src/backend/codex/mcp-config.ts +1 -2
- package/src/backend/codex/oauth-incompat.ts +8 -4
- package/src/backend/codex/one-shot.ts +1 -1
- package/src/backend/openai-agents/builtins.ts +55 -27
- package/src/backend/openai-agents/factory.ts +3 -3
- package/src/backend/openai-agents/handler/message.ts +3 -5
- package/src/backend/openai-agents/mcp-pool.ts +10 -27
- package/src/backend/remote-server/chat-turn.ts +6 -6
- package/src/backend/remote-server/events.ts +1 -5
- package/src/backend/remote-server/index.ts +0 -1
- package/src/backend/remote-server/messages.ts +3 -7
- package/src/backend/remote-server/one-shot.ts +1 -3
- package/src/backend/remote-server/session-helpers.ts +1 -4
- package/src/backend/remote-server/sse-stream.ts +8 -9
- package/src/backend/runtime/metrics.ts +7 -13
- package/src/backend/runtime/sleep.ts +1 -2
- package/src/backend/runtime/turn/handle-retry.ts +48 -2
- package/src/backend/runtime/turn/handler-to-events.ts +3 -4
- package/src/bootstrap.ts +9 -1
- package/src/cli/doctor.ts +3 -0
- package/src/cli/index.ts +8 -10
- package/src/cli/logs.ts +148 -9
- package/src/cli/setup.ts +9 -11
- package/src/cli/status.ts +29 -0
- package/src/core/agent-runtime/README.md +5 -19
- package/src/core/agent-runtime/events.ts +3 -39
- package/src/core/agent-runtime/model-ref.ts +0 -8
- package/src/core/agents/registry.ts +49 -2
- package/src/core/auth/expiry-monitor.ts +9 -1
- package/src/core/auth/login-flow.ts +9 -1
- package/src/core/auth/status.ts +31 -3
- package/src/core/background/cron/scheduler.ts +25 -5
- package/src/core/background/dream/index.ts +29 -10
- package/src/core/background/failure-backoff.ts +30 -0
- package/src/core/background/heartbeat/agent.ts +2 -27
- package/src/core/background/heartbeat/index.ts +0 -2
- package/src/core/background/heartbeat/scheduler.ts +21 -13
- package/src/core/background/heartbeat/state.ts +10 -2
- package/src/core/background/isolated-agent.ts +6 -2
- package/src/core/background/pulse/pulse.ts +9 -0
- package/src/core/background/triggers/exit.ts +54 -0
- package/src/core/background/triggers/index.ts +1 -3
- package/src/core/background/triggers/resume.ts +2 -4
- package/src/core/backup/archive/tar.ts +14 -4
- package/src/core/backup/restore.ts +21 -13
- package/src/core/backup/scheduler.ts +28 -9
- package/src/core/backup/snapshot.ts +43 -10
- package/src/core/backup/store.ts +7 -19
- package/src/core/backup/targets.ts +69 -17
- package/src/core/config/index.ts +14 -0
- package/src/core/daemon/crash-marker.ts +141 -0
- package/src/core/daemon/crash.ts +9 -2
- package/src/core/daemon/handoff.ts +15 -0
- package/src/core/daemon/health-alerts.ts +297 -0
- package/src/core/daemon/log-reader.ts +289 -0
- package/src/core/doctor/index.ts +18 -2
- package/src/core/doctor/logs.ts +124 -0
- package/src/core/doctor/types.ts +1 -1
- package/src/core/engine/backend-controller/index.ts +1 -13
- package/src/core/engine/backend-router/router.ts +1 -1
- package/src/core/engine/dispatcher.ts +55 -2
- package/src/core/engine/fault-text.ts +40 -0
- package/src/core/engine/gateway-actions/agents/index.ts +3 -2
- package/src/core/engine/gateway-actions/agents/report.ts +62 -0
- package/src/core/engine/gateway-actions/history.ts +2 -4
- package/src/core/engine/gateway-actions/native/exec.ts +13 -16
- package/src/core/engine/gateway.ts +60 -1
- package/src/core/engine/turn-health.ts +222 -0
- package/src/core/errors.ts +2 -2
- package/src/core/frontend-runtime/admin-notify.ts +1 -1
- package/src/core/frontend-runtime/alerts.ts +130 -0
- package/src/core/mcp-hub/children.ts +78 -29
- package/src/core/mcp-hub/index.ts +21 -18
- package/src/core/mcp-hub/proxy-server.ts +8 -4
- package/src/core/mcp-hub/talon-server.ts +5 -12
- package/src/core/mesh/credentials/store.ts +16 -1
- package/src/core/mesh/devices/service.ts +26 -15
- package/src/core/mesh/devices/teleport.ts +14 -2
- package/src/core/mesh/links/node-binaries.ts +13 -6
- package/src/core/mesh/persist.ts +22 -10
- package/src/core/mesh/transfers/device-files.ts +5 -23
- package/src/core/mesh/transfers/transfers.ts +16 -3
- package/src/core/models/active-model.ts +2 -55
- package/src/core/plugin/actions.ts +19 -20
- package/src/core/plugin/builtins.ts +80 -90
- package/src/core/plugin/index.ts +1 -4
- package/src/core/plugin/loader.ts +25 -33
- package/src/core/plugin/mcp.ts +3 -5
- package/src/core/plugin/registry.ts +19 -35
- package/src/core/plugin/types.ts +2 -5
- package/src/core/prompt/assemble.ts +15 -3
- package/src/core/scripts/lua.ts +6 -2
- package/src/core/tasks/table.ts +8 -2
- package/src/core/tools/bridge.ts +2 -4
- package/src/core/tools/chat/cross-send.ts +1 -1
- package/src/core/tools/chat/messaging.ts +1 -1
- package/src/core/tools/index.ts +2 -2
- package/src/core/tools/mcp-env.ts +2 -59
- package/src/core/tools/ops/agents.ts +22 -1
- package/src/core/tools/schemas.ts +4 -9
- package/src/core/vfs/fusefs.ts +0 -5
- package/src/core/vfs/index.ts +9 -2
- package/src/core/vfs/mounts/diagnostics.ts +109 -0
- package/src/core/vfs/mounts/proc.ts +17 -1
- package/src/core/vfs/workspace.ts +7 -3
- package/src/core/weaver/shuttle.ts +8 -1
- package/src/core/weaver/turn-log.ts +320 -0
- package/src/core/weaver/weaver.ts +48 -5
- package/src/frontend/discord/actions/index.ts +8 -1
- package/src/frontend/discord/diagnostics.ts +82 -6
- package/src/frontend/discord/handlers/index.ts +0 -2
- package/src/frontend/discord/middleware.ts +7 -13
- package/src/frontend/discord/runtime.ts +1 -3
- package/src/frontend/health/delivery.ts +115 -0
- package/src/frontend/health/outage.ts +116 -0
- package/src/frontend/native/bridge/routes/chats.ts +3 -5
- package/src/frontend/native/bridge/server.ts +106 -21
- package/src/frontend/native/index.ts +1 -1
- package/src/frontend/native/media/media.ts +5 -1
- package/src/frontend/native/runtime.ts +12 -7
- package/src/frontend/native/surface/handlers.ts +1 -1
- package/src/frontend/native/surface/memory.ts +1 -1
- package/src/frontend/native/surface/models.ts +3 -3
- package/src/frontend/native/surface/settings.ts +20 -8
- package/src/frontend/native/turn/context.ts +6 -8
- package/src/frontend/native/turn/turn-meta.ts +2 -5
- package/src/frontend/native/turn/turn.ts +8 -10
- package/src/frontend/presentation/format.ts +2 -4
- package/src/frontend/presentation/session-status.ts +2 -6
- package/src/frontend/teams/actions.ts +8 -1
- package/src/frontend/teams/graph.ts +0 -1
- package/src/frontend/teams/index.ts +1 -4
- package/src/frontend/teams/poll.ts +40 -2
- package/src/frontend/teams/runtime.ts +14 -5
- package/src/frontend/telegram/actions/index.ts +4 -1
- package/src/frontend/telegram/actions/send.ts +8 -0
- package/src/frontend/telegram/handlers/context.ts +13 -2
- package/src/frontend/telegram/handlers/delivery.ts +12 -9
- package/src/frontend/telegram/handlers/index.ts +0 -2
- package/src/frontend/telegram/index.ts +35 -9
- package/src/frontend/telegram/polling/poll-health.ts +110 -0
- package/src/frontend/telegram/userbot.ts +100 -36
- package/src/frontend/terminal/builtins/session.ts +2 -2
- package/src/frontend/terminal/index.ts +1 -3
- package/src/frontend/terminal/renderer.ts +2 -18
- package/src/frontend/whatsapp/actions/index.ts +12 -1
- package/src/frontend/whatsapp/actions/messaging.ts +7 -2
- package/src/frontend/whatsapp/connection/connection.ts +29 -9
- package/src/frontend/whatsapp/connection/health.ts +89 -0
- package/src/frontend/whatsapp/connection/identity.ts +4 -4
- package/src/frontend/whatsapp/runtime.ts +11 -5
- package/src/native/blake3.ts +28 -2
- package/src/native/fusefs.ts +23 -5
- package/src/native/registry.ts +1 -1
- package/src/native/warden.ts +33 -5
- package/src/plugins/github/index.ts +0 -1
- package/src/plugins/mempalace/index.ts +9 -3
- package/src/plugins/playwright/index.ts +2 -4
- package/src/plugins/playwright/provision.ts +12 -4
- package/src/storage/chat-settings.ts +6 -1
- package/src/storage/cron.ts +29 -4
- package/src/storage/daily-log.ts +43 -47
- package/src/storage/db.ts +61 -33
- package/src/storage/history.ts +6 -1
- package/src/storage/journal.ts +9 -2
- package/src/storage/kv.ts +19 -6
- package/src/storage/media-index.ts +28 -6
- package/src/storage/repositories/chat-settings-repo.ts +8 -2
- package/src/storage/repositories/sessions-repo.ts +10 -3
- package/src/storage/scripts.ts +24 -13
- package/src/storage/sessions.ts +11 -2
- package/src/storage/skills.ts +21 -2
- package/src/storage/stickers.ts +17 -3
- package/src/storage/triggers.ts +8 -3
- package/src/storage/turn-meta.ts +25 -7
- package/src/util/log.ts +189 -6
- package/src/util/logging/turn-scope.ts +85 -0
- package/src/util/time.ts +3 -3
- package/src/util/watchdog.ts +30 -0
- package/src/core/engine/backend-controller/legacy.ts +0 -111
|
@@ -17,6 +17,7 @@ import writeFileAtomic from "write-file-atomic";
|
|
|
17
17
|
import { log, logError, logWarn } from "../../util/log.js";
|
|
18
18
|
import { dirs, files } from "../../util/paths.js";
|
|
19
19
|
import { formatSmartTimestamp } from "../../util/time.js";
|
|
20
|
+
import { createOutage, errorText } from "../health/outage.js";
|
|
20
21
|
|
|
21
22
|
const SESSION_FILE = files.userSession;
|
|
22
23
|
|
|
@@ -152,6 +153,39 @@ export function withTimeout<T>(
|
|
|
152
153
|
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
|
|
153
154
|
}
|
|
154
155
|
|
|
156
|
+
/** How long the monitor may fail to reconnect before the operator hears. */
|
|
157
|
+
const USERBOT_OUTAGE_MS = 15 * 60 * 1000;
|
|
158
|
+
|
|
159
|
+
const outage = createOutage({
|
|
160
|
+
key: "telegram.userbot",
|
|
161
|
+
thresholdMs: USERBOT_OUTAGE_MS,
|
|
162
|
+
severity: "warn",
|
|
163
|
+
describe: (err, mins) =>
|
|
164
|
+
`The Telegram user client has failed to reconnect for ${mins} min: ${err}. ` +
|
|
165
|
+
"Full history search and member lookups are unavailable.",
|
|
166
|
+
recovered: "The Telegram user client is reconnected.",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/** One failed monitor step, logged with its place in the outage. */
|
|
170
|
+
function noteFailure(step: string, err: unknown): void {
|
|
171
|
+
const { attempt, downMs } = outage.fail(err);
|
|
172
|
+
logWarn(
|
|
173
|
+
"userbot",
|
|
174
|
+
`userbot.reconnect.fail step=${step} attempt=${attempt} down_ms=${downMs} ` +
|
|
175
|
+
`next_check_ms=${CHECK_INTERVAL_MS} err=${errorText(err)}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A healthy monitor step; logs the end of an outage when there was one. */
|
|
180
|
+
function noteHealthy(via: string): void {
|
|
181
|
+
const ended = outage.ok();
|
|
182
|
+
if (!ended) return;
|
|
183
|
+
log(
|
|
184
|
+
"userbot",
|
|
185
|
+
`userbot.reconnect.ok via=${via} failed_attempts=${ended.attempts} down_ms=${ended.downMs}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
let reconnecting = false;
|
|
156
190
|
// Bumped by stopConnectionMonitor so a tick whose awaits straddle a shutdown
|
|
157
191
|
// can tell that its own client was torn down under it — the liveness probe
|
|
@@ -164,8 +198,19 @@ function startConnectionMonitor(): void {
|
|
|
164
198
|
const gen = monitorGeneration;
|
|
165
199
|
reconnectTimer = setInterval(async () => {
|
|
166
200
|
if (gen !== monitorGeneration) return;
|
|
167
|
-
if (!client) return;
|
|
168
201
|
if (reconnecting) return; // prevent overlapping reconnect attempts
|
|
202
|
+
if (!client) {
|
|
203
|
+
// A failed re-init leaves no client. Retry it every tick; bailing
|
|
204
|
+
// here left the userbot dead until the daemon restarted.
|
|
205
|
+
if (!storedApiId || !storedApiHash) return;
|
|
206
|
+
reconnecting = true;
|
|
207
|
+
try {
|
|
208
|
+
await reinitClient();
|
|
209
|
+
} finally {
|
|
210
|
+
reconnecting = false;
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
169
214
|
|
|
170
215
|
// GramJS keeps `client.connected === true` even after the underlying
|
|
171
216
|
// socket has silently died, so a real round-trip is the only reliable
|
|
@@ -179,6 +224,7 @@ function startConnectionMonitor(): void {
|
|
|
179
224
|
RECONNECT_STEP_TIMEOUT_MS,
|
|
180
225
|
"liveness probe",
|
|
181
226
|
);
|
|
227
|
+
noteHealthy("probe");
|
|
182
228
|
return; // genuinely alive
|
|
183
229
|
} catch {
|
|
184
230
|
if (gen !== monitorGeneration) return; // monitor stopped mid-probe
|
|
@@ -203,55 +249,73 @@ function startConnectionMonitor(): void {
|
|
|
203
249
|
)
|
|
204
250
|
) {
|
|
205
251
|
log("userbot", "Reconnected successfully.");
|
|
252
|
+
noteHealthy("reconnect");
|
|
206
253
|
} else {
|
|
207
254
|
logWarn("userbot", "Reconnected but not authorized.");
|
|
255
|
+
noteFailure("reconnect", "connected but not authorized");
|
|
208
256
|
}
|
|
209
257
|
} catch (err) {
|
|
210
258
|
if (gen !== monitorGeneration) return; // stopped while reconnecting
|
|
211
259
|
logError("userbot", "Reconnect failed", err);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
// Drop the wedged client without awaiting it — its socket may be
|
|
216
|
-
// the thing that's hung.
|
|
217
|
-
const stale = client;
|
|
218
|
-
client = null;
|
|
219
|
-
void stale?.disconnect().catch(() => {});
|
|
220
|
-
let sessionString = "";
|
|
221
|
-
if (existsSync(SESSION_FILE)) {
|
|
222
|
-
sessionString = readFileSync(SESSION_FILE, "utf-8").trim();
|
|
223
|
-
}
|
|
224
|
-
const session = new StringSession(sessionString);
|
|
225
|
-
client = new TelegramClient(session, storedApiId, storedApiHash, {
|
|
226
|
-
connectionRetries: 5,
|
|
227
|
-
});
|
|
228
|
-
await withTimeout(
|
|
229
|
-
client.connect(),
|
|
230
|
-
RECONNECT_STEP_TIMEOUT_MS,
|
|
231
|
-
"re-init connect",
|
|
232
|
-
);
|
|
233
|
-
if (
|
|
234
|
-
await withTimeout(
|
|
235
|
-
client.isUserAuthorized(),
|
|
236
|
-
RECONNECT_STEP_TIMEOUT_MS,
|
|
237
|
-
"re-init auth check",
|
|
238
|
-
)
|
|
239
|
-
) {
|
|
240
|
-
log("userbot", "Full re-init reconnect succeeded.");
|
|
241
|
-
}
|
|
242
|
-
} catch (retryErr) {
|
|
243
|
-
logError("userbot", "Full re-init reconnect failed", retryErr);
|
|
244
|
-
client = null;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
260
|
+
if (storedApiId && storedApiHash) await reinitClient();
|
|
261
|
+
else noteFailure("reconnect", err);
|
|
247
262
|
} finally {
|
|
248
263
|
reconnecting = false;
|
|
249
264
|
}
|
|
250
265
|
}, CHECK_INTERVAL_MS);
|
|
251
266
|
}
|
|
252
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Replace the client with a fresh one from the saved session. On failure
|
|
270
|
+
* the slot is left empty (and the failed client closed); the monitor's
|
|
271
|
+
* next tick tries again.
|
|
272
|
+
*/
|
|
273
|
+
async function reinitClient(): Promise<void> {
|
|
274
|
+
// Drop the wedged client without awaiting it — its socket may be the
|
|
275
|
+
// thing that's hung.
|
|
276
|
+
const stale = client;
|
|
277
|
+
client = null;
|
|
278
|
+
void stale?.disconnect().catch(() => {});
|
|
279
|
+
let fresh: TelegramClient | null = null;
|
|
280
|
+
try {
|
|
281
|
+
let sessionString = "";
|
|
282
|
+
if (existsSync(SESSION_FILE)) {
|
|
283
|
+
sessionString = readFileSync(SESSION_FILE, "utf-8").trim();
|
|
284
|
+
}
|
|
285
|
+
const session = new StringSession(sessionString);
|
|
286
|
+
fresh = new TelegramClient(session, storedApiId, storedApiHash, {
|
|
287
|
+
connectionRetries: 5,
|
|
288
|
+
});
|
|
289
|
+
client = fresh;
|
|
290
|
+
await withTimeout(
|
|
291
|
+
fresh.connect(),
|
|
292
|
+
RECONNECT_STEP_TIMEOUT_MS,
|
|
293
|
+
"re-init connect",
|
|
294
|
+
);
|
|
295
|
+
if (
|
|
296
|
+
await withTimeout(
|
|
297
|
+
fresh.isUserAuthorized(),
|
|
298
|
+
RECONNECT_STEP_TIMEOUT_MS,
|
|
299
|
+
"re-init auth check",
|
|
300
|
+
)
|
|
301
|
+
) {
|
|
302
|
+
log("userbot", "Full re-init reconnect succeeded.");
|
|
303
|
+
noteHealthy("re-init");
|
|
304
|
+
} else {
|
|
305
|
+
noteFailure("re-init", "connected but not authorized");
|
|
306
|
+
}
|
|
307
|
+
} catch (retryErr) {
|
|
308
|
+
logError("userbot", "Full re-init reconnect failed", retryErr);
|
|
309
|
+
noteFailure("re-init", retryErr);
|
|
310
|
+
if (client === fresh) client = null;
|
|
311
|
+
// Its connect may still be retrying in the background.
|
|
312
|
+
void fresh?.disconnect().catch(() => {});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
253
316
|
function stopConnectionMonitor(): void {
|
|
254
317
|
monitorGeneration++;
|
|
318
|
+
outage.dispose();
|
|
255
319
|
if (reconnectTimer) {
|
|
256
320
|
clearInterval(reconnectTimer);
|
|
257
321
|
reconnectTimer = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** `/reset`, `/resume`, `/rename` — session lifecycle. */
|
|
2
2
|
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
-
import {
|
|
4
|
+
import { formatRelativeAge } from "../../../util/time.js";
|
|
5
5
|
import { isTerminalChatId } from "../../../core/frontend-runtime/chat-id.js";
|
|
6
6
|
import { resolveModel as coreResolveModel } from "../../../core/models/catalog.js";
|
|
7
7
|
import {
|
|
@@ -49,7 +49,7 @@ export const resumeCommand: Command = {
|
|
|
49
49
|
? `"${s.info.sessionName}"`
|
|
50
50
|
: pc.dim("(unnamed)");
|
|
51
51
|
const turns = `${s.info.turns} turn${s.info.turns !== 1 ? "s" : ""}`;
|
|
52
|
-
const ago =
|
|
52
|
+
const ago = formatRelativeAge(s.info.lastActive);
|
|
53
53
|
const model = s.info.lastModel
|
|
54
54
|
? (coreResolveModel(s.info.lastModel)?.displayName ?? s.info.lastModel)
|
|
55
55
|
: "";
|
|
@@ -8,9 +8,7 @@
|
|
|
8
8
|
* - Renderer NEVER touches readline. Only this file does.
|
|
9
9
|
*
|
|
10
10
|
* `start()` returns once that prompt is drawn — the frontend lifecycle
|
|
11
|
-
* contract (core/frontend-runtime/capabilities.ts).
|
|
12
|
-
* promise that never resolved, which is why the composition root had to
|
|
13
|
-
* special-case the terminal and start it without awaiting.
|
|
11
|
+
* contract (core/frontend-runtime/capabilities.ts).
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
14
|
import pc from "picocolors";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import pc from "picocolors";
|
|
10
|
-
import { formatUsd } from "../presentation/format.js";
|
|
10
|
+
import { formatTokenCount, formatUsd } from "../presentation/format.js";
|
|
11
11
|
|
|
12
12
|
// ── Types ────────────────────────────────────────────────────────────────────
|
|
13
13
|
|
|
@@ -70,22 +70,6 @@ export function wrap(text: string, indent: number, maxWidth: number): string {
|
|
|
70
70
|
.join("\n");
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function formatTimeAgo(ts: number): string {
|
|
74
|
-
const sec = Math.floor((Date.now() - ts) / 1000);
|
|
75
|
-
if (sec < 60) return "just now";
|
|
76
|
-
const min = Math.floor(sec / 60);
|
|
77
|
-
if (min < 60) return `${min}m ago`;
|
|
78
|
-
const hr = Math.floor(min / 60);
|
|
79
|
-
if (hr < 24) return `${hr}h ago`;
|
|
80
|
-
return `${Math.floor(hr / 24)}d ago`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function fmtTok(n: number): string {
|
|
84
|
-
if (n < 1000) return String(n);
|
|
85
|
-
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
|
|
86
|
-
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
73
|
export function extractToolDetail(
|
|
90
74
|
input: Record<string, unknown>,
|
|
91
75
|
maxLen: number,
|
|
@@ -189,7 +173,7 @@ export function createRenderer(cols?: number, displayName = "Talon"): Renderer {
|
|
|
189
173
|
if (info.sessionName) p.push(`"${info.sessionName}"`);
|
|
190
174
|
p.push(
|
|
191
175
|
`${info.turns} turn${info.turns !== 1 ? "s" : ""}`,
|
|
192
|
-
`${
|
|
176
|
+
`${formatTokenCount(info.inputTokens + info.outputTokens)} tok`,
|
|
193
177
|
);
|
|
194
178
|
if (typeof info.cacheHitPct === "number") {
|
|
195
179
|
p.push(`${info.cacheHitPct}% cache`);
|
|
@@ -33,6 +33,16 @@ import { mediaHandlers } from "./media.js";
|
|
|
33
33
|
import { messagingHandlers } from "./messaging.js";
|
|
34
34
|
import { moderationHandlers } from "./moderation.js";
|
|
35
35
|
import type { WhatsAppActionContext, WhatsAppActionHandlers } from "./types.js";
|
|
36
|
+
import {
|
|
37
|
+
createDeliveryTracker,
|
|
38
|
+
trackDeliveries,
|
|
39
|
+
} from "../../health/delivery.js";
|
|
40
|
+
|
|
41
|
+
const delivery = createDeliveryTracker("whatsapp", "WhatsApp", "whatsapp");
|
|
42
|
+
|
|
43
|
+
/** A cross-send names its destination; everything else goes to `chatId`. */
|
|
44
|
+
const destination = (body: Record<string, unknown>, chatId: number) =>
|
|
45
|
+
body.target !== undefined ? String(body.target) : chatId;
|
|
36
46
|
|
|
37
47
|
// Null-prototype so a request `action` of "toString" / "constructor" can't
|
|
38
48
|
// resolve an inherited Object.prototype method via `handlers[action]`.
|
|
@@ -62,7 +72,7 @@ export function createWhatsAppActionHandler(
|
|
|
62
72
|
) {
|
|
63
73
|
const scheduledMessages = new Map<string, ReturnType<typeof setTimeout>>();
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
const dispatch = async (
|
|
66
76
|
body: Record<string, unknown>,
|
|
67
77
|
chatId: number,
|
|
68
78
|
): Promise<ActionResult | null> => {
|
|
@@ -118,4 +128,5 @@ export function createWhatsAppActionHandler(
|
|
|
118
128
|
? { ...result, chat_id: chat.chatId }
|
|
119
129
|
: result;
|
|
120
130
|
};
|
|
131
|
+
return trackDeliveries(delivery, dispatch, destination);
|
|
121
132
|
}
|
|
@@ -17,6 +17,9 @@ import { tryAction } from "./try-action.js";
|
|
|
17
17
|
import { resolveQuoted, sendContent, sendText, boundedSend } from "./send.js";
|
|
18
18
|
import type { WhatsAppActionHandlers } from "./types.js";
|
|
19
19
|
|
|
20
|
+
/** The tool schema's documented ceiling (1-86400), as telegram/discord clamp. */
|
|
21
|
+
const MAX_DELAY_SEC = 24 * 60 * 60;
|
|
22
|
+
|
|
20
23
|
/** WhatsApp keeps a pin for 24h, 7d, or 30d — no indefinite option. */
|
|
21
24
|
const PIN_DURATIONS = [86_400, 604_800, 2_592_000] as const;
|
|
22
25
|
|
|
@@ -229,16 +232,18 @@ export const messagingHandlers: WhatsAppActionHandlers = {
|
|
|
229
232
|
schedule_message: (body, _chatId, ctx) =>
|
|
230
233
|
tryAction("schedule_message", async () => {
|
|
231
234
|
const text = String(body.text ?? "");
|
|
232
|
-
const
|
|
235
|
+
const requested = Number(body.delay_seconds ?? 0);
|
|
233
236
|
if (!text.trim()) {
|
|
234
237
|
return { ok: false, error: "schedule_message: text is required" };
|
|
235
238
|
}
|
|
236
|
-
if (!Number.isFinite(
|
|
239
|
+
if (!Number.isFinite(requested) || requested <= 0) {
|
|
237
240
|
return {
|
|
238
241
|
ok: false,
|
|
239
242
|
error: "schedule_message: delay_seconds must be a positive number",
|
|
240
243
|
};
|
|
241
244
|
}
|
|
245
|
+
// Past setTimeout's 2^31-1 ms (~24.8 days) the timer fires at once.
|
|
246
|
+
const seconds = Math.min(MAX_DELAY_SEC, requested);
|
|
242
247
|
const id = randomUUID();
|
|
243
248
|
const chat = ctx.chat!;
|
|
244
249
|
const timer = setTimeout(() => {
|
|
@@ -11,7 +11,10 @@ import makeWASocket, { type AuthenticationState, type WASocket } from "baileys";
|
|
|
11
11
|
import qrcode from "qrcode-terminal";
|
|
12
12
|
import { log, logError, logWarn } from "../../../util/log.js";
|
|
13
13
|
import { dirs } from "../../../util/paths.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
raiseAlert,
|
|
16
|
+
resolveAlert,
|
|
17
|
+
} from "../../../core/frontend-runtime/alerts.js";
|
|
15
18
|
import { useAtomicAuthState } from "./auth-state.js";
|
|
16
19
|
import { bareId } from "./identity.js";
|
|
17
20
|
import { handleInbound } from "../messages/inbound.js";
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
type WhatsAppRuntime,
|
|
24
27
|
} from "../runtime.js";
|
|
25
28
|
import { makeWaLogger } from "./wa-logger.js";
|
|
29
|
+
import { errorText } from "../../health/outage.js";
|
|
26
30
|
|
|
27
31
|
/** What the connection loop should do after a socket ends. */
|
|
28
32
|
type ConnectOutcome = "reconnect" | "logged-out" | "unpaired" | "stop";
|
|
@@ -64,9 +68,11 @@ function onQr(runtime: WhatsAppRuntime, qr: string): void {
|
|
|
64
68
|
qrcode.generate(qr, { small: true });
|
|
65
69
|
if (!runtime.unpairedNotified) {
|
|
66
70
|
runtime.unpairedNotified = true;
|
|
67
|
-
|
|
68
|
-
"
|
|
71
|
+
raiseAlert(
|
|
72
|
+
"whatsapp.linked",
|
|
73
|
+
"WhatsApp is not linked. Send /whatsapp pair when you're " +
|
|
69
74
|
"ready and I'll reply with a QR to scan.",
|
|
75
|
+
{ severity: "warn" },
|
|
70
76
|
);
|
|
71
77
|
}
|
|
72
78
|
}
|
|
@@ -77,10 +83,10 @@ function onOpen(runtime: WhatsAppRuntime, socket: WASocket): void {
|
|
|
77
83
|
.filter((id): id is string => Boolean(id))
|
|
78
84
|
.map(bareId);
|
|
79
85
|
runtime.reconnectDelay = RECONNECT_BASE_MS;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
86
|
+
// Announces only when a "not linked" / "unlinked" alert went out.
|
|
87
|
+
resolveAlert("whatsapp.linked", "WhatsApp linked and connected.");
|
|
83
88
|
runtime.unpairedNotified = false;
|
|
89
|
+
runtime.health.opened();
|
|
84
90
|
log(
|
|
85
91
|
"whatsapp",
|
|
86
92
|
`Connected as ${socket.user?.name ?? "?"} (${runtime.selfIds.join("/") || "?"})`,
|
|
@@ -91,6 +97,7 @@ function onClose(
|
|
|
91
97
|
runtime: WhatsAppRuntime,
|
|
92
98
|
state: AuthenticationState,
|
|
93
99
|
code: number | undefined,
|
|
100
|
+
reason: string,
|
|
94
101
|
): ConnectOutcome {
|
|
95
102
|
if (runtime.stopping) return "stop";
|
|
96
103
|
const disposition = classifyClose(code, state.creds.registered);
|
|
@@ -113,6 +120,7 @@ function onClose(
|
|
|
113
120
|
"whatsapp",
|
|
114
121
|
"Connection replaced by another client (440) — backing off",
|
|
115
122
|
);
|
|
123
|
+
runtime.health.noteClose("replaced by another client (440)");
|
|
116
124
|
runtime.reconnectDelay = REPLACED_BACKOFF_MS;
|
|
117
125
|
return "reconnect";
|
|
118
126
|
case "logged-out":
|
|
@@ -128,6 +136,7 @@ function onClose(
|
|
|
128
136
|
// session on its first ordinary disconnect.
|
|
129
137
|
if (!isPaired(state.creds)) return "unpaired";
|
|
130
138
|
log("whatsapp", `Connection closed (code ${code ?? "?"}) — reconnecting`);
|
|
139
|
+
runtime.health.noteClose(`code=${code ?? "?"} ${reason}`);
|
|
131
140
|
return "reconnect";
|
|
132
141
|
}
|
|
133
142
|
}
|
|
@@ -163,6 +172,9 @@ async function connectOnce(
|
|
|
163
172
|
// Atomic replacement for Baileys' useMultiFileAuthState — same disk
|
|
164
173
|
// format, torn-write-proof (see auth-state.ts for why that matters).
|
|
165
174
|
const { state, saveCreds } = await useAtomicAuthState(dirs.whatsappAuth);
|
|
175
|
+
// stop() ends runtime.sock, which is still null while auth loads; a
|
|
176
|
+
// socket built after it would never close and stop() would wait forever.
|
|
177
|
+
if (runtime.stopping) return "stop";
|
|
166
178
|
const socket = makeWASocket({
|
|
167
179
|
auth: state,
|
|
168
180
|
logger: makeWaLogger(),
|
|
@@ -195,7 +207,8 @@ async function connectOnce(
|
|
|
195
207
|
lastDisconnect?.error as
|
|
196
208
|
{ output?: { statusCode?: number } } | undefined
|
|
197
209
|
)?.output?.statusCode;
|
|
198
|
-
|
|
210
|
+
const reason = errorText(lastDisconnect?.error ?? "closed");
|
|
211
|
+
resolve(onClose(runtime, state, code, reason));
|
|
199
212
|
}
|
|
200
213
|
});
|
|
201
214
|
});
|
|
@@ -260,6 +273,7 @@ export async function runConnectionLoop(
|
|
|
260
273
|
try {
|
|
261
274
|
await connectionLoop(runtime, signalReady);
|
|
262
275
|
} finally {
|
|
276
|
+
runtime.health.dispose();
|
|
263
277
|
signalReady();
|
|
264
278
|
log("whatsapp", "WhatsApp connection loop ended");
|
|
265
279
|
}
|
|
@@ -290,6 +304,7 @@ async function connectionLoop(
|
|
|
290
304
|
// half-written auth dir, say) must not hold the boot open while
|
|
291
305
|
// the loop keeps trying.
|
|
292
306
|
signalReady();
|
|
307
|
+
runtime.health.noteClose(`socket error: ${errorText(err)}`);
|
|
293
308
|
outcome = "reconnect";
|
|
294
309
|
}
|
|
295
310
|
runtime.sock = null;
|
|
@@ -306,10 +321,13 @@ async function connectionLoop(
|
|
|
306
321
|
"whatsapp",
|
|
307
322
|
"Logged out by WhatsApp — parked until /whatsapp pair re-links",
|
|
308
323
|
);
|
|
309
|
-
|
|
310
|
-
|
|
324
|
+
runtime.health.dispose();
|
|
325
|
+
raiseAlert(
|
|
326
|
+
"whatsapp.linked",
|
|
327
|
+
"WhatsApp unlinked this device. When you're ready to " +
|
|
311
328
|
"re-link, send /whatsapp pair and scan the QR I reply with. " +
|
|
312
329
|
"Nothing is retried until then.",
|
|
330
|
+
{ severity: "warn" },
|
|
313
331
|
);
|
|
314
332
|
rmSync(dirs.whatsappAuth, { recursive: true, force: true });
|
|
315
333
|
await parkUntilPaired(runtime);
|
|
@@ -318,10 +336,12 @@ async function connectionLoop(
|
|
|
318
336
|
}
|
|
319
337
|
if (outcome === "unpaired") {
|
|
320
338
|
log("whatsapp", "Not paired — parked until /whatsapp pair links");
|
|
339
|
+
runtime.health.dispose();
|
|
321
340
|
await parkUntilPaired(runtime);
|
|
322
341
|
runtime.reconnectDelay = RECONNECT_BASE_MS;
|
|
323
342
|
continue;
|
|
324
343
|
}
|
|
344
|
+
runtime.health.reconnecting(runtime.reconnectDelay);
|
|
325
345
|
await sleep(runtime, runtime.reconnectDelay);
|
|
326
346
|
runtime.reconnectDelay = Math.min(
|
|
327
347
|
runtime.reconnectDelay * 2,
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection health — the operator hears when the WhatsApp socket keeps
|
|
3
|
+
* dropping instead of staying up.
|
|
4
|
+
*
|
|
5
|
+
* Every unexpected close is logged with its attempt number, the time the
|
|
6
|
+
* connection has been unstable, the backoff before the next socket, and
|
|
7
|
+
* the reason. Closes that keep coming for `CONNECTION_OUTAGE_MS` raise
|
|
8
|
+
* `whatsapp.connection`. An open alone does not end the outage — a socket
|
|
9
|
+
* that opens and dies again is exactly the flapping this watches for — so
|
|
10
|
+
* the outage ends only once a socket has stayed open for `STABLE_OPEN_MS`.
|
|
11
|
+
*
|
|
12
|
+
* Unlinking is not a connection fault: the loop parks and the
|
|
13
|
+
* `whatsapp.linked` alert speaks for it (connection.ts).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { log, logWarn } from "../../../util/log.js";
|
|
17
|
+
import { createOutage } from "../../health/outage.js";
|
|
18
|
+
|
|
19
|
+
const CONNECTION_OUTAGE_MS = 10 * 60_000;
|
|
20
|
+
/** How long a socket must stay open before the connection counts as stable. */
|
|
21
|
+
const STABLE_OPEN_MS = 60_000;
|
|
22
|
+
|
|
23
|
+
export type ConnectionHealth = {
|
|
24
|
+
/** Record why the socket just closed; `reconnecting` counts it. */
|
|
25
|
+
noteClose(detail: string): void;
|
|
26
|
+
/** The loop will build a new socket after `backoffMs`. */
|
|
27
|
+
reconnecting(backoffMs: number): void;
|
|
28
|
+
opened(): void;
|
|
29
|
+
/** Forget the outage without resolving it — park or shutdown. */
|
|
30
|
+
dispose(): void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function createConnectionHealth(
|
|
34
|
+
thresholdMs = CONNECTION_OUTAGE_MS,
|
|
35
|
+
stableMs = STABLE_OPEN_MS,
|
|
36
|
+
): ConnectionHealth {
|
|
37
|
+
const outage = createOutage({
|
|
38
|
+
key: "whatsapp.connection",
|
|
39
|
+
thresholdMs,
|
|
40
|
+
describe: (err, mins) =>
|
|
41
|
+
`The WhatsApp connection has kept dropping for ${mins} min: ${err}. ` +
|
|
42
|
+
"Messages may not be received or delivered.",
|
|
43
|
+
recovered: "The WhatsApp connection is stable again.",
|
|
44
|
+
});
|
|
45
|
+
let pending: string | null = null;
|
|
46
|
+
let stableTimer: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
+
const clearStable = (): void => {
|
|
48
|
+
if (stableTimer) clearTimeout(stableTimer);
|
|
49
|
+
stableTimer = null;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
noteClose(detail) {
|
|
54
|
+
clearStable();
|
|
55
|
+
pending = detail;
|
|
56
|
+
},
|
|
57
|
+
reconnecting(backoffMs) {
|
|
58
|
+
// A close nobody noted (515 finishing a pairing) is not a fault.
|
|
59
|
+
if (pending === null) return;
|
|
60
|
+
const detail = pending;
|
|
61
|
+
pending = null;
|
|
62
|
+
const { attempt, downMs } = outage.fail(detail);
|
|
63
|
+
logWarn(
|
|
64
|
+
"whatsapp",
|
|
65
|
+
`connection.reconnect attempt=${attempt} down_ms=${downMs} backoff_ms=${backoffMs} err=${detail}`,
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
opened() {
|
|
69
|
+
clearStable();
|
|
70
|
+
if (!outage.down) return;
|
|
71
|
+
stableTimer = setTimeout(() => {
|
|
72
|
+
stableTimer = null;
|
|
73
|
+
const ended = outage.ok();
|
|
74
|
+
if (ended) {
|
|
75
|
+
log(
|
|
76
|
+
"whatsapp",
|
|
77
|
+
`connection.stable failed_attempts=${ended.attempts} down_ms=${ended.downMs}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}, stableMs);
|
|
81
|
+
stableTimer.unref?.();
|
|
82
|
+
},
|
|
83
|
+
dispose() {
|
|
84
|
+
clearStable();
|
|
85
|
+
pending = null;
|
|
86
|
+
outage.dispose();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Identity resolution — the LID/phone-number duality.
|
|
3
3
|
*
|
|
4
4
|
* WhatsApp addresses people two ways. The historical form is the phone
|
|
5
|
-
* number (`
|
|
6
|
-
* is a linked identity (`
|
|
5
|
+
* number (`447700900102@s.whatsapp.net`, "PN"); the newer privacy form
|
|
6
|
+
* is a linked identity (`190000000000001@lid`, "LID") that hides the
|
|
7
7
|
* number. Which one arrives depends on the sender's privacy settings and
|
|
8
8
|
* the chat's addressing mode, and neither is derivable from the other —
|
|
9
9
|
* they are looked up.
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
import { isLidUser, jidNormalizedUser, type WASocket } from "baileys";
|
|
23
23
|
|
|
24
24
|
export type Identity = {
|
|
25
|
-
/** Phone-number form, digits only, when known: "
|
|
25
|
+
/** Phone-number form, digits only, when known: "447700900102". */
|
|
26
26
|
phone?: string;
|
|
27
|
-
/** LID form, digits only, when known: "
|
|
27
|
+
/** LID form, digits only, when known: "190000000000001". */
|
|
28
28
|
lid?: string;
|
|
29
29
|
/** Every bare id this person is known by — what allowlists match on. */
|
|
30
30
|
ids: string[];
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WhatsApp frontend runtime — the state every module shares.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* allow-lists and the live socket state; the modules own the behaviour.
|
|
4
|
+
* One explicit object, constructed once, that each module (access,
|
|
5
|
+
* inbound, connection) takes as its first parameter. The runtime carries
|
|
6
|
+
* the parsed settings, the allow-lists and the live socket state; the
|
|
7
|
+
* modules own the behaviour.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
10
|
import type { WASocket } from "baileys";
|
|
12
11
|
import type { TalonConfig } from "../../core/config/index.js";
|
|
13
12
|
import type { Gateway } from "../../core/engine/gateway.js";
|
|
14
13
|
import { bareId } from "./connection/identity.js";
|
|
14
|
+
import {
|
|
15
|
+
createConnectionHealth,
|
|
16
|
+
type ConnectionHealth,
|
|
17
|
+
} from "./connection/health.js";
|
|
15
18
|
|
|
16
19
|
type WhatsAppSettings = {
|
|
17
20
|
allowedJids: string[];
|
|
@@ -47,6 +50,8 @@ export type WhatsAppRuntime = {
|
|
|
47
50
|
reconnectDelay: number;
|
|
48
51
|
/** One "not linked" admin note per outage, not one per QR window. */
|
|
49
52
|
unpairedNotified: boolean;
|
|
53
|
+
/** Reconnect logging and the `whatsapp.connection` alert. */
|
|
54
|
+
readonly health: ConnectionHealth;
|
|
50
55
|
/** Our own ids (phone and LID), once connected — for mention detection. */
|
|
51
56
|
selfIds: string[];
|
|
52
57
|
};
|
|
@@ -76,6 +81,7 @@ export function createWhatsAppRuntime(
|
|
|
76
81
|
stopRequest: new AbortController(),
|
|
77
82
|
reconnectDelay: RECONNECT_BASE_MS,
|
|
78
83
|
unpairedNotified: false,
|
|
84
|
+
health: createConnectionHealth(),
|
|
79
85
|
selfIds: [],
|
|
80
86
|
};
|
|
81
87
|
}
|
package/src/native/blake3.ts
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import { createReadStream } from "node:fs";
|
|
35
35
|
import { createRequire } from "node:module";
|
|
36
36
|
import { fileURLToPath } from "node:url";
|
|
37
|
+
import { logDebug, logWarn } from "../util/log.js";
|
|
37
38
|
import { BLAKE3_WASM_BASE64 } from "./blake3-wasm-bytes.js";
|
|
38
39
|
import {
|
|
39
40
|
allocRegion,
|
|
@@ -99,13 +100,38 @@ function loadNativeBlake3(): NativeBlake3 | null {
|
|
|
99
100
|
const addon = requireAddon(candidate) as NativeBlake3;
|
|
100
101
|
// Trust nothing that can't produce a known digest — a truncated or
|
|
101
102
|
// wrong-arch artifact fails here and wasm silently takes over.
|
|
102
|
-
if (addon.hashHex(Buffer.alloc(0)) !== EMPTY_DIGEST)
|
|
103
|
+
if (addon.hashHex(Buffer.alloc(0)) !== EMPTY_DIGEST) {
|
|
104
|
+
reportAddonRejected(candidate, "digest mismatch");
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
103
107
|
return addon;
|
|
104
|
-
} catch {
|
|
108
|
+
} catch (err) {
|
|
109
|
+
reportAddonRejected(candidate, err);
|
|
105
110
|
return null;
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Wasm takes over either way; the log says why. An absent default
|
|
116
|
+
* artifact is the normal npm install; an explicit TALON_BLAKE3_NODE
|
|
117
|
+
* that fails to load is operator config silently not taking effect.
|
|
118
|
+
*/
|
|
119
|
+
function reportAddonRejected(candidate: string, reason: unknown): void {
|
|
120
|
+
const code = (reason as { code?: unknown } | null)?.code;
|
|
121
|
+
const why = reason instanceof Error ? reason.message : String(reason);
|
|
122
|
+
if (process.env.TALON_BLAKE3_NODE) {
|
|
123
|
+
logWarn(
|
|
124
|
+
"native",
|
|
125
|
+
`TALON_BLAKE3_NODE addon rejected, using wasm path=${candidate}: ${why}`,
|
|
126
|
+
);
|
|
127
|
+
} else if (code !== "MODULE_NOT_FOUND") {
|
|
128
|
+
logDebug(
|
|
129
|
+
"native",
|
|
130
|
+
`blake3 addon rejected, using wasm path=${candidate}: ${why}`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
/** Tests swap addons via TALON_BLAKE3_NODE and need the memo dropped. */
|
|
110
136
|
export function _resetNativeBlake3ForTesting(): void {
|
|
111
137
|
nativeAddon = undefined;
|