teapot-coding-agent 0.16.2 → 0.17.1

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.
@@ -1451,6 +1451,16 @@ export class Agent {
1451
1451
  const after = this.estimateTokens();
1452
1452
  this.stats.compactions++;
1453
1453
  this.compactedAtLen = this.messages.length;
1454
+ // the operator can inspect WHAT was remembered — a dedicated typed event
1455
+ // (not an agent message) keeps it out of the model's own voice
1456
+ await this.log.append("compaction", this.currentSession, this.currentBranch, {
1457
+ tokensBefore: before,
1458
+ tokensAfter: after,
1459
+ summarized: summarizedCount,
1460
+ dropped: droppedCount,
1461
+ mode,
1462
+ ...(summary ? { summary: String(summary).slice(0, 20_000) } : {}),
1463
+ });
1454
1464
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
1455
1465
  event: "context-compacted",
1456
1466
  tokensBefore: before,
@@ -43,6 +43,50 @@ export function safeJoin(cwd, p) {
43
43
  }
44
44
  return abs;
45
45
  }
46
+ /** per-workspace background shells (key: workspace path) */
47
+ const bgShellsByWs = new Map();
48
+ let bgSeq = 0;
49
+ /** how much of each job's output has already been handed to the model */
50
+ const bgReadCursors = new Map();
51
+ function bgMapFor(cwd) {
52
+ let m = bgShellsByWs.get(cwd);
53
+ if (!m) {
54
+ m = new Map();
55
+ bgShellsByWs.set(cwd, m);
56
+ }
57
+ return m;
58
+ }
59
+ function startBackgroundShell(cmd, ctx) {
60
+ const map = bgMapFor(ctx.cwd);
61
+ // opportunistic cleanup of long-dead jobs so the map can't grow forever
62
+ for (const [id, sh] of map) {
63
+ if (sh.exited && Date.now() - sh.startedAt > 30 * 60_000 && !map.delete(id))
64
+ void id;
65
+ }
66
+ const id = `bg${++bgSeq}`;
67
+ const child = spawn("/bin/bash", ["-lc", cmd], {
68
+ cwd: ctx.cwd,
69
+ detached: true,
70
+ stdio: ["ignore", "pipe", "pipe"],
71
+ env: { ...process.env, TERM: "dumb", GIT_PAGER: "cat", PAGER: "cat" },
72
+ });
73
+ const sh = {
74
+ id, cmd, child,
75
+ out: "", truncated: false,
76
+ startedAt: Date.now(), exited: false, code: null,
77
+ };
78
+ const collect = (chunk) => {
79
+ if (sh.out.length < 200_000)
80
+ sh.out += chunk.toString("utf8");
81
+ else
82
+ sh.truncated = true;
83
+ };
84
+ child.stdout?.on("data", collect);
85
+ child.stderr?.on("data", collect);
86
+ child.on("close", (code) => { sh.exited = true; sh.code = code; });
87
+ map.set(id, sh);
88
+ return id;
89
+ }
46
90
  /** Run a command in its own process group; kill the whole group on timeout. */
47
91
  function runShell(cmd, ctx, timeoutMs) {
48
92
  return new Promise((resolve) => {
@@ -652,13 +696,81 @@ export const TOOLS = [
652
696
  properties: {
653
697
  command: { type: "string" },
654
698
  timeout_ms: { type: "number", description: `default ${DEFAULT_TIMEOUT_MS}` },
699
+ background: {
700
+ type: "boolean",
701
+ description: "true = start the command and return IMMEDIATELY with a job id. Poll its output later via " +
702
+ "bash_output. Use for dev servers, watchers, long builds — anything you don't need to wait for.",
703
+ },
655
704
  },
656
705
  required: ["command"],
657
706
  },
658
707
  async run(args, ctx) {
708
+ if (args.background === true) {
709
+ const id = startBackgroundShell(str(args.command), ctx);
710
+ return {
711
+ ok: true,
712
+ result: `started in background (job ${id}). Continue with other work; read its output with ` +
713
+ `bash_output(job_id="${id}"). Kill it with bash_output(job_id="${id}", action="kill").`,
714
+ };
715
+ }
659
716
  return runShell(str(args.command), ctx, Math.min(num(args.timeout_ms, ctx.defaultTimeoutMs), 600_000));
660
717
  },
661
718
  },
719
+ {
720
+ name: "bash_output",
721
+ description: "Read or manage a background shell started via bash(background=true). " +
722
+ "action 'read' returns new output since the last read (plus exit status when done); " +
723
+ "'kill' terminates the whole process group. Use this instead of re-running long commands.",
724
+ parameters: {
725
+ type: "object",
726
+ properties: {
727
+ job_id: { type: "string", description: "the job id returned by bash(background=true)" },
728
+ action: {
729
+ type: "string",
730
+ enum: ["read", "kill"],
731
+ description: "default 'read'",
732
+ },
733
+ },
734
+ required: ["job_id"],
735
+ },
736
+ async run(args, ctx) {
737
+ const map = bgMapFor(ctx.cwd);
738
+ const sh = map.get(str(args.job_id));
739
+ if (!sh)
740
+ return {
741
+ ok: false,
742
+ result: `unknown background job "${str(args.job_id)}" in this workspace — ` +
743
+ `jobs are per-workspace and forgotten on harness restart`,
744
+ };
745
+ if (str(args.action) === "kill") {
746
+ if (!sh.exited) {
747
+ try {
748
+ if (sh.child.pid)
749
+ process.kill(-sh.child.pid, "SIGKILL");
750
+ }
751
+ catch { /* already gone */ }
752
+ }
753
+ const note = sh.exited ? "(already exited)" : "killed";
754
+ map.delete(sh.id);
755
+ return { ok: true, result: `job ${sh.id} ${note}. Output so far:\n${clip(sh.out, 4000)}` };
756
+ }
757
+ // action === read (default): hand back NEW bytes and remember the cursor
758
+ const fresh = sh.out.slice(bgReadCursors.get(sh.id) ?? 0);
759
+ bgReadCursors.set(sh.id, sh.out.length);
760
+ const status = sh.exited
761
+ ? `\n[exited with code ${sh.code ?? "?"}]`
762
+ : `\n[still running, ${(Math.round((Date.now() - sh.startedAt) / 100) / 10)}s]`;
763
+ const body = (fresh.length ? fresh : "(no new output)") +
764
+ (sh.truncated ? "\n… (output truncated at 200KB)" : "") +
765
+ status;
766
+ // finished jobs are removed once fully drained
767
+ if (sh.exited && bgReadCursors.get(sh.id) >= sh.out.length) {
768
+ map.delete(sh.id);
769
+ bgReadCursors.delete(sh.id);
770
+ }
771
+ return { ok: true, result: clip(body, ctx.maxOutputBytes) };
772
+ },
773
+ },
662
774
  {
663
775
  name: "read_url",
664
776
  description: "Fetch a web page and return its main readable content (title + plain text, boilerplate stripped via " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.16.2",
3
+ "version": "0.17.1",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",