shraga 0.1.79 → 0.1.81
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.
|
|
3
|
+
"version": "0.1.81",
|
|
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",
|
|
@@ -231,7 +231,15 @@ export async function startJob(owner: JobOwner, command: string): Promise<string
|
|
|
231
231
|
// stdin is /dev/null so a child can never block on (or steal) input; stdout+stderr go straight
|
|
232
232
|
// to the fd — nothing is buffered in this process, so a chatty job costs disk, not memory.
|
|
233
233
|
const proc = spawn('/bin/sh', ['-c', buildShell(id, cmd)], {
|
|
234
|
-
cwd: owner.cwd,
|
|
234
|
+
cwd: owner.cwd,
|
|
235
|
+
env: {
|
|
236
|
+
...childEnv(),
|
|
237
|
+
...owner.env,
|
|
238
|
+
// A long-running launcher can gate on these: only a REGISTERED job gets a completion wake,
|
|
239
|
+
// so one started in the foreground orphans silently when its 60s tool call is killed.
|
|
240
|
+
SHRAGA_JOB_ID: id,
|
|
241
|
+
SHRAGA_BG_JOB: '1',
|
|
242
|
+
},
|
|
235
243
|
detached: true, stdio: ['ignore', fd, fd],
|
|
236
244
|
});
|
|
237
245
|
j.pid = proc.pid;
|
|
@@ -251,6 +259,47 @@ export async function startJob(owner: JobOwner, command: string): Promise<string
|
|
|
251
259
|
return id;
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
/** The little of a spawned child this module needs to adopt one (node's ChildProcess satisfies it). */
|
|
263
|
+
type AdoptableProcess = {
|
|
264
|
+
pid?: number | null;
|
|
265
|
+
stdout?: { on(ev: 'data', cb: (chunk: unknown) => void): void } | null;
|
|
266
|
+
stderr?: { on(ev: 'data', cb: (chunk: unknown) => void): void } | null;
|
|
267
|
+
on(ev: 'close' | 'error', cb: (arg: never) => void): void;
|
|
268
|
+
unref?: () => void;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Register an ALREADY-RUNNING child as a background job. The Shell tool hands a foreground command
|
|
273
|
+
* over at its deadline instead of killing it: the work continues, and from here it is an ordinary job
|
|
274
|
+
* — polled with ShellOutput, and reported back to the session when it ends. Without this the deadline
|
|
275
|
+
* stays a kill, which is what abandoned a scheduled run midway.
|
|
276
|
+
*/
|
|
277
|
+
export function adoptJob(owner: JobOwner, command: string, proc: AdoptableProcess, seed = ''): string {
|
|
278
|
+
const id = `job-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
279
|
+
const j: JobRecord = {
|
|
280
|
+
id, sessionId: owner.sessionId, uid: owner.uid, userEmail: owner.userEmail,
|
|
281
|
+
command: command.trim(), cwd: owner.cwd, startedAt: Date.now(), status: 'running',
|
|
282
|
+
pid: proc.pid ?? undefined,
|
|
283
|
+
};
|
|
284
|
+
// The child was spawned with pipes by the caller, so output is appended here rather than dup'd to
|
|
285
|
+
// an fd the way a job we spawn ourselves does it.
|
|
286
|
+
const append = (chunk: unknown) => {
|
|
287
|
+
const text = typeof chunk === 'string' ? chunk : String(chunk ?? '');
|
|
288
|
+
if (!text) return;
|
|
289
|
+
try { writeFileSync(logFile(id), text, { flag: 'a' }); } catch { /* the job outlives its log */ }
|
|
290
|
+
};
|
|
291
|
+
if (seed) append(seed);
|
|
292
|
+
proc.stdout?.on('data', append);
|
|
293
|
+
proc.stderr?.on('data', append);
|
|
294
|
+
proc.on('error', ((err: Error) => finish(id, 'error', undefined, `adopted job error: ${err?.message ?? err}`)) as never);
|
|
295
|
+
proc.on('close', ((code: number | null) => finish(id, 'exited', code ?? undefined)) as never);
|
|
296
|
+
proc.unref?.();
|
|
297
|
+
records.set(id, j);
|
|
298
|
+
save(j);
|
|
299
|
+
console.log(`${PREFIX} adopted ${id} pid=${j.pid} session=${j.sessionId.slice(0, 8)} cmd=${j.command.slice(0, 120)}`);
|
|
300
|
+
return id;
|
|
301
|
+
}
|
|
302
|
+
|
|
254
303
|
/** Record a terminal state exactly once, then schedule the follow-up. */
|
|
255
304
|
function finish(id: string, status: JobStatus, exitCode?: number, note?: string): void {
|
|
256
305
|
const j = records.get(id);
|
|
@@ -479,6 +528,7 @@ export function sessionJobRegistry(owner: JobOwner) {
|
|
|
479
528
|
};
|
|
480
529
|
return {
|
|
481
530
|
start: (command: string) => startJob(owner, command),
|
|
531
|
+
adopt: (command: string, proc: AdoptableProcess, seed?: string) => adoptJob(owner, command, proc, seed),
|
|
482
532
|
output(id: string): string | null {
|
|
483
533
|
const j = mine(id);
|
|
484
534
|
if (!j) return null;
|
|
@@ -208,7 +208,7 @@ export async function runSchedule(
|
|
|
208
208
|
// Tell the run how to report its own truthful outcome (scheduler/outcome.ts). Prompt tasks only:
|
|
209
209
|
// a `bash` task's permission handler allows nothing but the task's own command, so such a run
|
|
210
210
|
// could not write the file even if it wanted to — its exit code is already the truth there.
|
|
211
|
-
if (task.kind === 'prompt') {
|
|
211
|
+
if ((task.kind ?? 'prompt') === 'prompt') {
|
|
212
212
|
// Cleared on RESUME too: a resume reuses the interrupted run's session id, so a declaration left
|
|
213
213
|
// by the attempt that crashed would be adopted as this attempt's verdict. The contract is
|
|
214
214
|
// re-stated for the same reason — the resumed turn must be able to declare for itself.
|
|
@@ -401,7 +401,7 @@ export async function runSchedule(
|
|
|
401
401
|
// The run's OWN verdict beats "the turn returned" — see scheduler/outcome.ts. Deliberately after
|
|
402
402
|
// the finally: the session lock is released by now, so a run that declared `pending` can be closed
|
|
403
403
|
// by a later turn in this session (a background job's wake, a follow-up message) while we wait.
|
|
404
|
-
if (status === 'ok' && task.kind === 'prompt') {
|
|
404
|
+
if (status === 'ok' && (task.kind ?? 'prompt') === 'prompt') {
|
|
405
405
|
const declared = await resolveDeclaredOutcome(sessionId, abortController, schedule.id);
|
|
406
406
|
if (declared) {
|
|
407
407
|
status = declared.status;
|
|
@@ -27,11 +27,28 @@ export function saveThrottleState(state: Record<string, number>): void {
|
|
|
27
27
|
renameSync(tmp, THROTTLE_FILE);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Schedules persisted before `task.kind` existed carry only the payload fields. Everything
|
|
32
|
+
* downstream branches on the discriminant, so infer it from the shape once, on load — a task
|
|
33
|
+
* missing its kind silently skipped the outcome contract and reported every run as `ok`.
|
|
34
|
+
* Only `prompt` is inferable: `bash` and `job` are both `{ command }`, and guessing between them
|
|
35
|
+
* would hand a run the wrong permission handler, so a kindless command task is left alone.
|
|
36
|
+
*/
|
|
37
|
+
function normalizeTask(task: Record<string, unknown>): void {
|
|
38
|
+
if (task.kind) return;
|
|
39
|
+
if (typeof task.prompt === 'string' || typeof task.promptFile === 'string') task.kind = 'prompt';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeSchedule(s: Schedule): Schedule {
|
|
43
|
+
if (s.task && typeof s.task === 'object') normalizeTask(s.task as unknown as Record<string, unknown>);
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
|
|
30
47
|
export function loadSchedules(): Schedule[] {
|
|
31
48
|
if (!existsSync(FILE)) return [];
|
|
32
49
|
try {
|
|
33
50
|
const parsed = JSON.parse(readFileSync(FILE, 'utf-8'));
|
|
34
|
-
return Array.isArray(parsed) ? parsed : [];
|
|
51
|
+
return Array.isArray(parsed) ? parsed.map((s) => normalizeSchedule(s as Schedule)) : [];
|
|
35
52
|
} catch (err) {
|
|
36
53
|
console.error('[scheduler] failed to parse schedules.json:', err);
|
|
37
54
|
return [];
|