@goodandready/dsh-plugin-notify 0.3.3 → 0.3.5
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/CHANGELOG.md +12 -1
- package/lib/index.js +71 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,18 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## 0.3.
|
|
7
|
+
## [0.3.5] - 2026-09-24
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- **Memory Leak in Turn Tracking (#32)**: Added TTL-based cleanup (30m max age) for stale `turnStarts` entries with automatic pruning on turn events.
|
|
11
|
+
- **SSE Connection Drops on Reverse Proxies (#33)**: Added periodic 25s keepalive ping comments (`: ping\n\n`) on `/dsh-plugin-notify/events` with unreferenced timer and automatic dead socket removal.
|
|
12
|
+
- **Reverse Iteration in Session Summarization (#34)**: Optimized `summarizeTurn` from O(N) forward iteration to reverse iteration with early break once the turn assistant message is found.
|
|
13
|
+
- **String Content Handling in textOf (#35)**: Fixed `textOf(content)` to properly return string content when passed a primitive string or strings within content arrays.
|
|
14
|
+
|
|
15
|
+
### Documented
|
|
16
|
+
- **Architectural Exception for Client Bundle (#36)**: Formally documented the conscious >600-line monolithic bundle exception for `lib/client.js` in `docs/design/DESIGN.md` in accordance with DSH `ModuleLoader` architecture.
|
|
17
|
+
|
|
18
|
+
## [0.3.3] - 2026-09-24
|
|
8
19
|
|
|
9
20
|
### Fixed
|
|
10
21
|
- The settings card no longer waits for the removed `settingsScope` service. It uses `configForms` on current DeepSeek Harness (#37).
|
package/lib/index.js
CHANGED
|
@@ -52,9 +52,24 @@ function isExcludedSession(sessionId, prefixes) {
|
|
|
52
52
|
|
|
53
53
|
const DEFAULT_EVENTS = ['task_done', 'error', 'approval_requested'];
|
|
54
54
|
/** Per-session last `turn/start` epoch ms, for turn-duration reporting. */
|
|
55
|
-
const turnStarts = new Map();
|
|
55
|
+
export const turnStarts = new Map();
|
|
56
56
|
const warnedLegacyUrls = new Set();
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Purge turnStarts entries older than maxAgeMs (default 2 hours) to prevent
|
|
60
|
+
* memory leaks from abandoned or cancelled sessions.
|
|
61
|
+
*/
|
|
62
|
+
export function cleanupTurnStarts(maxAgeMs = 7200000, now = Date.now()) {
|
|
63
|
+
let deleted = 0;
|
|
64
|
+
for (const [sid, start] of turnStarts.entries()) {
|
|
65
|
+
if (now - start > maxAgeMs) {
|
|
66
|
+
turnStarts.delete(sid);
|
|
67
|
+
deleted++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return deleted;
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
/** Resolve a config value to a webhook URL: credential ref (preferred), env fallback, or legacy raw URL. */
|
|
59
74
|
export async function resolveWebhookValue(ctx, refOrUrl) {
|
|
60
75
|
if (!refOrUrl || typeof refOrUrl !== 'string') return '';
|
|
@@ -87,7 +102,21 @@ export async function resolveWebhooks(ctx, webhooks = {}) {
|
|
|
87
102
|
return out;
|
|
88
103
|
}
|
|
89
104
|
|
|
90
|
-
const sseClients = new Set();
|
|
105
|
+
export const sseClients = new Set();
|
|
106
|
+
|
|
107
|
+
export function sendSseHeartbeat() {
|
|
108
|
+
if (sseClients.size === 0) return 0;
|
|
109
|
+
let sent = 0;
|
|
110
|
+
for (const res of sseClients) {
|
|
111
|
+
try {
|
|
112
|
+
res.write(': ping\n\n');
|
|
113
|
+
sent++;
|
|
114
|
+
} catch {
|
|
115
|
+
sseClients.delete(res);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return sent;
|
|
119
|
+
}
|
|
91
120
|
|
|
92
121
|
function isLoopback(address) {
|
|
93
122
|
if (!address) return false;
|
|
@@ -179,10 +208,17 @@ function mountWebServer(targetCtx) {
|
|
|
179
208
|
path: '/dsh-plugin-notify/events',
|
|
180
209
|
handler: (req, res) => handleSseConnection(req, res),
|
|
181
210
|
};
|
|
211
|
+
const startHeartbeat = () => {
|
|
212
|
+
const timer = setInterval(() => sendSseHeartbeat(), 25000);
|
|
213
|
+
timer?.unref?.();
|
|
214
|
+
return () => clearInterval(timer);
|
|
215
|
+
};
|
|
182
216
|
if (typeof targetCtx.effect === 'function') {
|
|
183
217
|
targetCtx.effect(() => targetCtx.webServer.register(routeDef), 'dsh-plugin-notify: sse events');
|
|
218
|
+
targetCtx.effect(startHeartbeat, 'dsh-plugin-notify: sse heartbeat');
|
|
184
219
|
} else {
|
|
185
220
|
targetCtx.webServer.register(routeDef);
|
|
221
|
+
startHeartbeat();
|
|
186
222
|
}
|
|
187
223
|
}
|
|
188
224
|
|
|
@@ -247,6 +283,9 @@ export function apply(ctx, config = {}) {
|
|
|
247
283
|
const excludeSessionPrefixes = cfg.excludeSessionPrefixes ?? [];
|
|
248
284
|
|
|
249
285
|
if (event.type === 'turn/start') {
|
|
286
|
+
if (turnStarts.size > 100) {
|
|
287
|
+
cleanupTurnStarts();
|
|
288
|
+
}
|
|
250
289
|
turnStarts.set(String(session.id), Date.now());
|
|
251
290
|
return;
|
|
252
291
|
}
|
|
@@ -388,10 +427,14 @@ function reasonLabel(reason) {
|
|
|
388
427
|
}
|
|
389
428
|
}
|
|
390
429
|
|
|
391
|
-
function textOf(content) {
|
|
430
|
+
export function textOf(content) {
|
|
431
|
+
if (typeof content === 'string') return content;
|
|
432
|
+
if (!Array.isArray(content)) return '';
|
|
392
433
|
let out = '';
|
|
393
434
|
for (const block of content) {
|
|
394
|
-
if (typeof block === '
|
|
435
|
+
if (typeof block === 'string') {
|
|
436
|
+
out += block;
|
|
437
|
+
} else if (typeof block === 'object' && block !== null && block.type === 'text') {
|
|
395
438
|
const text = block.text;
|
|
396
439
|
if (typeof text === 'string')
|
|
397
440
|
out += text;
|
|
@@ -400,27 +443,40 @@ function textOf(content) {
|
|
|
400
443
|
return out;
|
|
401
444
|
}
|
|
402
445
|
|
|
403
|
-
function sessionTitle(session) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
446
|
+
export function sessionTitle(session) {
|
|
447
|
+
const events = Array.isArray(session?.events) ? session.events : [];
|
|
448
|
+
for (const event of events) {
|
|
449
|
+
if (event && event.type === 'user/message') {
|
|
450
|
+
const text = textOf(event.data?.content).replace(/\s+/g, ' ').trim();
|
|
407
451
|
if (text)
|
|
408
452
|
return text.length > 60 ? `${text.slice(0, 60)}…` : text;
|
|
409
453
|
}
|
|
410
454
|
}
|
|
411
|
-
return String(session
|
|
455
|
+
return String(session?.id ?? 'session');
|
|
412
456
|
}
|
|
413
457
|
|
|
414
|
-
function summarizeTurn(session, turn) {
|
|
458
|
+
export function summarizeTurn(session, turn) {
|
|
459
|
+
const events = Array.isArray(session?.events) ? session.events : [];
|
|
415
460
|
let toolCalls = 0;
|
|
416
461
|
let lastText = '';
|
|
417
|
-
for
|
|
418
|
-
|
|
462
|
+
// Reverse iteration: events for current turn are at the end of the events array
|
|
463
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
464
|
+
const event = events[i];
|
|
465
|
+
if (!event || !event.data) continue;
|
|
466
|
+
if (event.data.turn !== turn) {
|
|
467
|
+
if (typeof event.data.turn === 'number' && event.data.turn < turn) {
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (event.type === 'tool/call') {
|
|
419
473
|
toolCalls += 1;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
474
|
+
}
|
|
475
|
+
if (event.type === 'assistant/message' && !lastText) {
|
|
476
|
+
const text = textOf(event.data.message?.content);
|
|
477
|
+
if (text) {
|
|
423
478
|
lastText = text;
|
|
479
|
+
}
|
|
424
480
|
}
|
|
425
481
|
}
|
|
426
482
|
const parts = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-plugin-notify",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "DSH plugin: audio chimes, cross-session toasts, desktop push, and IM webhooks for turn completion, errors, and approvals.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|