nightytidy 0.2.8 → 0.2.10
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/package.json +1 -1
- package/src/agent/cli-bridge.js +29 -7
- package/src/agent/index.js +40 -16
package/package.json
CHANGED
package/src/agent/cli-bridge.js
CHANGED
|
@@ -76,7 +76,7 @@ export class CliBridge {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
_run(args, onOutput, opts = {}) {
|
|
79
|
-
return new Promise((resolve
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
80
|
const binPath = path.resolve(import.meta.dirname, '../../bin/nightytidy.js');
|
|
81
81
|
const proc = spawn('node', [binPath, ...args], {
|
|
82
82
|
cwd: this.projectDir,
|
|
@@ -87,15 +87,40 @@ export class CliBridge {
|
|
|
87
87
|
let stdout = '';
|
|
88
88
|
let stderr = '';
|
|
89
89
|
let killed = false;
|
|
90
|
+
let settled = false;
|
|
91
|
+
|
|
92
|
+
const settle = (result) => {
|
|
93
|
+
if (settled) return;
|
|
94
|
+
settled = true;
|
|
95
|
+
if (timer) clearTimeout(timer);
|
|
96
|
+
if (killTimer) clearTimeout(killTimer);
|
|
97
|
+
resolve(result);
|
|
98
|
+
};
|
|
90
99
|
|
|
91
100
|
// Timeout — kill the process if it takes too long
|
|
92
101
|
let timer = null;
|
|
102
|
+
let killTimer = null;
|
|
93
103
|
if (opts.timeout) {
|
|
94
104
|
timer = setTimeout(() => {
|
|
95
105
|
killed = true;
|
|
96
106
|
const timeoutSec = Math.round(opts.timeout / 1000);
|
|
97
107
|
warn(`CLI process timed out after ${timeoutSec}s: ${args.join(' ')}`);
|
|
98
108
|
this.kill();
|
|
109
|
+
// On Windows, taskkill is fire-and-forget — the 'close' event may
|
|
110
|
+
// never fire. Force-resolve after 5s to prevent the agent from
|
|
111
|
+
// hanging forever.
|
|
112
|
+
killTimer = setTimeout(() => {
|
|
113
|
+
warn(`CLI process did not exit within 5s after kill — force-resolving`);
|
|
114
|
+
this.activeProcess = null;
|
|
115
|
+
settle({
|
|
116
|
+
success: false,
|
|
117
|
+
exitCode: -1,
|
|
118
|
+
stdout,
|
|
119
|
+
stderr: `Process timed out after ${timeoutSec}s — Claude Code may be unavailable`,
|
|
120
|
+
parsed: CliBridge.parseOutput(stdout),
|
|
121
|
+
timedOut: true,
|
|
122
|
+
});
|
|
123
|
+
}, 5000);
|
|
99
124
|
}, opts.timeout);
|
|
100
125
|
}
|
|
101
126
|
|
|
@@ -127,26 +152,23 @@ export class CliBridge {
|
|
|
127
152
|
});
|
|
128
153
|
|
|
129
154
|
proc.on('close', (code) => {
|
|
130
|
-
if (timer) clearTimeout(timer);
|
|
131
155
|
this.activeProcess = null;
|
|
132
|
-
|
|
133
|
-
resolve({
|
|
156
|
+
settle({
|
|
134
157
|
success: code === 0 && !killed,
|
|
135
158
|
exitCode: code,
|
|
136
159
|
stdout,
|
|
137
160
|
stderr: killed
|
|
138
161
|
? `Process timed out after ${Math.round(opts.timeout / 1000)}s — Claude Code may be unavailable`
|
|
139
162
|
: stderr,
|
|
140
|
-
parsed,
|
|
163
|
+
parsed: CliBridge.parseOutput(stdout),
|
|
141
164
|
timedOut: killed,
|
|
142
165
|
});
|
|
143
166
|
});
|
|
144
167
|
|
|
145
168
|
proc.on('error', (err) => {
|
|
146
|
-
if (timer) clearTimeout(timer);
|
|
147
169
|
this.activeProcess = null;
|
|
148
170
|
logError(`CLI process error: ${err.message}`);
|
|
149
|
-
|
|
171
|
+
settle({
|
|
150
172
|
success: false,
|
|
151
173
|
exitCode: -1,
|
|
152
174
|
stdout,
|
package/src/agent/index.js
CHANGED
|
@@ -378,6 +378,12 @@ export async function startAgent() {
|
|
|
378
378
|
|
|
379
379
|
case 'cancel-queued': {
|
|
380
380
|
runQueue.cancel(msg.runId);
|
|
381
|
+
// Also notify Firestore — the run may exist there even if not in local queue
|
|
382
|
+
// (e.g. orphaned after a timeout/crash where the agent already discarded it)
|
|
383
|
+
dispatchWithQueue('run_failed', {
|
|
384
|
+
projectId: msg.projectId || '',
|
|
385
|
+
run: { id: msg.runId },
|
|
386
|
+
}, []);
|
|
381
387
|
wsServer.broadcast({ type: 'queue-updated', queue: runQueue.getQueue() });
|
|
382
388
|
reply({ type: 'queue-updated', queue: runQueue.getQueue() });
|
|
383
389
|
break;
|
|
@@ -1080,22 +1086,35 @@ export async function startAgent() {
|
|
|
1080
1086
|
const progress = interrupted.lastProgress || {};
|
|
1081
1087
|
const completed = progress.completedCount || 0;
|
|
1082
1088
|
const total = interrupted.steps?.length || 0;
|
|
1083
|
-
info(`Found interrupted run: ${projName} (${completed}/${total} steps completed)`);
|
|
1084
|
-
info(` Run ID: ${interrupted.id}`);
|
|
1085
|
-
info(` Use the web app to Resume, Finish with Partial Results, or Discard`);
|
|
1086
1089
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
totalCost: progress.totalCost || 0,
|
|
1097
|
-
},
|
|
1090
|
+
if (completed === 0) {
|
|
1091
|
+
// Run never actually started (init timed out or crashed before any steps).
|
|
1092
|
+
// Auto-discard — there's nothing to resume or finish, and blocking the
|
|
1093
|
+
// queue for user action is pointless.
|
|
1094
|
+
info(`Auto-discarding interrupted run with 0 completed steps: ${projName} (${interrupted.id})`);
|
|
1095
|
+
runQueue.clearInterrupted();
|
|
1096
|
+
dispatchWithQueue('run_failed', {
|
|
1097
|
+
projectId: proj?.id || interrupted.projectId,
|
|
1098
|
+
run: { id: interrupted.id },
|
|
1098
1099
|
}, []);
|
|
1100
|
+
} else {
|
|
1101
|
+
info(`Found interrupted run: ${projName} (${completed}/${total} steps completed)`);
|
|
1102
|
+
info(` Run ID: ${interrupted.id}`);
|
|
1103
|
+
info(` Use the web app to Resume, Finish with Partial Results, or Discard`);
|
|
1104
|
+
|
|
1105
|
+
// Best-effort: notify Firestore that this run is interrupted
|
|
1106
|
+
// (in case the shutdown webhook didn't make it)
|
|
1107
|
+
if (proj) {
|
|
1108
|
+
dispatchWithQueue('run_interrupted', {
|
|
1109
|
+
projectId: proj.id,
|
|
1110
|
+
run: {
|
|
1111
|
+
id: interrupted.id,
|
|
1112
|
+
completedSteps: completed,
|
|
1113
|
+
failedSteps: progress.failedCount || 0,
|
|
1114
|
+
totalCost: progress.totalCost || 0,
|
|
1115
|
+
},
|
|
1116
|
+
}, []);
|
|
1117
|
+
}
|
|
1099
1118
|
}
|
|
1100
1119
|
}
|
|
1101
1120
|
|
|
@@ -1103,8 +1122,13 @@ export async function startAgent() {
|
|
|
1103
1122
|
// (agent died without graceful shutdown, so markInterrupted was never called)
|
|
1104
1123
|
const current = runQueue.getCurrent();
|
|
1105
1124
|
if (current && current.status === 'running' && !activeBridge) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1125
|
+
// Orphaned run with no progress — auto-discard instead of blocking the queue
|
|
1126
|
+
info(`Found orphaned running run: ${current.id} — auto-discarding (0 steps completed)`);
|
|
1127
|
+
runQueue.completeCurrent({ success: false });
|
|
1128
|
+
dispatchWithQueue('run_failed', {
|
|
1129
|
+
projectId: current.projectId,
|
|
1130
|
+
run: { id: current.id },
|
|
1131
|
+
}, []);
|
|
1108
1132
|
}
|
|
1109
1133
|
|
|
1110
1134
|
// Process any queued runs left from a previous session
|