@yemi33/squad 0.1.3 → 0.1.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/engine.js +50 -7
- package/package.json +1 -1
package/engine.js
CHANGED
|
@@ -128,10 +128,26 @@ function safeWrite(p, data) {
|
|
|
128
128
|
const tmp = p + '.tmp.' + process.pid;
|
|
129
129
|
try {
|
|
130
130
|
fs.writeFileSync(tmp, content);
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// Atomic rename — retry on Windows EPERM (file locking)
|
|
132
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
133
|
+
try {
|
|
134
|
+
fs.renameSync(tmp, p);
|
|
135
|
+
return;
|
|
136
|
+
} catch (e) {
|
|
137
|
+
if (e.code === 'EPERM' && attempt < 4) {
|
|
138
|
+
const delay = 50 * (attempt + 1); // 50, 100, 150, 200ms
|
|
139
|
+
const start = Date.now(); while (Date.now() - start < delay) {}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// Final attempt failed — fall through to direct write
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// All rename attempts failed — direct write as fallback (not atomic but won't lose data)
|
|
146
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
147
|
+
fs.writeFileSync(p, content);
|
|
148
|
+
} catch {
|
|
149
|
+
// Even direct write failed — clean up tmp silently
|
|
133
150
|
try { fs.unlinkSync(tmp); } catch {}
|
|
134
|
-
throw e;
|
|
135
151
|
}
|
|
136
152
|
}
|
|
137
153
|
|
|
@@ -837,6 +853,8 @@ function completeDispatch(id, result = 'success', reason = '') {
|
|
|
837
853
|
item.completed_at = ts();
|
|
838
854
|
item.result = result;
|
|
839
855
|
if (reason) item.reason = reason;
|
|
856
|
+
// Strip prompt from completed items (saves ~10KB per item, reduces file lock contention)
|
|
857
|
+
delete item.prompt;
|
|
840
858
|
dispatch.completed = dispatch.completed || [];
|
|
841
859
|
dispatch.completed.push(item);
|
|
842
860
|
// Keep last 100 completed
|
|
@@ -1146,13 +1164,22 @@ function getAdoToken() {
|
|
|
1146
1164
|
return null;
|
|
1147
1165
|
}
|
|
1148
1166
|
|
|
1149
|
-
async function adoFetch(url, token) {
|
|
1167
|
+
async function adoFetch(url, token, _retried = false) {
|
|
1150
1168
|
const res = await fetch(url, {
|
|
1151
1169
|
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }
|
|
1152
1170
|
});
|
|
1153
1171
|
if (!res.ok) throw new Error(`ADO API ${res.status}: ${res.statusText}`);
|
|
1154
1172
|
const text = await res.text();
|
|
1155
1173
|
if (!text || text.trimStart().startsWith('<')) {
|
|
1174
|
+
// Auth redirect — token likely expired. Invalidate cache and retry once.
|
|
1175
|
+
if (!_retried) {
|
|
1176
|
+
_adoTokenCache = { token: null, expiresAt: 0 };
|
|
1177
|
+
const freshToken = getAdoToken();
|
|
1178
|
+
if (freshToken) {
|
|
1179
|
+
log('info', 'ADO token expired mid-session — refreshed and retrying');
|
|
1180
|
+
return adoFetch(url, freshToken, true);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1156
1183
|
throw new Error(`ADO returned HTML instead of JSON (likely auth redirect) for ${url.split('?')[0]}`);
|
|
1157
1184
|
}
|
|
1158
1185
|
return JSON.parse(text);
|
|
@@ -2985,7 +3012,10 @@ async function tick() {
|
|
|
2985
3012
|
|
|
2986
3013
|
async function tickInner() {
|
|
2987
3014
|
const control = getControl();
|
|
2988
|
-
if (control.state !== 'running')
|
|
3015
|
+
if (control.state !== 'running') {
|
|
3016
|
+
log('info', `Engine state is "${control.state}" — exiting process`);
|
|
3017
|
+
process.exit(0);
|
|
3018
|
+
}
|
|
2989
3019
|
|
|
2990
3020
|
const config = getConfig();
|
|
2991
3021
|
tickCount++;
|
|
@@ -3058,8 +3088,16 @@ const commands = {
|
|
|
3058
3088
|
start() {
|
|
3059
3089
|
const control = getControl();
|
|
3060
3090
|
if (control.state === 'running') {
|
|
3061
|
-
|
|
3062
|
-
|
|
3091
|
+
// Check if the PID is actually alive
|
|
3092
|
+
let alive = false;
|
|
3093
|
+
if (control.pid) {
|
|
3094
|
+
try { process.kill(control.pid, 0); alive = true; } catch {}
|
|
3095
|
+
}
|
|
3096
|
+
if (alive) {
|
|
3097
|
+
console.log(`Engine is already running (PID ${control.pid}).`);
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
console.log(`Engine was running (PID ${control.pid}) but process is dead — restarting.`);
|
|
3063
3101
|
}
|
|
3064
3102
|
|
|
3065
3103
|
safeWrite(CONTROL_PATH, { state: 'running', pid: process.pid, started_at: ts() });
|
|
@@ -3124,6 +3162,11 @@ const commands = {
|
|
|
3124
3162
|
console.log(' On next start, they\'ll get a 20-min grace period before being marked as orphans.');
|
|
3125
3163
|
console.log(' To kill them now, run: node engine.js kill\n');
|
|
3126
3164
|
}
|
|
3165
|
+
// Kill the running engine process by PID
|
|
3166
|
+
const control = getControl();
|
|
3167
|
+
if (control.pid && control.pid !== process.pid) {
|
|
3168
|
+
try { process.kill(control.pid); } catch {}
|
|
3169
|
+
}
|
|
3127
3170
|
safeWrite(CONTROL_PATH, { state: 'stopped', stopped_at: ts() });
|
|
3128
3171
|
log('info', 'Engine stopped');
|
|
3129
3172
|
console.log('Engine stopped.');
|
package/package.json
CHANGED