teapot-coding-agent 0.37.57 → 0.37.59
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/dist/agent/agent.js +2 -0
- package/dist/agent/tools.js +144 -12
- package/dist/server/api.js +4 -2
- package/package.json +1 -1
- package/public/assets/index-KzHw4Sqo.js +36 -0
- package/public/assets/index-KzHw4Sqo.js.map +1 -0
- package/public/assets/{index-7Sxg9iQQ.css → index-mEDytv26.css} +1 -1
- package/public/assets/{shiki-c4QZPK2o.js → shiki-BwDjoZdu.js} +2 -2
- package/public/assets/{shiki-c4QZPK2o.js.map → shiki-BwDjoZdu.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-CNmXDnh6.js +0 -36
- package/public/assets/index-CNmXDnh6.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -610,6 +610,8 @@ export class Agent {
|
|
|
610
610
|
// so a background job records WHICH agent started it: sibling agents share a workspace,
|
|
611
611
|
// and `isLive()` must not count their jobs as this one's work in flight
|
|
612
612
|
agentId: opts.id,
|
|
613
|
+
// the job RECORD lives beside the session, not in a global dir: it is this agent's history
|
|
614
|
+
stateDir: opts.sessionDir,
|
|
613
615
|
defaultTimeoutMs: 120_000,
|
|
614
616
|
maxOutputBytes: 60_000,
|
|
615
617
|
skillRoots: this.skillRoots,
|
package/dist/agent/tools.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tool specs are plain JSON-schema function definitions — provider-agnostic.
|
|
4
4
|
*/
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { existsSync, promises as fs, readlinkSync, realpathSync } from "node:fs";
|
|
6
|
+
import { existsSync, promises as fs, mkdirSync, readFileSync, readlinkSync, realpathSync, writeFileSync } from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { discoverSkills, isValidSkillName, readSkillFile, saveSkill, SKILL_FILE, } from "./skills.js";
|
|
9
9
|
import { isReasoningEffort } from "../model-meta.js";
|
|
@@ -338,6 +338,62 @@ function toolShell() {
|
|
|
338
338
|
export function elapsed(t0, now = Date.now()) {
|
|
339
339
|
return `\n[took ${((now - t0) / 1000).toFixed(1)}s]`;
|
|
340
340
|
}
|
|
341
|
+
const jobsFile = (dir) => path.join(dir, "bg-jobs.json");
|
|
342
|
+
/** read the persisted records, tolerating a missing or corrupt file (never throws) */
|
|
343
|
+
function readJobRecords(dir) {
|
|
344
|
+
try {
|
|
345
|
+
const raw = readFileSync(jobsFile(dir), "utf8");
|
|
346
|
+
const parsed = JSON.parse(raw);
|
|
347
|
+
return Array.isArray(parsed) ? parsed.filter((r) => r && typeof r.id === "string") : [];
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Write the records atomically enough for this purpose.
|
|
355
|
+
*
|
|
356
|
+
* A torn write here is not worth a fsync: the file is a hint for the NEXT boot, and the worst case is
|
|
357
|
+
* losing one restart's worth of "this was running". Losing a job's identity entirely (the shipped
|
|
358
|
+
* behaviour) is what this exists to stop.
|
|
359
|
+
*/
|
|
360
|
+
function writeJobRecords(dir, records) {
|
|
361
|
+
try {
|
|
362
|
+
mkdirSync(dir, { recursive: true });
|
|
363
|
+
writeFileSync(jobsFile(dir), JSON.stringify(records, null, 1));
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
/* a read-only session dir must not break the tool */
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/** add or update one record (by id) */
|
|
370
|
+
function rememberJob(dir, rec) {
|
|
371
|
+
const all = readJobRecords(dir).filter((r) => r.id !== rec.id);
|
|
372
|
+
all.push(rec);
|
|
373
|
+
writeJobRecords(dir, all);
|
|
374
|
+
}
|
|
375
|
+
/** drop a record — the job finished AND was reported, or was killed */
|
|
376
|
+
function forgetJob(dir, id) {
|
|
377
|
+
writeJobRecords(dir, readJobRecords(dir).filter((r) => r.id !== id));
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Jobs a PREVIOUS process started and whose processes may still be alive.
|
|
381
|
+
*
|
|
382
|
+
* `alive` is answered by the caller (pidExists) rather than here, so this module stays free of the
|
|
383
|
+
* signal-plumbing and the test can drive either answer. A record whose pid is gone is stale and is
|
|
384
|
+
* dropped: reporting a finished job as running is the same class of lie as forgetting a live one.
|
|
385
|
+
*/
|
|
386
|
+
export function recoverableJobs(dir, alive) {
|
|
387
|
+
const all = readJobRecords(dir);
|
|
388
|
+
const live = [];
|
|
389
|
+
for (const r of all) {
|
|
390
|
+
if (r.pid !== null && alive(r.pid))
|
|
391
|
+
live.push(r);
|
|
392
|
+
}
|
|
393
|
+
if (live.length !== all.length)
|
|
394
|
+
writeJobRecords(dir, live);
|
|
395
|
+
return live;
|
|
396
|
+
}
|
|
341
397
|
function startBackgroundShell(cmd, ctx) {
|
|
342
398
|
const map = bgMapFor(ctx.cwd);
|
|
343
399
|
// opportunistic cleanup of long-dead jobs so the map can't grow forever
|
|
@@ -367,6 +423,20 @@ function startBackgroundShell(cmd, ctx) {
|
|
|
367
423
|
else
|
|
368
424
|
sh.truncated = true;
|
|
369
425
|
};
|
|
426
|
+
/**
|
|
427
|
+
* Persist the RECORD, so a later process can at least say this job exists.
|
|
428
|
+
*
|
|
429
|
+
* The `pid` is what makes it checkable on boot: a job whose pid is gone is dropped, one whose pid
|
|
430
|
+
* is alive is reported as "still running, output no longer reachable". That is the honest answer
|
|
431
|
+
* after a restart — see `ToolContext.stateDir`.
|
|
432
|
+
*/
|
|
433
|
+
if (ctx.stateDir) {
|
|
434
|
+
rememberJob(ctx.stateDir, {
|
|
435
|
+
id, cmd, owner: ctx.agentId ?? "", workspace: ctx.cwd,
|
|
436
|
+
pid: child.pid ?? null,
|
|
437
|
+
startedAt: sh.startedAt,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
370
440
|
child.stdout?.on("data", collect);
|
|
371
441
|
child.stderr?.on("data", collect);
|
|
372
442
|
// "exit" (process gone) and "close" (all stdio drained) are DIFFERENT events.
|
|
@@ -389,6 +459,9 @@ function startBackgroundShell(cmd, ctx) {
|
|
|
389
459
|
if (exitNotified)
|
|
390
460
|
return;
|
|
391
461
|
exitNotified = true;
|
|
462
|
+
// the job is over: its record must not claim otherwise on the next boot
|
|
463
|
+
if (ctx.stateDir)
|
|
464
|
+
forgetJob(ctx.stateDir, id);
|
|
392
465
|
// tell the harness so the agent learns the outcome WITHOUT polling
|
|
393
466
|
try {
|
|
394
467
|
ctx.onBackgroundExit?.({
|
|
@@ -434,7 +507,48 @@ export function isContextOverflow(err) {
|
|
|
434
507
|
* `owner` is included because several agents share one workspace (every sub-agent inherits its
|
|
435
508
|
* parent's), and "whose job is this" is the first thing an operator asks when deciding what to kill.
|
|
436
509
|
*/
|
|
437
|
-
export function listBgShells(cwd) {
|
|
510
|
+
export function listBgShells(cwd, stateDir) {
|
|
511
|
+
const live = liveShells(cwd);
|
|
512
|
+
/**
|
|
513
|
+
* Records from a previous process are appended, marked `detached`.
|
|
514
|
+
*
|
|
515
|
+
* Without them a job that survived a restart is INVISIBLE — MEASURED: a 3-hour GPU sweep kept
|
|
516
|
+
* writing while the panel showed nothing and `bash_output` said "forgotten on harness restart".
|
|
517
|
+
* The process cannot be re-attached (no fd survives the parent), so `detached` is the whole truth
|
|
518
|
+
* the UI can be given: it is running, and its output lives wherever the command redirected it.
|
|
519
|
+
*/
|
|
520
|
+
if (!stateDir)
|
|
521
|
+
return live;
|
|
522
|
+
const known = new Set(live.map((j) => j.id));
|
|
523
|
+
const recovered = recoverableJobs(stateDir, pidExists)
|
|
524
|
+
.filter((r) => !known.has(r.id) && r.workspace === cwd)
|
|
525
|
+
.map((r) => ({
|
|
526
|
+
id: r.id,
|
|
527
|
+
cmd: r.cmd,
|
|
528
|
+
owner: r.owner,
|
|
529
|
+
running: true,
|
|
530
|
+
detached: true,
|
|
531
|
+
pid: r.pid,
|
|
532
|
+
code: null,
|
|
533
|
+
startedAt: r.startedAt,
|
|
534
|
+
tail: "",
|
|
535
|
+
bytes: 0,
|
|
536
|
+
truncated: false,
|
|
537
|
+
}));
|
|
538
|
+
return [...live, ...recovered].sort((a, b) => b.startedAt - a.startedAt);
|
|
539
|
+
}
|
|
540
|
+
/** does this pid exist? (EPERM means it exists but is not ours to signal) */
|
|
541
|
+
function pidExists(pid) {
|
|
542
|
+
try {
|
|
543
|
+
process.kill(pid, 0);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
return err.code === "EPERM";
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/** the jobs THIS process is running, as the UI sees them */
|
|
551
|
+
function liveShells(cwd) {
|
|
438
552
|
const m = bgShellsByWs.get(cwd);
|
|
439
553
|
if (!m)
|
|
440
554
|
return [];
|
|
@@ -454,20 +568,38 @@ export function listBgShells(cwd) {
|
|
|
454
568
|
}))
|
|
455
569
|
.sort((a, b) => b.startedAt - a.startedAt);
|
|
456
570
|
}
|
|
457
|
-
/**
|
|
458
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Kill ONE background job; returns whether it existed.
|
|
573
|
+
*
|
|
574
|
+
* A DETACHED job (a record from a previous process) is handled differently on purpose: the harness no
|
|
575
|
+
* longer owns that process, so this drops the RECORD and does NOT signal the pid. Killing a pid we
|
|
576
|
+
* merely remember is how an unrelated process gets killed after pid reuse — and the operator's own
|
|
577
|
+
* instruction was to never touch a job from an earlier session. The caller reports what happened.
|
|
578
|
+
*/
|
|
579
|
+
export function killBgShell(cwd, id, stateDir) {
|
|
459
580
|
const m = bgShellsByWs.get(cwd);
|
|
460
581
|
const b = m?.get(id);
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
582
|
+
if (b) {
|
|
583
|
+
try {
|
|
584
|
+
killTree(b.child);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
/* already gone */
|
|
588
|
+
}
|
|
589
|
+
m.delete(id);
|
|
590
|
+
if (stateDir)
|
|
591
|
+
forgetJob(stateDir, id);
|
|
592
|
+
return true;
|
|
465
593
|
}
|
|
466
|
-
|
|
467
|
-
|
|
594
|
+
// not ours any more: forget it, leave the process alone
|
|
595
|
+
if (stateDir) {
|
|
596
|
+
const rec = readJobRecords(stateDir).find((r) => r.id === id);
|
|
597
|
+
if (rec) {
|
|
598
|
+
forgetJob(stateDir, id);
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
468
601
|
}
|
|
469
|
-
|
|
470
|
-
return true;
|
|
602
|
+
return false;
|
|
471
603
|
}
|
|
472
604
|
export function hasRunningBgShells(cwd) {
|
|
473
605
|
const m = bgShellsByWs.get(cwd);
|
package/dist/server/api.js
CHANGED
|
@@ -626,13 +626,15 @@ export function buildApp(master) {
|
|
|
626
626
|
const agent = master.agents.get(c.req.param("id"));
|
|
627
627
|
if (!agent)
|
|
628
628
|
return c.json({ error: "no such agent" }, 404);
|
|
629
|
-
|
|
629
|
+
const snap = agent.snapshot();
|
|
630
|
+
return c.json({ jobs: listBgShells(snap.workspace ?? "", snap.sessionDir) });
|
|
630
631
|
});
|
|
631
632
|
app.delete("/api/agents/:id/bg-jobs/:job", (c) => {
|
|
632
633
|
const agent = master.agents.get(c.req.param("id"));
|
|
633
634
|
if (!agent)
|
|
634
635
|
return c.json({ error: "no such agent" }, 404);
|
|
635
|
-
const
|
|
636
|
+
const snap = agent.snapshot();
|
|
637
|
+
const ok = killBgShell(snap.workspace ?? "", c.req.param("job"), snap.sessionDir);
|
|
636
638
|
return c.json({ ok }, ok ? 200 : 404);
|
|
637
639
|
});
|
|
638
640
|
app.get("/api/archived", (c) => c.json({ agents: master.archivedAgents() }));
|