shraga 0.1.47 → 0.1.49

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "The teammate you delegate coding to — a self-hostable, multi-user AI coding agent web UI (Claude Code, with a pluggable engine seam).",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -25,6 +25,8 @@ export class SelfUpgrade {
25
25
 
26
26
  private get o(): SelfUpgradeOptions { return this.options as SelfUpgradeOptions; }
27
27
 
28
+ private watching = false;
29
+
28
30
  /** Version of `pkg` this deployment currently has pinned in its app-root package.json. */
29
31
  public currentVersion(): string | null {
30
32
  try {
@@ -170,14 +172,42 @@ export class SelfUpgrade {
170
172
  */
171
173
  public deliverPendingReport(): UpgradeReport | null {
172
174
  const report = this.lastReport();
173
- if (!report) return null;
175
+ if (!report) {
176
+ // The supervisor writes the report only AFTER this process has booted and soaked, so on a
177
+ // successful upgrade there is NOTHING here yet — boot always loses the race. Without this
178
+ // watch the outcome DM was never delivered, and the in-flight marker sat until it aged out
179
+ // (30m), blocking every retry in between. Keep watching while the marker is live.
180
+ if (this.inFlight()) this.watchForReport();
181
+ return null;
182
+ }
174
183
  try { unlinkSync(this.o.reportFile); } catch { /* report already gone; emit anyway */ }
175
- try { unlinkSync(this.o.lockFile); } catch { /* no lock to clear */ }
184
+ // Clear the marker only if it belongs to THIS report. A boot can find a leftover report from
185
+ // the previous upgrade while a NEW one is already in flight (that is exactly the sequence when
186
+ // two upgrades run back to back) — deleting that marker would unguard the live attempt.
187
+ const marker = this.inFlight();
188
+ if (!marker || marker.target === report.target) {
189
+ try { unlinkSync(this.o.lockFile); } catch { /* no lock to clear */ }
190
+ }
176
191
 
177
192
  console.log(`${TAG} ${report.status}: ${report.detail}`);
178
193
  emitEvent('self-upgrade.finished', report);
179
194
  return report;
180
195
  }
196
+
197
+ /** Poll until the supervisor's report lands (or its marker ages out), then deliver it exactly
198
+ * once. Unref'd: a pending upgrade watch must never hold the process open. */
199
+ private watchForReport(): void {
200
+ if (this.watching) return;
201
+ this.watching = true;
202
+ const timer = setInterval(() => {
203
+ if (!this.inFlight()) { clearInterval(timer); this.watching = false; return; } // aged out
204
+ if (!existsSync(this.o.reportFile)) return;
205
+ clearInterval(timer);
206
+ this.watching = false;
207
+ this.deliverPendingReport();
208
+ }, this.o.reportPollMs);
209
+ timer.unref?.();
210
+ }
181
211
  }
182
212
 
183
213
  export class SelfUpgradeOptions {
@@ -193,6 +223,8 @@ export class SelfUpgradeOptions {
193
223
  public supervisorScript: string = path.join(PACKAGE_ROOT, 'src', 'server', 'self-upgrade', 'supervisor.sh');
194
224
  public reportFile: string = dataPath('.self-upgrade', 'report.json');
195
225
  public lockFile: string = dataPath('.self-upgrade', 'in-flight.json');
226
+ /** How often the post-boot watch looks for the supervisor's report. */
227
+ public reportPollMs: number = 5_000;
196
228
  public bootTimeoutSec: number = 180;
197
229
  public soakSec: number = 60;
198
230
  /** After this, an in-flight marker is treated as abandoned (supervisor killed by a reboot). */
@@ -136,10 +136,22 @@ wait_for_version() { # version timeout
136
136
 
137
137
  # A server that boots, flips the version, then dies 20s later has NOT upgraded successfully — that is
138
138
  # exactly the crash-loop an unattended upgrade must catch. Keep probing for the whole soak window.
139
+ # One dropped probe is not a crash-loop. This runs on a busy box where a single request can lose a
140
+ # 10s race (MCP mounts, a GC pause, the drain window of the restart we just did), and a zero-
141
+ # tolerance soak turns that blip into an automatic rollback of a perfectly good version — observed
142
+ # reverting 0.1.48 on feedox while the process stayed up throughout. Require CONSECUTIVE misses, so
143
+ # a real crash (which never answers again) still fails fast.
144
+ SOAK_MISS_LIMIT="${SOAK_MISS_LIMIT:-3}"
139
145
  soak() { # version seconds
140
- local deadline=$(( SECONDS + $2 ))
146
+ local deadline=$(( SECONDS + $2 )) misses=0
141
147
  while [ "$SECONDS" -lt "$deadline" ]; do
142
- reports_version "$1" || return 1
148
+ if reports_version "$1"; then
149
+ misses=0
150
+ else
151
+ misses=$(( misses + 1 ))
152
+ say "soak probe missed ($misses/$SOAK_MISS_LIMIT)"
153
+ [ "$misses" -ge "$SOAK_MISS_LIMIT" ] && return 1
154
+ fi
143
155
  sleep 5
144
156
  done
145
157
  return 0