teapot-coding-agent 0.22.9 → 0.22.12
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 +21 -1
- package/dist/agent/tools.js +20 -0
- package/dist/index.js +35 -4
- package/dist/master.js +55 -0
- package/dist/server/api.js +30 -0
- package/package.json +1 -1
- package/public/assets/index-vP-CYzAU.js +30 -0
- package/public/assets/index-vP-CYzAU.js.map +1 -0
- package/public/assets/{shiki-wiF0hU8g.js → shiki-CDxIkmYs.js} +2 -2
- package/public/assets/{shiki-wiF0hU8g.js.map → shiki-CDxIkmYs.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/assets/index-DtMSCXN5.js +0 -30
- package/public/assets/index-DtMSCXN5.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -10,7 +10,7 @@ import { promises as fs, existsSync, readFileSync, readdirSync, statSync } from
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { EventLog, readEvents } from "../log/events.js";
|
|
12
12
|
import { chat, chatStream } from "./llm.js";
|
|
13
|
-
import { executeTool, toolSpecs, currentSkills, safeJoin, isContextOverflow } from "./tools.js";
|
|
13
|
+
import { executeTool, toolSpecs, currentSkills, safeJoin, isContextOverflow, hasRunningBgShells } from "./tools.js";
|
|
14
14
|
import { bus } from "../bus.js";
|
|
15
15
|
/**
|
|
16
16
|
* SYSTEM_TEMPLATE must stay byte-identical across every request of a session:
|
|
@@ -884,6 +884,13 @@ export class Agent {
|
|
|
884
884
|
if (this.bgExits.length > 32)
|
|
885
885
|
this.bgExits.shift(); // cap runaway spam
|
|
886
886
|
bus.emit("update", { kind: "agent-update", agentId: this.opts.id });
|
|
887
|
+
// the round ended BECAUSE this job was running (auto-continue is
|
|
888
|
+
// suppressed while bg jobs are alive) — its exit IS the wake-up call.
|
|
889
|
+
// Without this the report would sit unread until the operator poked
|
|
890
|
+
// the agent. Stopped agents stay stopped (a deliberate human stop).
|
|
891
|
+
if (this.status === "idle" && !this.stopRequested) {
|
|
892
|
+
this.start(`background job ${info.id} finished`);
|
|
893
|
+
}
|
|
887
894
|
}
|
|
888
895
|
/** fold queued background-exit reports into the next LLM call (once each) */
|
|
889
896
|
drainBackgroundExits() {
|
|
@@ -994,6 +1001,19 @@ export class Agent {
|
|
|
994
1001
|
// fresh user input arrived while we were finishing up — another round now
|
|
995
1002
|
if (this.pendingPrompts.length)
|
|
996
1003
|
continue;
|
|
1004
|
+
// A round that parked its work on a background bash (dev server,
|
|
1005
|
+
// watcher, long build) ENDED ON PURPOSE — the job's exit notification
|
|
1006
|
+
// is the wake-up. Nagging with "continue working" here just made the
|
|
1007
|
+
// agent spin up duplicate work while it meant to wait. Checked BEFORE
|
|
1008
|
+
// the finished/goal gates: a finish(goalComplete=false) round that
|
|
1009
|
+
// leaves a bg job running is exactly the "wait for it" pattern.
|
|
1010
|
+
if (hasRunningBgShells(this.toolCtx.cwd)) {
|
|
1011
|
+
await this.log.append("system_note", this.currentSession, this.currentBranch, {
|
|
1012
|
+
event: "auto-continue-suppressed",
|
|
1013
|
+
reason: "background job still running — the agent resumes when it exits",
|
|
1014
|
+
});
|
|
1015
|
+
break;
|
|
1016
|
+
}
|
|
997
1017
|
// auto-continue only makes sense with an active goal to continue toward
|
|
998
1018
|
if (finished ||
|
|
999
1019
|
!this.opts.autoContinue ||
|
package/dist/agent/tools.js
CHANGED
|
@@ -109,6 +109,26 @@ export function isContextOverflow(err) {
|
|
|
109
109
|
const msg = String(err?.message ?? "").toLowerCase();
|
|
110
110
|
return (/context (length|window|limit)|too many tokens|prompt is too long|max.{0,12}tokens.{0,20}(exceed|surpass)|request too large|payload too large|exceeds.{0,20}limit/.test(msg) || /\b(400|413)\b.*token/.test(msg));
|
|
111
111
|
}
|
|
112
|
+
/** Any background shell still running for this workspace? The auto-continue
|
|
113
|
+
* loop consults this before nudging a finished round: an agent that parked
|
|
114
|
+
* its work on `bash(background=true)` — dev server, watcher, long build —
|
|
115
|
+
* must NOT be nagged with "Continue working toward the current goal". The
|
|
116
|
+
* job's exit notification will wake the agent on its own. */
|
|
117
|
+
export function hasRunningBgShells(cwd) {
|
|
118
|
+
const m = bgShellsByWs.get(cwd);
|
|
119
|
+
if (!m)
|
|
120
|
+
return false;
|
|
121
|
+
for (const sh of m.values()) {
|
|
122
|
+
// opportunistic cleanup mirrors startBackgroundShell's
|
|
123
|
+
if (sh.exited && Date.now() - sh.startedAt > 30 * 60_000) {
|
|
124
|
+
m.delete(sh.id);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (!sh.exited)
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
112
132
|
/** Run a command in its own process group; kill the whole group on timeout. */
|
|
113
133
|
function runShell(cmd, ctx, timeoutMs) {
|
|
114
134
|
return new Promise((resolve) => {
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,33 @@ async function main() {
|
|
|
31
31
|
console.log(" env: TEAPOT_PORT, TEAPOT_HOST, TEAPOT_CONFIG_DIR, TEAPOT_DATA_DIR, TEAPOT_API_TOKEN");
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
|
+
// Handle restart-from-old-process: if we're the new process spawned by a
|
|
35
|
+
// live update, wait for the old process to release the port and exit.
|
|
36
|
+
const restartFromPid = process.env.TEAPOT_RESTART_FROM
|
|
37
|
+
? parseInt(process.env.TEAPOT_RESTART_FROM, 10)
|
|
38
|
+
: null;
|
|
39
|
+
if (restartFromPid && !Number.isNaN(restartFromPid)) {
|
|
40
|
+
// Give the old process time to gracefully shut down and release the port
|
|
41
|
+
console.log(`[teapot] restart: waiting for old process ${restartFromPid} to exit...`);
|
|
42
|
+
try {
|
|
43
|
+
// Send SIGTERM to old process (it handles graceful shutdown)
|
|
44
|
+
process.kill(restartFromPid, "SIGTERM");
|
|
45
|
+
// Wait for it to exit (max 5 seconds)
|
|
46
|
+
for (let i = 0; i < 50; i++) {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(restartFromPid, 0); // check if alive
|
|
49
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Process no longer exists
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Old process might already be gone
|
|
59
|
+
}
|
|
60
|
+
}
|
|
34
61
|
const configPath = resolveConfigPath(cfgArg);
|
|
35
62
|
const configExisted = existsSync(configPath);
|
|
36
63
|
const config = loadConfig(configPath);
|
|
@@ -57,12 +84,16 @@ async function main() {
|
|
|
57
84
|
process.on("unhandledRejection", (err) => {
|
|
58
85
|
console.error("[teapot] unhandled rejection (master survived):", err);
|
|
59
86
|
});
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
let shuttingDown = false;
|
|
88
|
+
const shutdown = async (signal) => {
|
|
89
|
+
if (shuttingDown)
|
|
90
|
+
return;
|
|
91
|
+
shuttingDown = true;
|
|
92
|
+
console.log(`\n[teapot] shutting down${signal ? ` (${signal})` : ""}...`);
|
|
62
93
|
await Promise.allSettled([...master.agents.values()].map((a) => a.dispose()));
|
|
63
94
|
process.exit(0);
|
|
64
95
|
};
|
|
65
|
-
process.on("SIGINT", () => void shutdown());
|
|
66
|
-
process.on("SIGTERM", () => void shutdown());
|
|
96
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
97
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
67
98
|
}
|
|
68
99
|
void main();
|
package/dist/master.js
CHANGED
|
@@ -8,6 +8,7 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import os from "node:os";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
11
12
|
import { Agent } from "./agent/agent.js";
|
|
12
13
|
import { parseSchedule, matches, nextFireAt } from "./scheduler/cron.js";
|
|
13
14
|
import { bus } from "./bus.js";
|
|
@@ -966,6 +967,11 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
966
967
|
if (!llm.baseUrl)
|
|
967
968
|
throw new Error("no baseUrl resolved");
|
|
968
969
|
agent.setLlmConfig(llm);
|
|
970
|
+
// keep the snapshot's provider in sync — the UI's model switcher reads
|
|
971
|
+
// sel().provider, and the old code only updated the config entry (ac)
|
|
972
|
+
// so the live snapshot kept the stale provider (the "right panel model
|
|
973
|
+
// switching doesn't work" backend half).
|
|
974
|
+
agent.opts.provider = effectiveProvider;
|
|
969
975
|
const ac = this.config.agents.find((a) => a.id === id);
|
|
970
976
|
if (ac && providerName)
|
|
971
977
|
ac.provider = providerName;
|
|
@@ -1069,4 +1075,53 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
1069
1075
|
})),
|
|
1070
1076
|
};
|
|
1071
1077
|
}
|
|
1078
|
+
/** Current application version (injected at build time via package.json). */
|
|
1079
|
+
static VERSION = "__APP_VERSION__";
|
|
1080
|
+
/** Return the current server version. */
|
|
1081
|
+
getVersion() {
|
|
1082
|
+
return Master.VERSION;
|
|
1083
|
+
}
|
|
1084
|
+
/** Gracefully stop all agents with a timeout. Returns when all are stopped or timeout. */
|
|
1085
|
+
async stopAllAgents(timeoutMs = 30_000) {
|
|
1086
|
+
console.log(`[teapot] stopping all ${this.agents.size} agents...`);
|
|
1087
|
+
const ids = [...this.agents.keys()];
|
|
1088
|
+
const stopPromises = ids.map(async (id) => {
|
|
1089
|
+
const a = this.agents.get(id);
|
|
1090
|
+
if (!a)
|
|
1091
|
+
return;
|
|
1092
|
+
try {
|
|
1093
|
+
await a.dispose();
|
|
1094
|
+
}
|
|
1095
|
+
catch (err) {
|
|
1096
|
+
console.error("[teapot] dispose error:", err);
|
|
1097
|
+
}
|
|
1098
|
+
// remove from map so a re-spawn after restart doesn't carry state
|
|
1099
|
+
this.agents.delete(id);
|
|
1100
|
+
});
|
|
1101
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("agent stop timeout")), timeoutMs));
|
|
1102
|
+
await Promise.race([Promise.all(stopPromises), timeout]);
|
|
1103
|
+
console.log("[teapot] all agents stopped");
|
|
1104
|
+
}
|
|
1105
|
+
/** Restart the server process (spawns a new master, keeps this one alive until ready). */
|
|
1106
|
+
async restartServer() {
|
|
1107
|
+
console.log("[teapot] restarting server...");
|
|
1108
|
+
// Stop all agents first
|
|
1109
|
+
await this.stopAllAgents(30_000);
|
|
1110
|
+
// Spawn a new process that will become the new master
|
|
1111
|
+
// We keep THIS process alive until the new one is ready to accept connections
|
|
1112
|
+
const execPath = process.execPath;
|
|
1113
|
+
const args = process.argv.slice(1);
|
|
1114
|
+
const env = { ...process.env };
|
|
1115
|
+
// Tell the new process it's a restart (it will read the version from the old one)
|
|
1116
|
+
env.TEAPOT_RESTART_FROM = String(process.pid);
|
|
1117
|
+
const child = spawn(execPath, args, {
|
|
1118
|
+
detached: true,
|
|
1119
|
+
stdio: "ignore",
|
|
1120
|
+
env,
|
|
1121
|
+
});
|
|
1122
|
+
child.unref(); // Let the parent exit when ready
|
|
1123
|
+
// Give the new process time to start and bind the port
|
|
1124
|
+
// This process will exit when the new one takes over (SIGTERM handled in index.ts)
|
|
1125
|
+
await new Promise((r) => setTimeout(r, 1_000));
|
|
1126
|
+
}
|
|
1072
1127
|
}
|
package/dist/server/api.js
CHANGED
|
@@ -556,6 +556,7 @@ export function buildApp(master) {
|
|
|
556
556
|
maxSpawnDepth: master.config.maxSpawnDepth,
|
|
557
557
|
tasks: master.config.tasks,
|
|
558
558
|
agents: master.config.agents.map((a) => ({ id: a.id, workspace: a.workspace, provider: a.provider, model: a.model })),
|
|
559
|
+
version: master.getVersion(),
|
|
559
560
|
});
|
|
560
561
|
});
|
|
561
562
|
app.put("/api/config", async (c) => {
|
|
@@ -871,6 +872,23 @@ export function buildApp(master) {
|
|
|
871
872
|
return c.json({ ok: true });
|
|
872
873
|
});
|
|
873
874
|
// edit a previously-sent prompt: forks there, optionally summarizes the tail
|
|
875
|
+
app.post("/api/agents/:id/edit-prompt", async (c) => {
|
|
876
|
+
const a = master.agents.get(c.req.param("id"));
|
|
877
|
+
if (!a)
|
|
878
|
+
return c.json({ error: "not found" }, 404);
|
|
879
|
+
const body = await c.req
|
|
880
|
+
.json()
|
|
881
|
+
.catch(() => null);
|
|
882
|
+
if (!body?.eventId || !body.text?.trim())
|
|
883
|
+
return c.json({ error: "eventId and text required" }, 400);
|
|
884
|
+
try {
|
|
885
|
+
const r = await a.editPromptAt(body.eventId, body.text, body.tail === "summarize" ? "summarize" : "discard");
|
|
886
|
+
return c.json({ ok: true, ...r });
|
|
887
|
+
}
|
|
888
|
+
catch (err) {
|
|
889
|
+
return c.json({ error: err.message }, 409);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
874
892
|
app.post("/api/agents/:id/fork", async (c) => {
|
|
875
893
|
const a = master.agents.get(c.req.param("id"));
|
|
876
894
|
if (!a)
|
|
@@ -964,6 +982,18 @@ export function buildApp(master) {
|
|
|
964
982
|
});
|
|
965
983
|
// ---- metrics / SSE ----
|
|
966
984
|
app.get("/api/metrics", (c) => c.json(master.metrics()));
|
|
985
|
+
// current server version (for live-update polling)
|
|
986
|
+
app.get("/api/version", (c) => c.json({ version: master.getVersion() }));
|
|
987
|
+
// trigger a full server restart (agents stopped gracefully, new process spawned)
|
|
988
|
+
app.post("/api/update/restart", async (c) => {
|
|
989
|
+
try {
|
|
990
|
+
await master.restartServer();
|
|
991
|
+
return c.json({ ok: true });
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
return c.json({ error: err.message }, 500);
|
|
995
|
+
}
|
|
996
|
+
});
|
|
967
997
|
// scheduled tasks with computed next-fire times (cron visibility for the UI)
|
|
968
998
|
app.get("/api/tasks", (c) => c.json({ tasks: master.tasksView() }));
|
|
969
999
|
app.get("/api/events", (c) => {
|