shraga 0.1.51 → 0.1.52
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.
|
|
3
|
+
"version": "0.1.52",
|
|
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",
|
package/src/server/boot.ts
CHANGED
|
@@ -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; }
|