shraga 0.1.51 → 0.1.53

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.
@@ -100,6 +100,9 @@ curl -s -H "$AUTH" -H 'content-type: application/json' \
100
100
  therefore refused by design: this cannot fire unattended.
101
101
  - **Check `blockers` first and relay them.** A source checkout (upgrade with git instead) and a
102
102
  local dev symlink at `node_modules/shraga` are both refused on purpose — do not work around either.
103
+ - **A scheduled job mid-run also blocks it.** The restart would kill that run (SIGTERM/143), so the
104
+ default is to wait for idle. Say so and offer to retry shortly; only send `{"force":true}` if the
105
+ owner, told what is running, asks for it anyway.
103
106
  - **202 means started, not succeeded.** The upgrade restarts the server, so the POST cannot report
104
107
  the outcome. A detached supervisor installs, restarts, waits for `/api/version` to report the new
105
108
  version, soaks it, and **reverts to the previous version automatically** if it doesn't hold.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shraga",
3
- "version": "0.1.51",
3
+ "version": "0.1.53",
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",
@@ -210,7 +210,7 @@ app.get('/api/self-upgrade', requireAuth, async (req, res) => {
210
210
  app.post('/api/self-upgrade', requireAuth, async (req, res) => {
211
211
  if (!(req as any).user?.isOwner) return void res.status(403).json({ error: 'Only the owner can manage upgrades' });
212
212
  try {
213
- const plan = await selfUpgrade.start({ version: req.body?.version });
213
+ const plan = await selfUpgrade.start({ version: req.body?.version, force: req.body?.force === true });
214
214
  // 409, not 500: a refusal is a well-formed answer about the deployment's state, and the caller
215
215
  // (often the agent, relaying to a human) needs the reason, not a stack trace.
216
216
  res.status(plan.started ? 202 : 409).json(plan);
@@ -61,8 +61,18 @@ export class SelfUpgrade {
61
61
  * the running process disagreeing with no way to reconcile.
62
62
  * - upgrade in flight → two supervisors racing on package.json is how you get a tree that
63
63
  * matches neither version. */
64
- public blockers(): string[] {
64
+ public blockers(opts: { force?: boolean } = {}): string[] {
65
65
  const reasons: string[] = [];
66
+
67
+ // An upgrade restarts the process, which kills any scheduled run mid-flight — observed taking
68
+ // out the morning social recon (SIGTERM/143) during a routine upgrade. Wait for idle instead;
69
+ // `force` is the deliberate escape hatch for an owner who wants it now anyway.
70
+ if (!opts.force) {
71
+ const running = this.o.runningJobs();
72
+ if (running.length) {
73
+ reasons.push(`${running.length} scheduled job(s) still running (${running.join(', ')}) — restarting now would kill them mid-flight; retry when idle or pass {"force":true}`);
74
+ }
75
+ }
66
76
  const pkgPath = path.join(this.o.appRoot, 'package.json');
67
77
 
68
78
  if (!existsSync(pkgPath)) {
@@ -114,11 +124,11 @@ export class SelfUpgrade {
114
124
  * Start an upgrade. Resolves as soon as the supervisor is detached — the result arrives later, in
115
125
  * the report file, because this process is about to be restarted by that supervisor.
116
126
  */
117
- public async start(opts: { version?: string } = {}): Promise<UpgradePlan> {
127
+ public async start(opts: { version?: string; force?: boolean } = {}): Promise<UpgradePlan> {
118
128
  const from = this.currentVersion();
119
129
  const target = !opts.version || opts.version === 'latest' ? await this.latestVersion() : opts.version.replace(/^v/, '');
120
130
 
121
- const blockers = this.blockers();
131
+ const blockers = this.blockers({ force: opts.force });
122
132
  if (blockers.length) return { started: false, from, target, reason: blockers.join('; ') };
123
133
  if (from === target) return { started: false, from, target, reason: `already on ${target}` };
124
134
  if (!await this.versionExists(target)) return { started: false, from, target, reason: `${this.o.pkg}@${target} does not exist on the registry` };
@@ -236,6 +246,14 @@ export class SelfUpgradeOptions {
236
246
  public soakSec: number = 60;
237
247
  /** After this, an in-flight marker is treated as abandoned (supervisor killed by a reboot). */
238
248
  public maxRunMs: number = 30 * 60 * 1000;
249
+ /** Scheduled runs currently in flight. Lazy + guarded: the scheduler is not always mounted (and
250
+ * must not be imported at module load), and a preflight that throws is worse than one that
251
+ * assumes idle. Injectable so the preflight is testable without a running scheduler. */
252
+ public runningJobs: () => string[] = () => {
253
+ try { return require('../scheduler/engine.ts').getRunningIds() as string[]; }
254
+ catch (err) { console.warn(`${TAG} could not read running jobs:`, (err as Error).message); return []; }
255
+ };
256
+
239
257
  /** Injectable so the preflight is testable without depending on the test host's PATH. */
240
258
  public hasPython3: () => boolean = () => {
241
259
  try { return spawnSync('python3', ['--version'], { stdio: 'ignore' }).status === 0; }