@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.
Files changed (2) hide show
  1. package/engine.js +50 -7
  2. 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
- fs.renameSync(tmp, p);
132
- } catch (e) {
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') return;
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
- console.log('Engine is already running.');
3062
- return;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/squad",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.squad/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "squad": "bin/squad.js"