@ddtcorex/dsh-maestro-supervisor 0.7.5 → 0.7.7
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/plugin.d.ts +3 -0
- package/lib/plugin.js +28 -3
- package/lib/resume-log.d.ts +23 -0
- package/lib/resume-log.js +18 -0
- package/lib/resume.js +5 -1
- package/package.json +1 -1
- package/skills/dsh-safe-restart/scripts/restart-dsh-web.sh +9 -1
package/lib/plugin.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { findInterrupted as defaultFindInterrupted, findDanglingOpenTurns as defaultFindDanglingOpenTurns } from './resume.js';
|
|
9
9
|
import type { RestartIntent } from './intents.js';
|
|
10
|
+
import { type ResumeLogEntry } from './resume-log.js';
|
|
10
11
|
import { runSessionHealthCheck } from './session-health.js';
|
|
11
12
|
export * from './resume-tools.js';
|
|
12
13
|
export declare const inject: readonly ["sessions", "agents", "connection", "tools", "skills"];
|
|
@@ -69,6 +70,7 @@ export declare function runAutoResume(ctx: any, opts?: {
|
|
|
69
70
|
findInterrupted?: typeof defaultFindInterrupted;
|
|
70
71
|
findDanglingOpenTurns?: typeof defaultFindDanglingOpenTurns;
|
|
71
72
|
resumeInterrupted?: typeof resumeInterrupted;
|
|
73
|
+
logResume?: (entry: ResumeLogEntry) => void;
|
|
72
74
|
config?: SupervisorPluginConfig;
|
|
73
75
|
}): Promise<void>;
|
|
74
76
|
export declare function resumeInterrupted(ctx: any, ids: string[], deps?: {
|
|
@@ -78,6 +80,7 @@ export declare function resumeInterrupted(ctx: any, ids: string[], deps?: {
|
|
|
78
80
|
resolveToolScope?: ToolScopeResolver;
|
|
79
81
|
notify?: (line: string) => Promise<void>;
|
|
80
82
|
injectSessionMessage?: (sessionId: string, content: string) => unknown;
|
|
83
|
+
logResume?: (entry: ResumeLogEntry) => void;
|
|
81
84
|
config?: SupervisorPluginConfig;
|
|
82
85
|
}): Promise<string[]>;
|
|
83
86
|
export declare function createResumeRpcHandler(ctx: any, opts?: {
|
package/lib/plugin.js
CHANGED
|
@@ -11,6 +11,7 @@ import * as os from 'node:os';
|
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { findInterrupted as defaultFindInterrupted, findDanglingOpenTurns as defaultFindDanglingOpenTurns } from './resume.js';
|
|
13
13
|
import { readIntent, consumeIntent } from './intents.js';
|
|
14
|
+
import { appendResumeLog } from './resume-log.js';
|
|
14
15
|
import { makeSkillProvider } from './skill-provider.js';
|
|
15
16
|
import { registerRestartTool } from './restart-tool.js';
|
|
16
17
|
import { makePreExecuteGuard } from './self-kill-guard.js';
|
|
@@ -214,8 +215,13 @@ export async function runAutoResume(ctx, opts = {}) {
|
|
|
214
215
|
const doFind = opts.findInterrupted ?? defaultFindInterrupted;
|
|
215
216
|
const doFindDangling = opts.findDanglingOpenTurns ?? defaultFindDanglingOpenTurns;
|
|
216
217
|
const doResume = opts.resumeInterrupted ?? resumeInterrupted;
|
|
218
|
+
const doLog = opts.logResume ?? ((entry) => { try {
|
|
219
|
+
appendResumeLog(entry);
|
|
220
|
+
}
|
|
221
|
+
catch { } });
|
|
217
222
|
if (!getAutoResumeEnabled(opts?.config)) {
|
|
218
223
|
ctx.logger?.info?.('[supervisor] auto-resume disabled — skip');
|
|
224
|
+
doLog({ ts: Date.now(), kind: 'scan', scanned: 0, interrupted: [], detail: 'auto-resume disabled' });
|
|
219
225
|
return;
|
|
220
226
|
}
|
|
221
227
|
const withinMs = getResumeWithinMs(opts?.config);
|
|
@@ -233,12 +239,16 @@ export async function runAutoResume(ctx, opts = {}) {
|
|
|
233
239
|
ctx.logger?.warn?.(`[supervisor] auto-resume: dangling-open-turn scan failed, continuing with closed-turn results only: ${e?.message ?? String(e)}`);
|
|
234
240
|
}
|
|
235
241
|
const merged = Array.from(new Set([...interrupted, ...dangling]));
|
|
242
|
+
doLog({ ts: Date.now(), kind: 'scan', scanned, interrupted: merged, detail: `withinMs=${withinMs}` });
|
|
236
243
|
if (!merged.length) {
|
|
237
244
|
ctx.logger?.info?.(`[supervisor] auto-resume: 0/${scanned} interrupted within ${withinMs}ms — nothing to do`);
|
|
238
245
|
return;
|
|
239
246
|
}
|
|
240
247
|
ctx.logger?.info?.(`[supervisor] auto-resume: ${merged.length}/${scanned} interrupted within ${withinMs}ms: ${merged.slice(0, 3).join(', ')}`);
|
|
241
|
-
await doResume(ctx, merged, {
|
|
248
|
+
await doResume(ctx, merged, {
|
|
249
|
+
...(opts?.config !== undefined ? { config: opts.config } : {}),
|
|
250
|
+
...(opts.logResume !== undefined ? { logResume: opts.logResume } : {}),
|
|
251
|
+
});
|
|
242
252
|
}
|
|
243
253
|
catch (e) {
|
|
244
254
|
try {
|
|
@@ -252,6 +262,10 @@ export async function resumeInterrupted(ctx, ids, deps = {}) {
|
|
|
252
262
|
const doConsumeIntent = deps.consumeIntent ?? consumeIntent;
|
|
253
263
|
const doProbe = deps.probeToolView ?? probeToolView;
|
|
254
264
|
const doResolveToolScope = deps.resolveToolScope ?? defaultResolveToolScope;
|
|
265
|
+
const doLog = deps.logResume ?? ((entry) => { try {
|
|
266
|
+
appendResumeLog(entry);
|
|
267
|
+
}
|
|
268
|
+
catch { } });
|
|
255
269
|
const coreToolPolicy = getResumeCoreToolPolicy(deps.config);
|
|
256
270
|
const resumed = [];
|
|
257
271
|
for (const id of ids) {
|
|
@@ -286,6 +300,7 @@ export async function resumeInterrupted(ctx, ids, deps = {}) {
|
|
|
286
300
|
}
|
|
287
301
|
if (typeof agent?.followup !== 'function') {
|
|
288
302
|
ctx.logger?.warn?.(`[supervisor] auto-resume: no live agent available for ${id}`);
|
|
303
|
+
doLog({ ts: Date.now(), sessionId, kind: 'no-agent', detail: 'agents.resume returned no followup-capable handle' });
|
|
289
304
|
continue;
|
|
290
305
|
}
|
|
291
306
|
// Ensure bash tool is registered before followup — initial resume header with
|
|
@@ -337,10 +352,13 @@ export async function resumeInterrupted(ctx, ids, deps = {}) {
|
|
|
337
352
|
// message instead of the generic "outcome unknown" recovery prompt, then
|
|
338
353
|
// consume the sidecar so it cannot re-trigger on a later resume.
|
|
339
354
|
let resumeMessage = idleMessage;
|
|
355
|
+
let intentReason = '';
|
|
340
356
|
try {
|
|
341
357
|
const intent = doReadIntent(sessionId);
|
|
342
|
-
if (intent)
|
|
343
|
-
|
|
358
|
+
if (intent) {
|
|
359
|
+
intentReason = intent.reason ?? '';
|
|
360
|
+
resumeMessage = `You requested a dsh web restart${intentReason ? ` (reason: ${intentReason})` : ''} and it completed. Do NOT call dsh_web_restart again. Verify current state if needed, then continue the original task.`;
|
|
361
|
+
}
|
|
344
362
|
}
|
|
345
363
|
catch { }
|
|
346
364
|
agent.followup(createUserMessage({
|
|
@@ -354,6 +372,12 @@ export async function resumeInterrupted(ctx, ids, deps = {}) {
|
|
|
354
372
|
catch { }
|
|
355
373
|
resumed.push(id);
|
|
356
374
|
ctx.logger?.info?.(`[supervisor] auto-resume: sent recovery continue for ${id}`);
|
|
375
|
+
doLog({
|
|
376
|
+
ts: Date.now(),
|
|
377
|
+
sessionId,
|
|
378
|
+
kind: 'resumed',
|
|
379
|
+
detail: resumeMessage === idleMessage ? 'idle-recovery-prompt' : `restart-intent:${intentReason.slice(0, 100)}`,
|
|
380
|
+
});
|
|
357
381
|
// C1 observability probe — snapshot the resumed session's SCOPED tool
|
|
358
382
|
// view at the success point so a post-resume bash loss surfaces in the
|
|
359
383
|
// journal the moment it happens (Part D reads this line). Defensive:
|
|
@@ -379,6 +403,7 @@ export async function resumeInterrupted(ctx, ids, deps = {}) {
|
|
|
379
403
|
}
|
|
380
404
|
catch (e) {
|
|
381
405
|
ctx.logger?.warn?.(`[supervisor] auto-resume failed ${id}: ${e?.message ?? String(e)}`);
|
|
406
|
+
doLog({ ts: Date.now(), sessionId: String(id).split('/').pop() ?? String(id), kind: 'resume-failed', error: e?.message ?? String(e) });
|
|
382
407
|
}
|
|
383
408
|
}
|
|
384
409
|
return resumed;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable out-of-band resume log (`~/.dsh/.supervisor/resume.log.jsonl`).
|
|
3
|
+
*
|
|
4
|
+
* The in-tree supervisor's `ctx.logger` output does not reach `dsh-web.log`
|
|
5
|
+
* (it goes to the per-session console), which made every `agents.resume`
|
|
6
|
+
* failure invisible on real machines — the 2026-09-02 investigation only
|
|
7
|
+
* recovered the pattern ("last successful continue 12:00Z, then 5 restarts
|
|
8
|
+
* with none", `lastResumeProbe: null`, unconsumed intents) by scanning the
|
|
9
|
+
* session logs. This append-only JSONL sidecar gives a machine-local audit
|
|
10
|
+
* trail of resume outcomes that survives no matter where plugin logs land.
|
|
11
|
+
* Never throws: a write failure must not break the resume path.
|
|
12
|
+
*/
|
|
13
|
+
export interface ResumeLogEntry {
|
|
14
|
+
ts: number;
|
|
15
|
+
sessionId?: string;
|
|
16
|
+
kind: 'scan' | 'resume-failed' | 'resumed' | 'no-agent';
|
|
17
|
+
error?: string;
|
|
18
|
+
detail?: string;
|
|
19
|
+
scanned?: number;
|
|
20
|
+
interrupted?: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function resumeLogPath(): string;
|
|
23
|
+
export declare function appendResumeLog(entry: ResumeLogEntry): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, chmodSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
export function resumeLogPath() {
|
|
5
|
+
return join(homedir(), '.dsh', '.supervisor', 'resume.log.jsonl');
|
|
6
|
+
}
|
|
7
|
+
export function appendResumeLog(entry) {
|
|
8
|
+
try {
|
|
9
|
+
const p = resumeLogPath();
|
|
10
|
+
mkdirSync(join(homedir(), '.dsh', '.supervisor'), { recursive: true });
|
|
11
|
+
appendFileSync(p, JSON.stringify(entry) + '\n', 'utf8');
|
|
12
|
+
try {
|
|
13
|
+
chmodSync(p, 0o600);
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
}
|
|
17
|
+
catch { }
|
|
18
|
+
}
|
package/lib/resume.js
CHANGED
|
@@ -117,7 +117,11 @@ async function readSessionAllLines(zstdPath, jsonlPath, sinceMs) {
|
|
|
117
117
|
}
|
|
118
118
|
if (fs.existsSync(zstdPath)) {
|
|
119
119
|
const { execSync } = await import('node:child_process');
|
|
120
|
-
|
|
120
|
+
// maxBuffer must exceed the decompressed size of any real session log —
|
|
121
|
+
// worker sessions decode to 8-23MB while execSync's default 1MB would
|
|
122
|
+
// throw ENOBUFS and silently drop the session from every scan that needs
|
|
123
|
+
// the full file (findDanglingOpenTurns). Use 64MB to leave headroom.
|
|
124
|
+
const out = execSync(`zstd -d -c ${JSON.stringify(zstdPath)} 2>/dev/null`, { encoding: 'utf-8', maxBuffer: 64 * 1024 * 1024 });
|
|
121
125
|
return out.split('\n').filter(Boolean);
|
|
122
126
|
}
|
|
123
127
|
if (fs.existsSync(jsonlPath)) {
|
package/package.json
CHANGED
|
@@ -15,7 +15,15 @@ marker="${DSH_SUPERVISOR_MARKER:-$HOME/.dsh/.supervisor/planned-restart}"
|
|
|
15
15
|
# the script's own path — never a hard-coded location.
|
|
16
16
|
dsh_home="${DSH_HOME:-$HOME/.dsh}"
|
|
17
17
|
export SESSIONS_ROOT="${SESSIONS_ROOT:-$dsh_home/sessions}"
|
|
18
|
-
|
|
18
|
+
# Resolve the script's real location first: when invoked through a symlinked
|
|
19
|
+
# skill path (e.g. ~/.agents/skills/dsh-safe-restart/...), BASH_SOURCE keeps the
|
|
20
|
+
# link and "../../.." lands in ~/.agents — breaking the session-health preflight
|
|
21
|
+
# import (lib/session-health.js). readlink -f recovers the package checkout.
|
|
22
|
+
SCRIPT_SRC="${BASH_SOURCE[0]}"
|
|
23
|
+
if resolved="$(readlink -f "$SCRIPT_SRC" 2>/dev/null)" && [ -n "$resolved" ]; then
|
|
24
|
+
SCRIPT_SRC="$resolved"
|
|
25
|
+
fi
|
|
26
|
+
PLUGIN_DIR="${DSH_SUPERVISOR_PLUGIN_DIR:-$(cd "$(dirname "$SCRIPT_SRC")/../../.." && pwd)}"
|
|
19
27
|
confirmed=false
|
|
20
28
|
dry_run=false
|
|
21
29
|
auto_mode=false
|