dsh-connect 0.6.7 → 0.7.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/lib/binding.d.ts +2 -0
- package/lib/binding.d.ts.map +1 -1
- package/lib/binding.js +1 -1
- package/lib/binding.js.map +1 -1
- package/lib/commands.d.ts +10 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +18 -6
- package/lib/commands.js.map +1 -1
- package/lib/dedup.d.ts +31 -0
- package/lib/dedup.d.ts.map +1 -0
- package/lib/dedup.js +52 -0
- package/lib/dedup.js.map +1 -0
- package/lib/i18n.d.ts +17 -1
- package/lib/i18n.d.ts.map +1 -1
- package/lib/i18n.js +34 -2
- package/lib/i18n.js.map +1 -1
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/mirror-lock.d.ts +51 -0
- package/lib/mirror-lock.d.ts.map +1 -0
- package/lib/mirror-lock.js +56 -0
- package/lib/mirror-lock.js.map +1 -0
- package/lib/retry.d.ts +35 -0
- package/lib/retry.d.ts.map +1 -0
- package/lib/retry.js +41 -0
- package/lib/retry.js.map +1 -0
- package/lib/runner.d.ts +42 -6
- package/lib/runner.d.ts.map +1 -1
- package/lib/runner.js +214 -106
- package/lib/runner.js.map +1 -1
- package/lib/scheduler.d.ts +46 -0
- package/lib/scheduler.d.ts.map +1 -0
- package/lib/scheduler.js +162 -0
- package/lib/scheduler.js.map +1 -0
- package/lib/service.d.ts +27 -0
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +106 -3
- package/lib/service.js.map +1 -1
- package/lib/types.d.ts +8 -0
- package/lib/types.d.ts.map +1 -1
- package/package.json +1 -1
package/lib/runner.js
CHANGED
|
@@ -13,6 +13,8 @@ import { createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
|
13
13
|
import { createAsyncQueue } from "./stream.js";
|
|
14
14
|
import { helpText, parseCommand } from "./commands.js";
|
|
15
15
|
import { messages } from "./i18n.js";
|
|
16
|
+
import { DEFAULT_LOCK_TIMEOUT_MS, acquire as lockAcquire, canWrite as lockCanWrite, isLockTimedOut as lockIsTimedOut, release as lockRelease, } from "./mirror-lock.js";
|
|
17
|
+
import { formatRemindAt, parseRemindTime } from "./scheduler.js";
|
|
16
18
|
export function resolveConnectConfig(config) {
|
|
17
19
|
return {
|
|
18
20
|
agentPreset: config.agentPreset,
|
|
@@ -364,6 +366,8 @@ export class AgentRunner {
|
|
|
364
366
|
adapter;
|
|
365
367
|
bindings;
|
|
366
368
|
adapters;
|
|
369
|
+
requeue;
|
|
370
|
+
reminders;
|
|
367
371
|
queue = [];
|
|
368
372
|
running = false;
|
|
369
373
|
agent;
|
|
@@ -374,7 +378,15 @@ export class AgentRunner {
|
|
|
374
378
|
notifyLevel;
|
|
375
379
|
progressTimeoutMs;
|
|
376
380
|
t;
|
|
377
|
-
constructor(ctx, config, channel, chatKey, chatType, adapter, bindings, adapters
|
|
381
|
+
constructor(ctx, config, channel, chatKey, chatType, adapter, bindings, adapters,
|
|
382
|
+
/**
|
|
383
|
+
* Route a replayed queued message back into the runner of its own channel
|
|
384
|
+
* (wired by ConnectService). Without it, replayed messages would run in
|
|
385
|
+
* whichever runner happened to release the lock.
|
|
386
|
+
*/
|
|
387
|
+
requeue,
|
|
388
|
+
/** Persistent chat-level reminders (`/remind`), wired by ConnectService. */
|
|
389
|
+
reminders) {
|
|
378
390
|
this.ctx = ctx;
|
|
379
391
|
this.config = config;
|
|
380
392
|
this.channel = channel;
|
|
@@ -383,6 +395,8 @@ export class AgentRunner {
|
|
|
383
395
|
this.adapter = adapter;
|
|
384
396
|
this.bindings = bindings;
|
|
385
397
|
this.adapters = adapters;
|
|
398
|
+
this.requeue = requeue;
|
|
399
|
+
this.reminders = reminders;
|
|
386
400
|
this.workDir = config.workDir ?? this.resolveDefaultWorkDir();
|
|
387
401
|
// A per-chat language override (set via the settings menu) wins over config.
|
|
388
402
|
const stored = this.bindings.get(channel, chatKey);
|
|
@@ -685,7 +699,7 @@ export class AgentRunner {
|
|
|
685
699
|
await this.sendTurnStats(msg, outcome);
|
|
686
700
|
// Release lock after task completion
|
|
687
701
|
if (usesLock)
|
|
688
|
-
this.releaseLock();
|
|
702
|
+
await this.releaseLock();
|
|
689
703
|
if (outcome.reason !== "completed") {
|
|
690
704
|
await this.sendSummary(msg, outcome);
|
|
691
705
|
}
|
|
@@ -693,7 +707,7 @@ export class AgentRunner {
|
|
|
693
707
|
catch (error) {
|
|
694
708
|
// Release lock even on error
|
|
695
709
|
if (usesLock)
|
|
696
|
-
this.releaseLock();
|
|
710
|
+
await this.releaseLock();
|
|
697
711
|
const detail = error instanceof Error ? error.message : String(error);
|
|
698
712
|
await this.adapter
|
|
699
713
|
.sendText(this.target(msg), this.t.processingFailedAdvice(truncate(detail, 400), this.errorAdvice(detail)))
|
|
@@ -1405,6 +1419,18 @@ export class AgentRunner {
|
|
|
1405
1419
|
await this.handleAppend(command.text, target, msg);
|
|
1406
1420
|
break;
|
|
1407
1421
|
}
|
|
1422
|
+
case "remind": {
|
|
1423
|
+
await this.handleRemind(command.text, target, msg);
|
|
1424
|
+
break;
|
|
1425
|
+
}
|
|
1426
|
+
case "send": {
|
|
1427
|
+
await this.handleSend(command.path, target);
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
case "broadcast": {
|
|
1431
|
+
await this.handleBroadcast(command.text, target, msg);
|
|
1432
|
+
break;
|
|
1433
|
+
}
|
|
1408
1434
|
case "help": {
|
|
1409
1435
|
await this.adapter.sendText(target, helpText(this.t));
|
|
1410
1436
|
break;
|
|
@@ -1446,6 +1472,103 @@ export class AgentRunner {
|
|
|
1446
1472
|
this.touchBinding(msg);
|
|
1447
1473
|
await this.adapter.sendText(target, this.t.psReceived(text));
|
|
1448
1474
|
}
|
|
1475
|
+
/**
|
|
1476
|
+
* `/remind <time> <text>` — persist a chat-level reminder that fires
|
|
1477
|
+
* without waking the agent (see src/scheduler.ts).
|
|
1478
|
+
*/
|
|
1479
|
+
async handleRemind(argText, target, msg) {
|
|
1480
|
+
if (this.reminders === undefined) {
|
|
1481
|
+
await this.adapter.sendText(target, this.t.reminderNoText);
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const arg = argText.trim();
|
|
1485
|
+
if (arg === "") {
|
|
1486
|
+
await this.adapter.sendText(target, this.t.reminderNoText);
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const space = arg.indexOf(" ");
|
|
1490
|
+
const time = space === -1 ? arg : arg.slice(0, space);
|
|
1491
|
+
const body = space === -1 ? "" : arg.slice(space + 1).trim();
|
|
1492
|
+
const dueAt = parseRemindTime(time);
|
|
1493
|
+
if (dueAt === undefined) {
|
|
1494
|
+
await this.adapter.sendText(target, this.t.reminderParseFailed(time));
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
if (body === "") {
|
|
1498
|
+
await this.adapter.sendText(target, this.t.reminderNoText);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
this.reminders.add({
|
|
1502
|
+
channel: this.channel,
|
|
1503
|
+
chatKey: this.chatKey,
|
|
1504
|
+
chatType: this.chatType,
|
|
1505
|
+
text: body,
|
|
1506
|
+
dueAt,
|
|
1507
|
+
ownerKey: msg.senderKey,
|
|
1508
|
+
});
|
|
1509
|
+
await this.adapter.sendText(target, this.t.reminderSet(formatRemindAt(dueAt, this.language), body));
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* `/send <path>` — deliver a file from the workspace through the channel's
|
|
1513
|
+
* `sendFile` capability; falls back to sending the path as text.
|
|
1514
|
+
*/
|
|
1515
|
+
async handleSend(pathArg, target) {
|
|
1516
|
+
if (pathArg === "") {
|
|
1517
|
+
await this.adapter.sendText(target, this.t.sendUsage);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const path = isAbsolute(pathArg) ? pathArg : join(this.workDir, pathArg);
|
|
1521
|
+
let size;
|
|
1522
|
+
try {
|
|
1523
|
+
if (!existsSync(path)) {
|
|
1524
|
+
await this.adapter.sendText(target, this.t.sendNotFound(path));
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
const stat = statSync(path);
|
|
1528
|
+
if (stat.isDirectory()) {
|
|
1529
|
+
await this.adapter.sendText(target, this.t.sendIsDir(path));
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
size = stat.size;
|
|
1533
|
+
}
|
|
1534
|
+
catch {
|
|
1535
|
+
await this.adapter.sendText(target, this.t.sendNotFound(path));
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
if (size > 20 * 1024 * 1024) {
|
|
1539
|
+
await this.adapter.sendText(target, this.t.sendTooLarge(path));
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (this.adapter.sendFile === undefined) {
|
|
1543
|
+
await this.adapter.sendText(target, `${this.t.sendUnsupported}\n${path}`);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
try {
|
|
1547
|
+
await this.adapter.sendFile(target, path);
|
|
1548
|
+
await this.adapter.sendText(target, this.t.sendSent(path));
|
|
1549
|
+
}
|
|
1550
|
+
catch (error) {
|
|
1551
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1552
|
+
await this.adapter.sendText(target, this.t.sendFailed(truncate(detail)));
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* `/broadcast <text>` — admins push a message to every bound chat.
|
|
1557
|
+
* Admin gating lives in the connect service (allowUsers).
|
|
1558
|
+
*/
|
|
1559
|
+
async handleBroadcast(text, target, msg) {
|
|
1560
|
+
const service = this.ctx.get("connect");
|
|
1561
|
+
if (service?.broadcast === undefined) {
|
|
1562
|
+
await this.adapter.sendText(target, this.t.broadcastUnsupported);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const result = await service.broadcast(msg.senderKey, text);
|
|
1566
|
+
if (!result.ok) {
|
|
1567
|
+
await this.adapter.sendText(target, result.reason === "disabled" ? this.t.broadcastDisabled : this.t.broadcastNotAdmin);
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
await this.adapter.sendText(target, this.t.broadcastSent(result.count ?? 0));
|
|
1571
|
+
}
|
|
1449
1572
|
recordSession(sessionId, title, ownerKey) {
|
|
1450
1573
|
const binding = this.bindings.get(this.channel, this.chatKey);
|
|
1451
1574
|
const others = (binding?.sessions ?? []).filter((s) => s.sessionId !== sessionId);
|
|
@@ -2195,26 +2318,38 @@ export class AgentRunner {
|
|
|
2195
2318
|
}
|
|
2196
2319
|
}
|
|
2197
2320
|
async showSchedule(target) {
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
}
|
|
2203
|
-
const reminders = foldReminders(agent.session.events);
|
|
2204
|
-
if (reminders.length === 0) {
|
|
2205
|
-
await this.adapter.sendText(target, this.t.noReminders);
|
|
2206
|
-
return;
|
|
2207
|
-
}
|
|
2321
|
+
// Agent-level reminders (session `schedule` tool) + persistent chat-level
|
|
2322
|
+
// reminders (`/remind`), merged into one list.
|
|
2323
|
+
const agentReminders = this.agent === undefined ? [] : foldReminders(this.agent.session.events);
|
|
2324
|
+
const persisted = this.reminders?.listFor(this.channel, this.chatKey) ?? [];
|
|
2208
2325
|
const now = Date.now();
|
|
2209
2326
|
const locale = this.language === "en" ? "en-US" : "zh-CN";
|
|
2210
|
-
const
|
|
2327
|
+
const agentLines = agentReminders.map((r) => {
|
|
2211
2328
|
const due = Date.parse(r.scheduledAt) <= now;
|
|
2212
2329
|
const state = due ? this.t.reminderDue : this.t.reminderClock;
|
|
2213
2330
|
const kind = r.kind === "every" ? this.t.reminderEvery(Math.round((r.everySeconds ?? 0) / 60)) : this.t.reminderOnce;
|
|
2214
2331
|
const when = new Date(Date.parse(r.scheduledAt)).toLocaleString(locale);
|
|
2215
2332
|
return this.t.reminderLine(state, r.id, r.prompt, kind, when);
|
|
2216
2333
|
});
|
|
2217
|
-
|
|
2334
|
+
const persistedLines = persisted.map((r) => {
|
|
2335
|
+
const due = r.dueAt <= now;
|
|
2336
|
+
const state = due ? this.t.reminderDue : this.t.reminderClock;
|
|
2337
|
+
const when = new Date(r.dueAt).toLocaleString(locale);
|
|
2338
|
+
return this.t.reminderLine(state, r.id, r.text, this.t.reminderOnce, when);
|
|
2339
|
+
});
|
|
2340
|
+
const total = agentLines.length + persistedLines.length;
|
|
2341
|
+
if (total === 0) {
|
|
2342
|
+
await this.adapter.sendText(target, this.t.noReminders);
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
const parts = [];
|
|
2346
|
+
if (agentLines.length > 0)
|
|
2347
|
+
parts.push(...agentLines);
|
|
2348
|
+
if (persistedLines.length > 0) {
|
|
2349
|
+
parts.push(this.t.reminderPersistedHeader);
|
|
2350
|
+
parts.push(...persistedLines);
|
|
2351
|
+
}
|
|
2352
|
+
await this.adapter.sendText(target, this.t.remindersCount(total, parts.join("\n")));
|
|
2218
2353
|
}
|
|
2219
2354
|
async showAllWorkspaces(target) {
|
|
2220
2355
|
const registry = this.ctx.get("workspaceRegistry");
|
|
@@ -2299,7 +2434,7 @@ export class AgentRunner {
|
|
|
2299
2434
|
}
|
|
2300
2435
|
// Process any queued messages before releasing
|
|
2301
2436
|
const queuedCount = binding.queuedMessages?.length ?? 0;
|
|
2302
|
-
this.releaseLock();
|
|
2437
|
+
await this.releaseLock();
|
|
2303
2438
|
let message = this.t.unlockSuccess;
|
|
2304
2439
|
if (queuedCount > 0) {
|
|
2305
2440
|
message += `\n${this.t.queueProcessed(queuedCount)}`;
|
|
@@ -2332,7 +2467,8 @@ export class AgentRunner {
|
|
|
2332
2467
|
await this.adapter.sendText(target, this.t.lockRenewed(timeoutMin));
|
|
2333
2468
|
}
|
|
2334
2469
|
/**
|
|
2335
|
-
* Export conversation history
|
|
2470
|
+
* Export conversation history as Markdown (a future PDF pipeline can be
|
|
2471
|
+
* added here; `/export pdf` currently falls back to Markdown).
|
|
2336
2472
|
*/
|
|
2337
2473
|
async handleExport(target, format) {
|
|
2338
2474
|
const agent = this.agent;
|
|
@@ -2340,12 +2476,6 @@ export class AgentRunner {
|
|
|
2340
2476
|
await this.adapter.sendText(target, this.t.exportNoSession);
|
|
2341
2477
|
return;
|
|
2342
2478
|
}
|
|
2343
|
-
// Default to markdown if not specified
|
|
2344
|
-
const exportFormat = format ?? "markdown";
|
|
2345
|
-
if (exportFormat === "pdf") {
|
|
2346
|
-
await this.adapter.sendText(target, this.t.exportPdfNotSupported);
|
|
2347
|
-
return;
|
|
2348
|
-
}
|
|
2349
2479
|
try {
|
|
2350
2480
|
const markdown = this.generateMarkdown(agent);
|
|
2351
2481
|
const fileName = `conversation-${agent.id}-${Date.now()}.md`;
|
|
@@ -2414,54 +2544,35 @@ export class AgentRunner {
|
|
|
2414
2544
|
*/
|
|
2415
2545
|
canWrite(channel) {
|
|
2416
2546
|
const binding = this.bindings.get(this.channel, this.chatKey);
|
|
2417
|
-
if (binding
|
|
2418
|
-
return true; // No
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2547
|
+
if (binding === undefined)
|
|
2548
|
+
return true; // No binding, free access
|
|
2549
|
+
if (lockCanWrite(binding, channel)) {
|
|
2550
|
+
// A timed-out lock counts as free; release it so the next acquire sees a clean state.
|
|
2551
|
+
if (lockIsTimedOut(binding))
|
|
2552
|
+
this.releaseTimeoutLock(binding);
|
|
2422
2553
|
return true;
|
|
2423
2554
|
}
|
|
2424
|
-
return
|
|
2555
|
+
return false;
|
|
2425
2556
|
}
|
|
2426
2557
|
/**
|
|
2427
2558
|
* Acquire session lock for the given channel.
|
|
2428
2559
|
* Returns true if lock acquired, false if already locked by another channel.
|
|
2429
2560
|
*/
|
|
2430
2561
|
acquireLock(channel) {
|
|
2431
|
-
|
|
2562
|
+
let binding = this.bindings.get(this.channel, this.chatKey);
|
|
2432
2563
|
if (binding === undefined)
|
|
2433
2564
|
return true;
|
|
2434
|
-
//
|
|
2435
|
-
|
|
2565
|
+
// A timed-out lock is treated as free: release it (with the user notice)
|
|
2566
|
+
// before acquiring so the next state is clean.
|
|
2567
|
+
if (lockIsTimedOut(binding)) {
|
|
2436
2568
|
this.releaseTimeoutLock(binding);
|
|
2437
|
-
|
|
2438
|
-
const refreshed = this.bindings.get(this.channel, this.chatKey);
|
|
2439
|
-
if (refreshed !== undefined && (refreshed.lockOwner === undefined || refreshed.lockOwner === channel)) {
|
|
2440
|
-
const defaultTimeoutMs = refreshed.lockTimeoutMs ?? 5 * 60 * 1000;
|
|
2441
|
-
this.bindings.put({
|
|
2442
|
-
...refreshed,
|
|
2443
|
-
lockOwner: channel,
|
|
2444
|
-
lockAcquiredAt: Date.now(),
|
|
2445
|
-
lockTimeoutMs: defaultTimeoutMs,
|
|
2446
|
-
lastActiveAt: Date.now()
|
|
2447
|
-
});
|
|
2448
|
-
return true;
|
|
2449
|
-
}
|
|
2569
|
+
binding = this.bindings.get(this.channel, this.chatKey) ?? binding;
|
|
2450
2570
|
}
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
lockOwner: channel,
|
|
2457
|
-
lockAcquiredAt: Date.now(),
|
|
2458
|
-
lockTimeoutMs: timeoutMs,
|
|
2459
|
-
lastActiveAt: Date.now()
|
|
2460
|
-
});
|
|
2461
|
-
return true;
|
|
2462
|
-
}
|
|
2463
|
-
// Locked by other channel - queue the message if from Web
|
|
2464
|
-
return false;
|
|
2571
|
+
const next = lockAcquire(binding, channel);
|
|
2572
|
+
if (next === undefined)
|
|
2573
|
+
return false; // Locked by another channel
|
|
2574
|
+
this.bindings.put({ ...next, lastActiveAt: Date.now() });
|
|
2575
|
+
return true;
|
|
2465
2576
|
}
|
|
2466
2577
|
/**
|
|
2467
2578
|
* Queue a message for later execution when lock is released.
|
|
@@ -2479,6 +2590,8 @@ export class AgentRunner {
|
|
|
2479
2590
|
replyRef: msg.replyRef,
|
|
2480
2591
|
images: msg.images,
|
|
2481
2592
|
files: msg.files,
|
|
2593
|
+
// Keep the true source channel so replay routes to the right runner.
|
|
2594
|
+
channel: msg.channel,
|
|
2482
2595
|
});
|
|
2483
2596
|
this.bindings.put({ ...binding, queuedMessages: queued });
|
|
2484
2597
|
return queued.length;
|
|
@@ -2496,69 +2609,64 @@ export class AgentRunner {
|
|
|
2496
2609
|
const count = messages.length;
|
|
2497
2610
|
// Clear queue immediately to prevent duplicate processing
|
|
2498
2611
|
this.bindings.put({ ...binding, queuedMessages: [] });
|
|
2499
|
-
// Notify about queued messages being processed
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2612
|
+
// Notify about queued messages being processed.
|
|
2613
|
+
await this.adapter
|
|
2614
|
+
.sendText({ chatKey: this.chatKey, chatType: this.chatType }, this.t.queueProcessed(count))
|
|
2615
|
+
.catch(() => undefined);
|
|
2616
|
+
// Replay each queued message through the service routing so it lands in
|
|
2617
|
+
// the runner of its own channel (a web message goes to the web runner,
|
|
2618
|
+
// never the releasing feishu runner). Without a router we fall back to
|
|
2619
|
+
// the local queue so behavior stays functional in isolation.
|
|
2620
|
+
for (const queued of messages) {
|
|
2621
|
+
const inboundMsg = {
|
|
2622
|
+
channel: queued.channel ?? "web",
|
|
2623
|
+
chatKey: this.chatKey,
|
|
2624
|
+
chatType: this.chatType,
|
|
2625
|
+
senderKey: queued.senderKey,
|
|
2626
|
+
text: queued.text,
|
|
2627
|
+
replyRef: queued.replyRef,
|
|
2628
|
+
images: queued.images,
|
|
2629
|
+
files: queued.files,
|
|
2630
|
+
};
|
|
2631
|
+
if (this.requeue !== undefined) {
|
|
2632
|
+
this.requeue(inboundMsg);
|
|
2633
|
+
}
|
|
2634
|
+
else {
|
|
2519
2635
|
this.queue.push(inboundMsg);
|
|
2520
2636
|
}
|
|
2521
|
-
// Trigger processing if not already running
|
|
2522
|
-
void this.drain();
|
|
2523
2637
|
}
|
|
2638
|
+
if (this.requeue === undefined)
|
|
2639
|
+
void this.drain();
|
|
2524
2640
|
}
|
|
2525
|
-
/**
|
|
2526
|
-
|
|
2641
|
+
/**
|
|
2642
|
+
* Release session lock. Awaits the queued-message drain, then clears the
|
|
2643
|
+
* lock from a FRESH read: the drain's queue-clearing put must not be
|
|
2644
|
+
* overwritten by a stale binding object (which would resurrect the queue
|
|
2645
|
+
* and cause duplicate processing on the next release).
|
|
2646
|
+
*/
|
|
2647
|
+
async releaseLock() {
|
|
2527
2648
|
const binding = this.bindings.get(this.channel, this.chatKey);
|
|
2528
|
-
if (binding
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
}
|
|
2534
|
-
/** Check if lock has timed out. */
|
|
2535
|
-
isLockTimedOut(binding) {
|
|
2536
|
-
if (binding.lockOwner === undefined || binding.lockAcquiredAt === undefined) {
|
|
2537
|
-
return false;
|
|
2538
|
-
}
|
|
2539
|
-
const timeoutMs = binding.lockTimeoutMs ?? 5 * 60 * 1000; // Default 5 minutes
|
|
2540
|
-
const elapsed = Date.now() - binding.lockAcquiredAt;
|
|
2541
|
-
return elapsed > timeoutMs;
|
|
2649
|
+
if (binding === undefined || binding.lockOwner === undefined)
|
|
2650
|
+
return;
|
|
2651
|
+
await this.processQueuedMessages();
|
|
2652
|
+
const fresh = this.bindings.get(this.channel, this.chatKey) ?? binding;
|
|
2653
|
+
this.bindings.put(lockRelease(fresh));
|
|
2542
2654
|
}
|
|
2543
|
-
/** Check and release timed-out locks. */
|
|
2655
|
+
/** Check and release timed-out locks (delegates to the pure lock module). */
|
|
2544
2656
|
checkAndReleaseTimeoutLock() {
|
|
2545
2657
|
const binding = this.bindings.get(this.channel, this.chatKey);
|
|
2546
|
-
if (binding &&
|
|
2658
|
+
if (binding !== undefined && lockIsTimedOut(binding)) {
|
|
2547
2659
|
this.releaseTimeoutLock(binding);
|
|
2548
2660
|
}
|
|
2549
2661
|
}
|
|
2550
2662
|
/** Release a timed-out lock and notify. */
|
|
2551
2663
|
releaseTimeoutLock(binding) {
|
|
2552
|
-
const timeoutMin = Math.round((binding.lockTimeoutMs ??
|
|
2664
|
+
const timeoutMin = Math.round((binding.lockTimeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS) / 60000);
|
|
2553
2665
|
const lockedBy = binding.lockOwner;
|
|
2554
|
-
this.bindings.put(
|
|
2555
|
-
...binding,
|
|
2556
|
-
lockOwner: undefined,
|
|
2557
|
-
lockAcquiredAt: undefined
|
|
2558
|
-
});
|
|
2666
|
+
this.bindings.put(lockRelease(binding));
|
|
2559
2667
|
// Notify users about timeout release
|
|
2560
2668
|
const adapter = this.adapters?.get(this.channel);
|
|
2561
|
-
if (adapter && lockedBy) {
|
|
2669
|
+
if (adapter !== undefined && lockedBy !== undefined) {
|
|
2562
2670
|
void adapter.sendText({ chatKey: this.chatKey, chatType: this.chatType }, this.t.lockTimeoutReleased(timeoutMin)).catch(() => undefined);
|
|
2563
2671
|
}
|
|
2564
2672
|
}
|