@ddtcorex/dsh-maestro-supervisor 0.7.6 → 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 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, { ...(opts?.config !== undefined ? { config: opts.config } : {}) });
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
- resumeMessage = `You requested a dsh web restart${intent.reason ? ` (reason: ${intent.reason})` : ''} and it completed. Do NOT call dsh_web_restart again. Verify current state if needed, then continue the original task.`;
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
- const out = execSync(`zstd -d -c ${JSON.stringify(zstdPath)} 2>/dev/null`, { encoding: 'utf-8' });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ddtcorex/dsh-maestro-supervisor",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "Supervisor daemon for DSH Web resilience — auto-detect crashes, rollback to LKG, report",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",