teapot-coding-agent 0.14.11 → 0.15.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.
@@ -469,7 +469,17 @@ export class Agent {
469
469
  break;
470
470
  body = stripped;
471
471
  }
472
- return { text: body, status, updatedAt: new Date().toISOString() };
472
+ // verification contract (pi-goal-x style): a `verify:` block at the tail
473
+ let verify = "";
474
+ const vm = body.match(/\nverify:\s*\n([\s\S]+)$/i) ?? body.match(/^verify:\s*(.+)$/im);
475
+ if (vm) {
476
+ verify = vm[1].trim();
477
+ body = (body.slice(0, vm.index).trimEnd());
478
+ }
479
+ const out = { text: body, status, updatedAt: new Date().toISOString() };
480
+ if (verify)
481
+ out.verify = verify;
482
+ return out;
473
483
  }
474
484
  async writeGoalFile() {
475
485
  // don't let previously-appended bookkeeping lines accumulate in the body
@@ -480,7 +490,8 @@ export class Agent {
480
490
  break;
481
491
  body = stripped;
482
492
  }
483
- await fs.writeFile(this.goalFile, `${body}\n\nstatus: ${this.goal.status}\nupdated: ${this.goal.updatedAt}\n`, "utf8");
493
+ await fs.writeFile(this.goalFile, `${body}\n\nstatus: ${this.goal.status}\nupdated: ${this.goal.updatedAt}\n` +
494
+ (this.goal.verify ? `verify:\n${this.goal.verify}\n` : ""), "utf8");
484
495
  }
485
496
  async setGoal(text) {
486
497
  this.goal = { text, status: "active", updatedAt: new Date().toISOString() };
@@ -546,6 +557,33 @@ export class Agent {
546
557
  await this.writeGoalFile();
547
558
  await this.log.append("goal", this.currentSession, this.currentBranch, { event: "status", status });
548
559
  }
560
+ /** Set the goal's verification contract (completion requirements). */
561
+ async setGoalVerify(verify) {
562
+ this.goal = { ...this.goal, verify: verify.slice(0, 4000), updatedAt: new Date().toISOString() };
563
+ await this.writeGoalFile();
564
+ await this.log.append("goal", this.currentSession, this.currentBranch, {
565
+ event: "verify-set",
566
+ verify: verify.slice(0, 2000),
567
+ });
568
+ }
569
+ /** Record a verification-contract audit outcome (pi-goal-x style review). */
570
+ async setGoalAudit(verdict, feedback) {
571
+ this.goal = {
572
+ ...this.goal,
573
+ updatedAt: new Date().toISOString(),
574
+ audit: { verdict, feedback: feedback.slice(0, 4000), at: new Date().toISOString() },
575
+ };
576
+ if (verdict === "approved")
577
+ this.goal.status = "done";
578
+ else if (this.goal.status === "done")
579
+ this.goal.status = "active"; // reopened
580
+ await this.writeGoalFile();
581
+ await this.log.append("goal", this.currentSession, this.currentBranch, {
582
+ event: "audit",
583
+ verdict,
584
+ feedback: feedback.slice(0, 2000),
585
+ });
586
+ }
549
587
  snapshot() {
550
588
  return {
551
589
  id: this.opts.id,
@@ -554,7 +592,12 @@ export class Agent {
554
592
  workspace: this.workspace,
555
593
  session: this.currentSession,
556
594
  branch: this.currentBranch,
557
- goal: { status: this.goal.status, text: this.goal.text.slice(0, 400) },
595
+ goal: {
596
+ status: this.goal.status,
597
+ text: this.goal.text.slice(0, 400),
598
+ ...(this.goal.verify ? { verify: this.goal.verify.slice(0, 1000) } : {}),
599
+ ...(this.goal.audit ? { audit: this.goal.audit } : {}),
600
+ },
558
601
  latestProgress: this.latestProgress,
559
602
  stats: { ...this.stats },
560
603
  model: this.opts.llm.model,
@@ -585,7 +628,17 @@ export class Agent {
585
628
  * lazy session restore (before the mailbox is filled, so no duplicates).
586
629
  */
587
630
  enqueuePrompt(text, source = "user") {
588
- // a prompt during a tool park (wait_children) takes over instantly
631
+ // a prompt during a tool park (wait_children) takes over instantly:
632
+ // unpark fixes the DISPLAY, aborting the park signal actually ends the
633
+ // wait — the run chain resumes with this prompt drained at the boundary
634
+ if (this.parkedByTool) {
635
+ this.toolAbort.abort();
636
+ // the abort signal is one-shot — rearm it AND republish on toolCtx
637
+ // (toolCtx holds a COPY of the signal reference, so replacing only
638
+ // this.toolAbort left the park listening to the dead controller)
639
+ this.toolAbort = new AbortController();
640
+ this.toolCtx.signal = this.toolAbort.signal;
641
+ }
589
642
  this.unparkFromTool();
590
643
  this.wake?.();
591
644
  // a user message means the operator JUST checked in — they obviously know
@@ -595,20 +648,69 @@ export class Agent {
595
648
  this.activityChars = 0;
596
649
  this.turnsSinceProgress = 0;
597
650
  }
651
+ // stable id shared by the log event and the UI's pending echo — the UI
652
+ // flips its echo to "sent" when prompt-delivered carries this id back
653
+ const promptId = source === "user" ? `u${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}` : "";
598
654
  void this.ensureReady()
599
655
  .then(() => {
600
- this.pendingPrompts.push({ source, text });
601
- return this.log.append("prompt", this.currentSession, this.currentBranch, { source, text });
656
+ this.pendingPrompts.push({ source, text, ...(promptId ? { id: promptId } : {}) });
657
+ return this.log.append("prompt", this.currentSession, this.currentBranch, {
658
+ source,
659
+ text,
660
+ ...(promptId ? { promptId } : {}),
661
+ });
662
+ })
663
+ .then(() => bus.emit("update", { kind: "agent-update", agentId: this.opts.id }))
664
+ .catch(() => { });
665
+ return promptId;
666
+ }
667
+ /**
668
+ * Withdraw a still-pending user prompt (before it reaches an LLM call).
669
+ * Returns the text so the UI can put it back into the composer draft.
670
+ */
671
+ cancelPrompt(promptId) {
672
+ const i = this.pendingPrompts.findIndex((p) => p.id === promptId);
673
+ if (i === -1)
674
+ return null; // already delivered or unknown
675
+ const [p] = this.pendingPrompts.splice(i, 1);
676
+ void this.log
677
+ .append("system_note", this.currentSession, this.currentBranch, {
678
+ event: "prompt-cancelled",
679
+ promptId,
680
+ preview: p.text.slice(0, 80),
602
681
  })
603
682
  .then(() => bus.emit("update", { kind: "agent-update", agentId: this.opts.id }))
604
683
  .catch(() => { });
684
+ return p.text;
605
685
  }
606
- /** Hand queued user prompts to the model at a turn boundary. */
686
+ /**
687
+ * Hand queued user prompts to the model at a turn boundary. Each consumed
688
+ * prompt is logged as a system_note (prompt-delivered) so the UI can flip
689
+ * its pending echo to "sent" at exactly the moment the text enters an LLM
690
+ * call payload — not merely when it was logged.
691
+ */
607
692
  drainPendingPrompts() {
608
693
  for (const p of this.pendingPrompts.splice(0)) {
609
694
  this.messages.push({ role: "user", content: p.text });
695
+ if (p.source === "user") {
696
+ const id = p.id ?? `u${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
697
+ this.deliveredPrompts.push(id);
698
+ // cap: UI only needs recent confirmations
699
+ if (this.deliveredPrompts.length > 64)
700
+ this.deliveredPrompts.shift();
701
+ void this.log
702
+ .append("system_note", this.currentSession, this.currentBranch, {
703
+ event: "prompt-delivered",
704
+ promptId: id,
705
+ preview: p.text.slice(0, 80),
706
+ })
707
+ .then(() => bus.emit("update", { kind: "agent-update", agentId: this.opts.id }))
708
+ .catch(() => { });
709
+ }
610
710
  }
611
711
  }
712
+ /** ids of user prompts that have entered an LLM call payload (recent first-capped) */
713
+ deliveredPrompts = [];
612
714
  /** Resolves when all queued work (including a running loop) has settled. */
613
715
  settled() {
614
716
  return this.runChain.catch(() => { });
@@ -634,8 +736,10 @@ export class Agent {
634
736
  stop(reason = "stopped by user") {
635
737
  this.stopRequested = true;
636
738
  this.parkedByTool = false; // the park display must not outlive a stop
637
- // interrupt an in-flight LLM call immediately instead of waiting it out
739
+ // interrupt an in-flight LLM call AND any parked tool (wait_children):
740
+ // without aborting toolCtx.signal the park ran to its full timeout
638
741
  this.abort?.abort();
742
+ this.toolAbort.abort();
639
743
  this.wake?.();
640
744
  void this.enqueue(() => {
641
745
  this.setStatus("stopped", reason);
@@ -873,11 +977,20 @@ export class Agent {
873
977
  const text = String(a.text ?? "").trim();
874
978
  if (text)
875
979
  await this.setGoal(text);
980
+ // pi-goal-x-style verification contract: plain-text completion
981
+ // requirements audited before finish(goalComplete=true) is honored
982
+ if (typeof a.verify === "string" && a.verify.trim())
983
+ await this.setGoalVerify(a.verify.trim().slice(0, 4000));
876
984
  await this.answerMeta(call, text ? "goal updated" : "empty goal rejected");
877
985
  continue;
878
986
  }
879
987
  if (call.function.name === "get_goal") {
880
- await this.answerMeta(call, JSON.stringify({ goal: this.goal.text || "(none set)", status: this.goal.status }, null, 1));
988
+ await this.answerMeta(call, JSON.stringify({
989
+ goal: this.goal.text || "(none set)",
990
+ status: this.goal.status,
991
+ ...(this.goal.verify ? { verify: this.goal.verify } : {}),
992
+ ...(this.goal.audit ? { lastAudit: this.goal.audit } : {}),
993
+ }, null, 1));
881
994
  continue;
882
995
  }
883
996
  if (call.function.name === "ask_user") {
@@ -1054,8 +1167,59 @@ export class Agent {
1054
1167
  }
1055
1168
  async handleFinish(argsJson) {
1056
1169
  const args = safeParse(argsJson);
1057
- if (args.goalComplete === true)
1058
- await this.setGoalStatus("done");
1170
+ if (args.goalComplete === true && this.goal.status !== "done") {
1171
+ if (this.goal.verify?.trim()) {
1172
+ // pi-goal-x-style INDEPENDENT COMPLETION REVIEW: a separate LLM call
1173
+ // checks the verification contract against the conversation + summary
1174
+ // before "done" is honored. changes-required reopens the goal and
1175
+ // queues the auditor's feedback as the next user prompt.
1176
+ await this.log.append("goal", this.currentSession, this.currentBranch, {
1177
+ event: "audit-started",
1178
+ verify: this.goal.verify.slice(0, 2000),
1179
+ });
1180
+ try {
1181
+ const auditPrompt = `You are an independent completion AUDITOR (not the worker). Decide whether the goal is genuinely complete.\n\n` +
1182
+ `GOAL:\n${this.goal.text.slice(0, 4000)}\n\nVERIFICATION CONTRACT (must ALL hold):\n${this.goal.verify.slice(0, 2000)}\n\n` +
1183
+ `WORKER'S FINAL SUMMARY:\n${String(args.summary ?? "").slice(0, 3000)}\n\n` +
1184
+ `Conversation evidence follows. Reply with EXACTLY one line starting either\n` +
1185
+ `"APPROVED: <one-sentence justification>" or\n` +
1186
+ `"CHANGES-REQUIRED: <the specific gaps that must be addressed>".`;
1187
+ const res = await this.callLlm([
1188
+ ...this.buildMessages(),
1189
+ { role: "user", content: auditPrompt },
1190
+ ], []);
1191
+ const text = String(res.message.content ?? "");
1192
+ const approved = /^APPROVED\b/i.test(text.trim());
1193
+ const feedback = text.replace(/^(APPROVED|CHANGES-REQUIRED)\s*[:—-]?\s*/i, "").trim();
1194
+ await this.setGoalAudit(approved ? "approved" : "changes-required", feedback || "(no detail)");
1195
+ await this.log.append("message", this.currentSession, this.currentBranch, {
1196
+ role: "assistant",
1197
+ content: approved
1198
+ ? `✅ completion audit: APPROVED — ${feedback}`
1199
+ : `🔍 completion audit: CHANGES REQUIRED — ${feedback}`,
1200
+ });
1201
+ if (!approved) {
1202
+ // hand the gaps back to the worker as its next instruction
1203
+ this.pendingPrompts.push({
1204
+ source: "harness",
1205
+ text: `[harness] The completion audit REJECTED this finish. Address these gaps, then finish again with goalComplete=true:\n\n${feedback.slice(0, 2000)}`,
1206
+ });
1207
+ }
1208
+ }
1209
+ catch (err) {
1210
+ // auditor unavailable → fail open (record it, honor the finish) so a
1211
+ // flaky provider can't trap work in an unauditable loop
1212
+ await this.log.append("system_note", this.currentSession, this.currentBranch, {
1213
+ event: "audit-failed",
1214
+ detail: String(err.message).slice(0, 300),
1215
+ });
1216
+ await this.setGoalStatus("done");
1217
+ }
1218
+ }
1219
+ else {
1220
+ await this.setGoalStatus("done");
1221
+ }
1222
+ }
1059
1223
  await this.log.append("message", this.currentSession, this.currentBranch, {
1060
1224
  role: "assistant",
1061
1225
  final: true,
@@ -1420,10 +1584,19 @@ function allToolSpecs() {
1420
1584
  type: "function",
1421
1585
  function: {
1422
1586
  name: "set_goal",
1423
- description: "Replace the harness-managed goal text. Use when the objective itself changes — not for routine updates.",
1587
+ description: "Replace the harness-managed goal text. Use when the objective itself changes — not for routine updates. " +
1588
+ "Optionally attach a verification contract (plain-text completion requirements); finish(goalComplete=true) " +
1589
+ "is then audited against it by an independent reviewer before the goal counts as done.",
1424
1590
  parameters: {
1425
1591
  type: "object",
1426
- properties: { text: { type: "string" } },
1592
+ properties: {
1593
+ text: { type: "string" },
1594
+ verify: {
1595
+ type: "string",
1596
+ description: "completion requirements that must verifiably hold, e.g. 'npm test passes with zero failures; " +
1597
+ "the new endpoint appears in README'. Omit for no audit.",
1598
+ },
1599
+ },
1427
1600
  required: ["text"],
1428
1601
  },
1429
1602
  },
@@ -1617,6 +1790,13 @@ function rebuildMessagesFrom(list) {
1617
1790
  "get_feedback", "add_feedback", "record_decision", "get_decisions",
1618
1791
  ]);
1619
1792
  const openCalls = new Map(); // real tool_call id -> name
1793
+ // callId -> the tool_call entry on its assistant message (fast arg enrichment)
1794
+ const assistantCalls = new Map();
1795
+ const rememberAssistantCalls = (m) => {
1796
+ if (Array.isArray(m.tool_calls))
1797
+ for (const t of m.tool_calls)
1798
+ assistantCalls.set(t.id, t);
1799
+ };
1620
1800
  // meta answers are now logged as regular tool_results; the legacy progress
1621
1801
  // synthesizer below must not duplicate them
1622
1802
  const loggedResults = new Set(list.filter((e) => e.type === "tool_result").map((e) => String(e.data.callId ?? "")));
@@ -1654,14 +1834,20 @@ function rebuildMessagesFrom(list) {
1654
1834
  if (!META_TOOLS.has(t.function.name))
1655
1835
  openCalls.set(t.id, t.function.name);
1656
1836
  }
1837
+ rememberAssistantCalls(m);
1657
1838
  msgs.push(m);
1658
1839
  }
1659
1840
  else if (e.type === "tool_call") {
1660
1841
  // enrich the preceding assistant tool_calls with real arguments —
1661
1842
  // prefer the provider's raw string so restored requests stay
1662
- // byte-identical (prefix caches stay warm across restarts)
1663
- const prev = [...msgs].reverse().find((x) => x.role === "assistant" && x.tool_calls?.some((t) => t.id === d.callId));
1664
- const tc = prev?.tool_calls?.find((t) => t.id === d.callId);
1843
+ // byte-identical (prefix caches stay warm across restarts).
1844
+ // Indexed by call id: the old [...msgs].reverse().find() scanned the
1845
+ // whole message list per call — O(n²) on multi-thousand-event logs.
1846
+ let tc = assistantCalls.get(String(d.callId ?? ""));
1847
+ if (!tc) {
1848
+ const prev = [...msgs].reverse().find((x) => x.role === "assistant" && x.tool_calls?.some((t) => t.id === d.callId));
1849
+ tc = prev?.tool_calls?.find((t) => t.id === d.callId);
1850
+ }
1665
1851
  if (tc) {
1666
1852
  if (typeof d.argsRaw === "string")
1667
1853
  tc.function.arguments = d.argsRaw;
@@ -16,7 +16,12 @@ function clip(s, max) {
16
16
  async function readText(p) {
17
17
  return fs.readFile(p, "utf8");
18
18
  }
19
- /** Resolve a path inside the workspace; reject escapes (incl. symlink targets). */
19
+ /** Resolve a path inside the workspace; reject escapes (incl. symlink targets).
20
+ * KNOWN LIMIT (review finding, accepted): the check-then-open pattern has a
21
+ * TOCTOU window — a symlink swapped in between safeJoin() and fs.open() could
22
+ * point outside the workspace. Closing it needs openat(2)/O_NOFOLLOW at every
23
+ * call site; the operator-facing risk is low (requires workspace write access
24
+ * already), so we document rather than rewrite all call sites. */
20
25
  export function safeJoin(cwd, p) {
21
26
  const abs = path.resolve(cwd, p);
22
27
  const rel = path.relative(cwd, abs);
@@ -37,7 +37,8 @@ export const ConfigPatchSchema = z.object({
37
37
  defaultProvider: z.string().optional(),
38
38
  progressIntervalMs: z.number().int().min(10_000, "progress interval must be ≥ 10s").optional(),
39
39
  progressMinChars: z.number().int().min(100).optional(),
40
- contextTokenBudget: z.number().int().min(1_000).optional(),
40
+ // null clears a previously pinned compact budget → back to per-model derivation
41
+ contextTokenBudget: z.number().int().min(1_000).nullable().optional(),
41
42
  contextWindowTokens: z.number().int().min(1_000).optional(),
42
43
  maxSpawnDepth: z.number().int().min(0).max(8).optional(),
43
44
  tasks: z.array(TaskSchema).optional(),
@@ -97,8 +97,12 @@ export class EventLog {
97
97
  }
98
98
  const p = new Promise((resolve, reject) => {
99
99
  this.chain = this.chain.then(() => {
100
- if (!this.stream)
100
+ if (!this.stream) {
101
+ // log closed (dispose) — resolve so callers don't await forever,
102
+ // but say so: silent drops made post-shutdown writes look logged
103
+ console.error(`[teapot] event dropped after log close (${this.agentId}): ${type}`);
101
104
  return resolve(evt);
105
+ }
102
106
  this.stream.write(JSON.stringify(evt) + "\n", "utf8", (err) => err ? reject(err) : resolve(evt));
103
107
  }, () => resolve(evt));
104
108
  });
package/dist/master.js CHANGED
@@ -250,11 +250,17 @@ export class Master {
250
250
  }
251
251
  }
252
252
  for (const t of this.config.tasks ?? []) {
253
- this.tasks.push({
254
- task: t,
255
- schedule: parseSchedule(t.schedule),
256
- lastRunMin: t.lastRunMin ?? -1,
257
- });
253
+ try {
254
+ this.tasks.push({
255
+ task: t,
256
+ schedule: parseSchedule(t.schedule),
257
+ lastRunMin: t.lastRunMin ?? -1,
258
+ });
259
+ }
260
+ catch (err) {
261
+ // a hand-edited schedule typo must not stop the server from listening
262
+ console.error(`[teapot] skipping task "${t.id}": ${err.message}`);
263
+ }
258
264
  }
259
265
  // single low-frequency tick for everything periodic (idle cost ≈ 0)
260
266
  setInterval(() => void this.tick(), 15_000).unref();
@@ -401,7 +407,11 @@ export class Master {
401
407
  })),
402
408
  stop: (ids) => this.stopChildrenFor(ac.id, ids),
403
409
  message: (id, text) => this.messageChild(ac.id, id, text),
404
- wait: (ids, ms) => this.waitChildren(ac.id, ids, ms),
410
+ wait: (ids, ms) =>
411
+ // wire the parent's tool-abort signal: a user stop / dispose must
412
+ // interrupt the park immediately instead of leaving the run chain
413
+ // blocked until the full timeout (up to 1h)
414
+ this.waitChildren(ac.id, ids, ms, self.toolCtx.signal),
405
415
  };
406
416
  agent.toolCtx.subAgents = hooks;
407
417
  }
@@ -77,7 +77,17 @@ export function buildApp(master) {
77
77
  }
78
78
  };
79
79
  send({ kind: "hello", agents: [...master.agents.values()].map((a) => a.snapshot()) });
80
- onUpdate = (ev) => send(ev);
80
+ onUpdate = (raw) => {
81
+ const ev = raw;
82
+ // attach the fresh snapshot to agent-update events so clients can
83
+ // update their state WITHOUT a follow-up GET /api/agents poll
84
+ if (ev?.kind === "agent-update" && ev.agentId) {
85
+ const a = master.agents.get(ev.agentId);
86
+ send({ ...ev, snapshot: a?.snapshot() });
87
+ return;
88
+ }
89
+ send(ev);
90
+ };
81
91
  bus.on("update", onUpdate);
82
92
  // app-level liveness ping every 30s
83
93
  ka = setInterval(() => send({ kind: "ping" }), 30_000);
@@ -205,7 +215,9 @@ export function buildApp(master) {
205
215
  // collision, auto-suffix so creating ~/a/proj and ~/b/proj just works
206
216
  let id = base;
207
217
  let n = 2;
208
- while (master.agents.has(id))
218
+ // ids must be unique among RUNNING agents AND persisted config entries —
219
+ // a config-only agent (not yet loaded) would otherwise collide on addAgent
220
+ while (master.agents.has(id) || master.config.agents.some((a) => a.id === id))
209
221
  id = `${base.slice(0, 38)}-${n++}`;
210
222
  try {
211
223
  const agent = await master.addAgent({ id, workspace: ws, provider: body.provider, model: body.model }, { persist: true, fresh: true });
@@ -451,11 +463,25 @@ export function buildApp(master) {
451
463
  if (!body.text?.trim())
452
464
  return c.json({ error: "text required" }, 400);
453
465
  // returns immediately: the prompt is logged + broadcast now, delivered to
454
- // the model at the next turn boundary (never blocks on a running agent)
455
- a.enqueuePrompt(body.text, "user");
466
+ // the model at the next turn boundary (never blocks on a running agent).
467
+ // promptId ties the UI's pending echo to the later prompt-delivered note.
468
+ const promptId = a.enqueuePrompt(body.text, "user");
456
469
  if (body.start !== false && a.status !== "running")
457
470
  a.start("prompt");
458
- return c.json({ ok: true, queued: a.snapshot().pendingPrompts });
471
+ return c.json({ ok: true, promptId, queued: a.snapshot().pendingPrompts });
472
+ });
473
+ // Withdraw a still-pending prompt; returns its text for the composer draft.
474
+ app.post("/api/agents/:id/prompt/cancel", async (c) => {
475
+ const a = master.agents.get(c.req.param("id"));
476
+ if (!a)
477
+ return c.json({ error: "not found" }, 404);
478
+ const body = await c.req.json();
479
+ if (!body.promptId)
480
+ return c.json({ error: "promptId required" }, 400);
481
+ const text = a.cancelPrompt(body.promptId);
482
+ if (text === null)
483
+ return c.json({ error: "not pending (already delivered?)" }, 409);
484
+ return c.json({ ok: true, text });
459
485
  });
460
486
  app.post("/api/agents/:id/start", (c) => {
461
487
  const a = master.agents.get(c.req.param("id"));
@@ -534,10 +560,18 @@ export function buildApp(master) {
534
560
  const body = await c.req.json();
535
561
  if (body.text) {
536
562
  await a.setGoal(body.text);
563
+ if (typeof body.verify === "string" && body.verify.trim())
564
+ await a.setGoalVerify(body.verify.trim());
537
565
  // goals live behind get_goal(), so a silent save would go unnoticed —
538
566
  // queue a harness prompt unless the caller explicitly declines
539
- if (body.notify !== false)
567
+ if (body.notify !== false) {
540
568
  a.enqueuePrompt(`[harness] The operator set a new goal:\n\n${body.text}\n\nAlign your work with it.`, "harness");
569
+ // an idle agent must actually START working on the new goal — the
570
+ // queued prompt alone sat there forever when nothing else started
571
+ // the loop (reported: goal save left the session idle indefinitely)
572
+ if (a.status !== "running")
573
+ a.start("goal set");
574
+ }
541
575
  }
542
576
  else if (body.status)
543
577
  await a.setGoalStatus(body.status);
@@ -701,12 +735,33 @@ export function buildApp(master) {
701
735
  return c.json({ ok: true, ...r });
702
736
  });
703
737
  // ---- event log access (human/inspect friendly) ----
738
+ // mtime-keyed parse cache: the web UI polls this endpoint every ~400ms and
739
+ // re-reading + re-parsing the WHOLE chat.jsonl each time dominated CPU on
740
+ // multi-MB sessions. The file only ever appends, so an unchanged mtime lets
741
+ // us serve the previous parse verbatim.
742
+ const eventsCache = new Map();
743
+ async function readEventsCached(filePath) {
744
+ try {
745
+ const st = await fs.stat(filePath);
746
+ const hit = eventsCache.get(filePath);
747
+ if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
748
+ return hit.events;
749
+ const events = await readEvents(filePath);
750
+ if (eventsCache.size > 64)
751
+ eventsCache.clear();
752
+ eventsCache.set(filePath, { mtimeMs: st.mtimeMs, size: st.size, events });
753
+ return events;
754
+ }
755
+ catch {
756
+ return readEvents(filePath);
757
+ }
758
+ }
704
759
  app.get("/api/agents/:id/events", async (c) => {
705
760
  const a = master.agents.get(c.req.param("id"));
706
761
  if (!a)
707
762
  return c.json({ error: "not found" }, 404);
708
763
  const limit = Math.min(Number(c.req.query("limit") ?? 200), 5000);
709
- let events = await readEvents(a.log.filePath);
764
+ let events = await readEventsCached(a.log.filePath);
710
765
  const branch = c.req.query("branch");
711
766
  const session = c.req.query("session");
712
767
  if (branch)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.14.11",
3
+ "version": "0.15.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",
@@ -0,0 +1 @@
1
+ :root{--bg-darkest:#1a1c22;--bg-dark:#232630;--bg-mid:#2b2e39;--bg-light:#343845;--fg:#dcdee4;--dim:#9298a5;--line:#1d2027;--acc:#5865f2;--ok:#3ba55d;--warn:#faa81a;--err:#ed4245;--tool:#3ba0c9;--term-bg:#0d0e12;--term-fg:#dcdee4;font-family:gg sans,ui-sans-serif,system-ui,-apple-system,Segoe UI,sans-serif}[data-theme=midnight]{--bg-darkest:#0d0f18;--bg-dark:#12141f;--bg-mid:#1b1f31;--bg-light:#252a42;--fg:#dfe2f2;--dim:#8a90ad;--line:#0a0c14;--acc:#8b7cf7;--ok:#54c08a;--warn:#e5b567;--err:#ef6a6a;--tool:#6aa9e9;--term-bg:#0a0c13;--term-fg:#dfe2f2}[data-theme=tea]{--bg-darkest:#241c16;--bg-dark:#2e241c;--bg-mid:#3b2f24;--bg-light:#493b2d;--fg:#f0e4d2;--dim:#b39d82;--line:#1b140f;--acc:#d98e32;--ok:#7fa653;--warn:#e0b058;--err:#cf5b47;--tool:#c9975f;--term-bg:#1d1712;--term-fg:#ecdcc3}[data-theme=coffee]{--bg-darkest:#191410;--bg-dark:#211a14;--bg-mid:#30271e;--bg-light:#403527;--fg:#ede3d0;--dim:#a89478;--line:#120d07;--acc:#c68a4b;--ok:#8aa15c;--warn:#d9a441;--err:#c25450;--tool:#a67c52;--term-bg:#15100c;--term-fg:#e8dcc5}[data-theme=matcha]{--bg-darkest:#1b2217;--bg-dark:#232b1e;--bg-mid:#2f3a28;--bg-light:#3c4a33;--fg:#e8eeda;--dim:#9aab84;--line:#131a10;--acc:#8db64e;--ok:#6fae4e;--warn:#d8b13c;--err:#cd5c50;--tool:#5da88f;--term-bg:#151b11;--term-fg:#dfe8cb}[data-theme=matrix]{--bg-darkest:#060a06;--bg-dark:#0a0f0a;--bg-mid:#122116;--bg-light:#18301d;--fg:#c9f2cf;--dim:#6fa075;--line:#030603;--acc:#2fd558;--ok:#39c95f;--warn:#b8d977;--err:#ff6b6b;--tool:#57c99b;--term-bg:#030503;--term-fg:#3f7}[data-theme=sunset]{--bg-darkest:#1c1422;--bg-dark:#241b2b;--bg-mid:#33253e;--bg-light:#452f52;--fg:#f2e6ea;--dim:#ab93a8;--line:#150e1b;--acc:#f2784b;--ok:#63b46f;--warn:#eda63d;--err:#e25563;--tool:#b06ac9;--term-bg:#170f1d;--term-fg:#f0dfe4}[data-theme=light]{--bg-darkest:#eceef4;--bg-dark:#f6f7fa;--bg-mid:#e2e5ee;--bg-light:#fff;--fg:#24272f;--dim:#5d6470;--line:#d8dbe4;--acc:#4757d8;--ok:#2b8a4b;--warn:#b97a0f;--err:#d83c3f;--tool:#2278a8;--term-bg:#fff;--term-fg:#24272f}[data-theme=strawberry]{--bg-darkest:#fbe4ea;--bg-dark:#fdf0f3;--bg-mid:#f7d4de;--bg-light:#fffafb;--fg:#4a2733;--dim:#96707e;--line:#f0ccd6;--acc:#e05575;--ok:#4d9e63;--warn:#c98a2d;--err:#d43d51;--tool:#b05687;--term-bg:#fff5f8;--term-fg:#4a2733}[data-theme=ramune]{--bg-darkest:#ddeff7;--bg-dark:#ecf7fb;--bg-mid:#c9e7f4;--bg-light:#fbfeff;--fg:#173342;--dim:#5b7d90;--line:#c2e2ef;--acc:#1d86ae;--ok:#3a9e6e;--warn:#cf9a2a;--err:#d64f5c;--tool:#3a9db8;--term-bg:#f4fbfd;--term-fg:#173342}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{background:var(--bg-dark);color:var(--fg);font-size:15px}.layout{grid-template-columns:250px 1fr 370px;height:100dvh;display:grid;overflow:hidden}.layout.right-hidden{grid-template-columns:250px 1fr}.layout.right-hidden .rightbar{display:none}@media (width<=1100px){.layout,.layout.right-hidden{grid-template-columns:220px 1fr}.layout.right-hidden .rightbar{display:block}.rightbar{z-index:5;width:min(420px,92vw);transition:transform .18s;position:fixed;top:0;bottom:0;right:0;transform:translate(100%);box-shadow:-8px 0 24px #0007}.rightbar.open{transform:none}}.sidebar{background:var(--bg-darkest);flex-direction:column;padding:10px 8px;display:flex;overflow-y:auto}.themepop{background:var(--bg-mid);border:1px solid var(--bg-light);border-radius:8px;flex-direction:column;gap:8px;padding:10px;display:flex;position:static}.trow{color:var(--fg);cursor:pointer;align-items:center;gap:6px;font-size:12.5px;display:flex}.trow select{background:var(--bg-darkest);color:var(--fg);border:none;border-radius:5px;outline:none;padding:3px 6px}.themegrid{grid-template-columns:1fr 1fr;gap:4px;display:grid}.themebtn{background:var(--bg-light);color:var(--fg);cursor:pointer;text-align:left;border:1px solid #0000;border-radius:6px;align-items:center;gap:6px;min-width:0;padding:5px 8px;font-size:12px;display:flex;overflow:hidden}.themebtn>:last-child{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.themebtn:hover{filter:brightness(1.15)}.themebtn.on{border-color:var(--acc)}.swdots{flex-shrink:0;gap:2px;display:inline-flex}.swdots i{border-radius:50%;width:9px;height:9px;display:inline-block;box-shadow:inset 0 0 0 1px #0004}.agent-list{flex:1;min-height:0;overflow-y:auto}.agent-item{cursor:pointer;color:var(--dim);border-radius:6px;align-items:center;gap:8px;margin-bottom:2px;padding:7px 10px;display:flex}.agent-item .subtag{color:var(--dim);font-size:10px}.caret{cursor:pointer;width:12px;color:var(--dim);text-align:center;-webkit-user-select:none;user-select:none;flex-shrink:0;font-size:9px}.caret:hover{color:var(--fg)}.caret-spacer{flex-shrink:0;width:12px}.agent-item:hover{background:var(--bg-mid)}.agent-item.sel{background:var(--bg-mid);color:var(--fg)}.dot{border-radius:50%;flex-shrink:0;width:9px;height:9px}.dot.running{background:var(--ok);box-shadow:0 0 6px var(--ok)}.dot.waiting{background:var(--acc);box-shadow:0 0 6px var(--acc);animation:1.6s ease-in-out infinite pulse}.badge.waiting{color:var(--acc)}.dot.idle{background:var(--warn)}.dot.stopped{background:var(--dim)}.dot.error{background:var(--err);box-shadow:0 0 6px var(--err)}.sidebar .metrics{color:var(--dim);border-top:1px solid var(--line);margin-top:12px;padding:8px 10px;font-size:11px;line-height:1.7}.sidebar-footer{border-top:1px solid var(--line);flex-direction:column;gap:2px;margin-top:auto;padding-top:8px;display:flex}.footer-row{align-items:center;gap:8px;display:flex}.footer-row .conn{margin-left:0}.sidebar-footer .metrics{border-top:none;margin-top:4px;padding:0}.brand{color:var(--fg);align-items:center;gap:8px;font-size:14px;display:flex}.version{color:var(--dim);font-family:ui-monospace,Menlo,monospace;font-size:11px}.channel{flex-direction:column;min-width:0;min-height:0;display:flex;position:relative;overflow:hidden}.chan-head{border-bottom:2px solid var(--line);background:var(--bg-dark);flex-shrink:0;align-items:center;gap:10px;padding:10px 16px;display:flex}.chan-head .hash{color:var(--dim);font-size:20px}.chan-head .title{font-weight:700}.chan-head .sub{color:var(--dim);text-overflow:ellipsis;white-space:nowrap;margin-left:8px;font-size:12px;overflow:hidden}.badge{background:var(--bg-light);vertical-align:middle;border-radius:10px;padding:1px 8px;font-size:11px}.badge.running{color:var(--ok)}.badge.error{color:var(--err)}.badge.idle{color:var(--warn)}.badge.done{color:var(--acc)}.badge.queued{color:var(--warn);white-space:nowrap;background:#faa81a1a;border:1px solid #faa81a44}.feed{overscroll-behavior:contain;flex:1;min-height:0;padding:14px 0 8px;overflow-y:auto}.msg{gap:14px;padding:3px 18px;display:flex}.msg:hover{background:#ffffff08}.msg.pending{opacity:.75}.msg.pending .ts{color:var(--warn);font-weight:600}.msg.pending[data-sent="1"] .ts{color:var(--ok)}.msg.pending .editbtn.cancel{color:var(--err)}.msg.grouped{padding-top:0}.avatar{border-radius:50%;flex-shrink:0;justify-content:center;align-items:center;width:38px;height:38px;margin-top:2px;font-size:17px;display:flex}.msg-body{flex:1;min-width:0}.msg-head{align-items:baseline;gap:8px;display:flex}.actor{color:var(--warn);white-space:nowrap;font-size:11px}.runcard{flex-direction:column;gap:8px;display:flex}.statgrid{grid-template-columns:repeat(3,1fr);gap:6px;display:grid}.stat{background:var(--bg-mid);border-radius:6px;flex-direction:column;gap:1px;min-width:0;padding:5px 8px;display:flex}.stat span{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;font-size:10px}.stat b{text-overflow:ellipsis;font-size:15px;overflow:hidden}.statrow{align-items:center;gap:8px;min-width:0;font-size:12.5px;display:flex}.statrow .k{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;white-space:nowrap;flex-shrink:0;width:auto;min-width:58px;font-size:10px}.pill{background:var(--bg-mid);white-space:nowrap;border-radius:999px;margin-left:auto;padding:1px 8px;font-size:11px}.pill.ok{color:var(--ok)}.pill.warn{color:var(--warn)}.pill.crit{color:var(--err);background:#ed42451a}.bartrack{background:var(--bg-light);border-radius:3px;height:6px;margin-top:2px;overflow:hidden}.barfill{background:var(--ok);border-radius:3px;min-width:2px;height:100%;transition:width .3s}.barfill.warn{background:var(--warn)}.barfill.crit{background:var(--err)}.author{font-size:14.5px;font-weight:600}.ts{color:var(--dim);font-size:11px}.content{white-space:pre-wrap;word-break:break-word;line-height:1.45}.content p{margin:2px 0}.content pre{background:var(--bg-darkest);border-radius:6px;padding:8px;overflow-x:auto}.content code{background:var(--bg-darkest);border-radius:4px;padding:1px 4px;font-size:13px}.content pre code{background:0 0;padding:0}.content h1,.content h2,.content h3,.content h4,.content h5,.content h6{margin:8px 0 2px;font-size:15px}.content h1{font-size:16.5px}.content h2{font-size:15.5px}.content blockquote{border-left:3px solid var(--bg-light);color:var(--dim);background:#ffffff05;border-radius:0 4px 4px 0;margin:4px 0;padding:2px 10px}.content blockquote p{margin:2px 0}.content hr{border:none;border-top:1px solid var(--bg-light);margin:8px 0}.content del{color:var(--dim)}.content img{border-radius:6px;max-width:100%}.content li.task{margin-left:-18px;list-style:none}.content li:has(>input[type=checkbox]){margin-left:-18px;list-style:none}.content li input[type=checkbox]{accent-color:var(--ok);margin-right:6px}input[type=checkbox]{accent-color:var(--acc)}.content a{color:var(--tool);text-decoration:underline;-webkit-text-decoration-color:color-mix(in srgb, var(--tool) 45%, transparent);text-decoration-color:color-mix(in srgb, var(--tool) 45%, transparent);text-underline-offset:2px}.content a:hover{filter:brightness(1.15)}.themebtn.on{background:var(--acc);color:#fff}.trow select:focus{outline:1px solid var(--acc)}.tbl{margin:6px 0;overflow-x:auto}.tbl table{border-collapse:collapse;font-size:13px}.tbl th,.tbl td{border:1px solid var(--bg-light);text-align:left;padding:3px 9px}.tbl th{background:var(--bg-darkest);font-weight:600}.embed{border-left:3px solid var(--tool);background:var(--bg-darkest);border-radius:4px;margin-top:3px;padding:6px 10px;font-size:13.5px}.embed.fail{border-color:var(--err)}.embed.done{border-color:var(--ok)}.embed.running{border-color:var(--warn)}.embed.running summary .meta:after{content:"⋯";color:var(--warn);margin-left:6px;animation:1s step-end infinite blink;display:inline-block}.embed summary{gap:8px}.embed summary .mono{font-family:ui-monospace,Menlo,monospace}.toolbody{background:var(--bg-darkest);white-space:pre-wrap;word-break:break-word;border-radius:4px;max-height:340px;margin:4px 0;padding:6px 8px;font-size:12.5px;overflow-y:auto}.toolbody.del{border-left:2px solid var(--err);color:#f0a5a6;background:#ed424514}.toolbody.add{border-left:2px solid var(--ok);color:#9fd8ae;background:#3ba55d14}.toolbody.patch{padding:4px 8px}.toolbody.patch .pline{white-space:pre-wrap;word-break:break-word}.toolbody.patch .pline.add{color:#9fd8ae;border-left:2px solid var(--ok);background:#3ba55d14;padding-left:4px}.toolbody.patch .pline.del{color:#f0a5a6;border-left:2px solid var(--err);background:#ed424514;padding-left:4px}.toolbody.patch .pline.meta{color:var(--dim)}.embed.question{border-color:var(--acc);background:#5865f20f}.qopts{flex-wrap:wrap;gap:6px;margin-top:6px;display:flex}.qopt{background:var(--acc);color:#fff;cursor:pointer;border:none;border-radius:999px;padding:3px 12px;font-size:12.5px}.qopt:hover{filter:brightness(1.15)}.qopt:disabled{opacity:.45;cursor:default;filter:none}.embed.question.answered{opacity:.75}.qfree{gap:6px;margin-top:8px;display:flex}.qfree input{background:var(--bg-mid);border:1px solid var(--bg-light);color:var(--fg);border-radius:6px;outline:none;flex:1;padding:4px 10px;font-size:13px}.qfree input:focus{border-color:var(--acc)}.qfree button{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;padding:4px 12px;font-size:12.5px}.qfree button:disabled{opacity:.45;cursor:default}.bashelapsed{color:var(--warn);margin-left:8px;font-size:11px}.embed.question .meta{margin-top:6px}.embed.decision{border-color:var(--acc);background:#5865f20f}.embed.decision summary .meta{margin-left:6px}.embed.decision .content{margin-top:3px;font-size:13px}.embed summary{cursor:pointer;-webkit-user-select:none;user-select:none;list-style-position:outside}.embed summary::marker{color:var(--dim)}.embed[open] summary{margin-bottom:4px}.embed .mono{white-space:pre-wrap;word-break:break-word;max-height:340px;font-family:ui-monospace,Menlo,monospace;font-size:12.5px;overflow-y:auto}.embed .meta{color:var(--dim);margin-top:3px;font-size:11px}.reasoning{border-left:3px dashed var(--bg-light);color:var(--dim);background:#ffffff05;border-radius:4px;margin:2px 0;padding:2px 10px;font-size:12.5px}.reasoning summary{cursor:pointer;-webkit-user-select:none;user-select:none;opacity:.75}.reasoning summary:hover{opacity:1;color:var(--fg)}.reasoning[open] summary{margin-bottom:4px}.reasoning .mono{white-space:pre-wrap;word-break:break-word;max-height:260px;overflow-y:auto}.msg.live .avatar{animation:1.6s ease-in-out infinite pulse}.cursor{color:var(--acc);animation:1s step-end infinite blink}.interrupted{color:var(--warn);margin-top:3px;font-size:11.5px}@keyframes blink{50%{opacity:0}}@keyframes pulse{50%{opacity:.55}}.conn{background:var(--err);vertical-align:middle;width:8px;height:8px;box-shadow:0 0 6px var(--err);border-radius:50%;display:inline-block}.conn.ok{background:var(--ok);box-shadow:0 0 6px var(--ok)}.copybtn{border:1px solid var(--bg-light);color:var(--dim);cursor:pointer;background:0 0;border-radius:4px;flex-shrink:0;padding:0 5px;font-size:11px;line-height:16px}.copybtn:hover{color:var(--fg);filter:brightness(1.3)}.embed summary{align-items:center;gap:6px;display:flex}.msgfoot{color:var(--dim);align-items:center;gap:6px;margin-top:4px;font-size:11px;display:flex}.termdrawer{background:var(--term-bg);border-top:2px solid var(--line);flex-direction:column;display:flex}.termgrip{cursor:ns-resize;background:var(--bg-light);opacity:.6;border-bottom:1px solid var(--line);height:6px}.termgrip:hover{opacity:1}.termbar{color:var(--dim);border-bottom:1px solid var(--line);background:var(--bg-darkest);justify-content:space-between;align-items:center;padding:4px 10px;font-size:11.5px;display:flex}.termtabs{flex:1;align-items:center;gap:2px;min-width:0;display:flex;overflow-x:auto}.termtab{cursor:pointer;color:var(--dim);white-space:nowrap;background:var(--bg-mid);border-radius:5px;align-items:center;gap:6px;padding:2px 8px;font-size:11.5px;display:inline-flex}.termtab.active{color:var(--fg);outline:1px solid var(--acc)}.termtab:hover{filter:brightness(1.15)}.tabx{color:inherit;opacity:.5;cursor:pointer;background:0 0;border:none;padding:0;font-size:10px}.tabx:hover{opacity:1;color:var(--err)}.termbody{background:var(--term-bg);flex:1;gap:4px;min-height:0;padding:4px;display:flex}.termbody.split .termpane{width:50%}.termpane{background:var(--term-bg);border-radius:6px;flex:1;min-width:0;position:relative;overflow:hidden}.termpane:not(.focused){opacity:.9}.termpane.focused{box-shadow:inset 0 0 0 2px var(--acc)}.termhost{position:absolute;inset:0}.termsession{padding:4px 6px;position:absolute;inset:0}.termsplit{background:var(--line);cursor:col-resize;border-radius:2px;flex-shrink:0;width:3px}.termempty{color:var(--dim);place-items:center;font-size:12px;display:grid;position:absolute;inset:0}.jump{background:var(--acc);color:#fff;cursor:pointer;z-index:2;border:none;border-radius:999px;padding:6px 14px;font-size:12.5px;font-weight:600;position:absolute;top:-34px;left:50%;transform:translate(-50%);box-shadow:0 4px 14px #0008}.divider-msg{color:var(--dim);align-items:center;gap:10px;padding:4px 18px;font-size:11.5px;display:flex}.divider-msg:before,.divider-msg:after{content:"";background:var(--line);flex:1;height:1px}.divider-msg.err{color:var(--err)}.day-divider{align-items:center;gap:10px;padding:14px 18px 6px;display:flex}.day-divider:before,.day-divider:after{content:"";background:var(--bg-light);flex:1;height:1px}.day-divider span{color:var(--dim);font-size:11px}.composer{padding:0 16px 18px;position:relative}.composer form{background:var(--bg-light);border-radius:10px;align-items:flex-end;gap:8px;padding:10px 12px;display:flex}.composer textarea{color:var(--fg);font:inherit;resize:none;background:0 0;border:none;outline:none;flex:1;max-height:160px;line-height:1.4;overflow-y:auto}.cmds{background:var(--bg-mid);border:1px solid var(--bg-light);z-index:3;border-radius:8px;position:absolute;bottom:calc(100% - 8px);left:16px;right:16px;overflow:hidden;box-shadow:0 -6px 20px #0007}.cmdrow{cursor:pointer;align-items:baseline;gap:10px;padding:7px 12px;font-size:13.5px;display:flex}.cmdrow:hover{background:var(--bg-light)}.cmdrow.active{background:var(--acc);color:#fff}.cmdrow.active .muted{color:#e6e9ff}.cmdrow .muted{font-size:12px}.composer button{background:var(--acc);color:#fff;cursor:pointer;border:none;border-radius:8px;padding:7px 14px;font-weight:600}.composer button:hover{opacity:.9}.composer label{color:var(--dim);white-space:nowrap;align-items:center;gap:4px;font-size:12px;display:flex}.hint{color:var(--dim);margin-top:5px;font-size:11px}.rightbar{background:var(--bg-dark);border-left:2px solid var(--line);scrollbar-gutter:stable;flex-direction:column;min-height:0;padding:14px;font-size:13px;display:flex;overflow-y:auto}.rightbar>*{flex-shrink:0}.rightbar h3{text-transform:uppercase;letter-spacing:.04em;color:var(--dim);margin:14px 0 6px;font-size:11px}.rightbar h3:first-child{margin-top:0}.card{background:var(--bg-darkest);white-space:pre-wrap;word-break:break-word;border-radius:8px;max-height:200px;padding:10px;overflow-y:auto}.card.runcard{max-height:none;overflow-y:visible}.muted{color:var(--dim)}.prog .progrow{gap:8px;margin-bottom:4px;display:flex}.prog .progrow b{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;min-width:62px;padding-top:2px;font-size:10px}.embed .progrow{gap:8px;margin-bottom:4px;display:flex}.embed .progrow b{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;min-width:62px;padding-top:2px;font-size:10px}.embed .progrow span{min-width:0}.inline-md{display:inline}.inline-md>:first-child{margin-top:0}.inline-md>:last-child{margin-bottom:0}.prog .progrow.warn span{color:var(--err)}.msg-head .editbtn{color:var(--dim);cursor:pointer;opacity:0;background:0 0;border:none;padding:0 2px;font-size:11px;transition:opacity .12s}.msg:hover .msg-head .editbtn{opacity:1}.msg-head .editbtn:hover{color:var(--acc)}.branch-row{color:var(--dim);cursor:pointer;justify-content:space-between;padding:3px 0;font-size:12px;display:flex}.branch-row:hover,.branch-row.cur{color:var(--fg)}.mini-cron{opacity:.75;margin-left:auto;font-size:10px}.sched-row{cursor:pointer;border-radius:6px;margin-bottom:2px;padding:6px 8px}.sched-row:hover,.sched-row.cur{background:var(--bg-mid)}.sched-top{align-items:baseline;gap:6px;font-size:12.5px;display:flex}.sched-row .sched-meta{color:var(--dim);margin-top:2px;font-size:11px}.sched-row .sched-prompt{color:var(--dim);text-overflow:ellipsis;white-space:nowrap;margin-top:2px;font-size:11.5px;overflow:hidden}.badge.cron{color:var(--tool);white-space:nowrap;background:#3ba0c91a;border:1px solid #3ba0c944}.sesscard{flex-direction:column;gap:6px;font-size:12.5px;display:flex}.sessrow{align-items:center;gap:8px;display:flex}.filebox{background:var(--bg-darkest);border-radius:8px;max-height:260px;padding:4px;overflow-y:auto}.filerow{cursor:pointer;color:var(--fg);border-radius:4px;align-items:center;gap:6px;padding:3px 8px;font-size:12.5px;display:flex}.filerow:hover{background:var(--bg-mid)}.filerow .ficon{width:10px;color:var(--dim);text-align:center;flex-shrink:0;font-size:9px}.filerow.isdir .fname{font-weight:600}.fname{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.fsize{color:var(--dim);flex-shrink:0;margin-left:auto;font-size:10px}.sessrow .k{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;flex-shrink:0;width:74px;font-size:10px}.ellip{text-overflow:ellipsis;white-space:nowrap;text-align:left;direction:rtl;overflow:hidden}.modelbox{background:var(--bg-darkest);border-radius:8px;flex-direction:column;gap:6px;padding:10px;display:flex}.modelbox select,.modelbox input[type=text]{background:var(--bg-mid);color:var(--fg);font:inherit;border:none;border-radius:6px;outline:none;min-width:0;padding:6px 8px}.modelbox button{background:var(--acc);color:#fff;cursor:pointer;white-space:nowrap;border:none;border-radius:6px;padding:6px 10px;font-size:12.5px;font-weight:600}.modelbox button:hover{opacity:.9}.modelbox .meta{color:var(--dim);font-size:11px}.btnrow{gap:6px;margin:8px 0;display:flex}.btnrow button{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;padding:6px 10px;font-size:13px}.btnrow button:hover{filter:brightness(1.2)}.btnrow .runbtn{background:var(--ok);color:#fff;font-weight:600}.btnrow .danger{background:var(--err);color:#fff;font-weight:600}.ctrlrow{align-items:center;gap:8px;margin-top:6px;font-size:12.5px;display:flex}.ctrlrow label{cursor:pointer;color:var(--fg);white-space:nowrap;align-items:center;gap:5px;display:flex}.ctrlrow .muted{font-size:11.5px}.goalstate{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;padding:3px 10px;font-size:12px}.goalstate.on.active{background:var(--ok);color:#fff;font-weight:600}.goalstate.on.done{background:var(--acc);color:#fff;font-weight:600}.goalstate.on.paused{background:var(--warn);color:#fff;font-weight:600}.goalstate:disabled{cursor:default;filter:none}.verifycard{border-left:3px solid var(--tool);margin-top:6px}.auditcard{border-left:3px solid var(--warn);margin-top:6px}.auditcard.ok{border-left-color:var(--ok)}.vtitle{color:var(--dim);text-transform:uppercase;letter-spacing:.04em;align-items:center;gap:6px;margin-bottom:4px;font-size:12px;font-weight:600;display:flex}.iconbtn{background:var(--bg-light);color:var(--fg);cursor:pointer;border:none;border-radius:6px;width:26px;height:24px;font-size:13px}.iconbtn:hover{filter:brightness(1.3)}.overlay{z-index:10;background:#0009;place-items:center;display:grid;position:fixed;inset:0}.modal{background:var(--bg-mid);border-radius:10px;width:min(620px,92vw);max-height:88vh;padding:16px 18px;overflow-y:auto}.modal-head{justify-content:space-between;align-items:center;margin-bottom:12px;font-size:16px;display:flex}.modal label{color:var(--dim);flex-direction:column;gap:4px;font-size:12.5px;display:flex}.modal input[type=text],.modal input[type=number],.modal input[type=password],.modal input[type=url],.modal input[type=email],.modal select,.modal textarea{background:var(--bg-darkest);color:var(--fg);font:inherit;border:1px solid #0000;border-radius:6px;outline:none;padding:7px 9px}.modal input:focus,.modal select:focus,.modal textarea:focus{border-color:var(--acc)}.modal ::placeholder{color:#7c818f;opacity:1}.modal select option{background:var(--bg-darkest);color:var(--fg)}.modal input.compact{padding:5px 8px;font-size:12.5px}.modal textarea{resize:vertical}.w100{width:100%}.mono{font-family:ui-monospace,Menlo,monospace;font-size:13px}.modal fieldset{border:1px solid var(--bg-light);border-radius:8px;margin:0 0 4px;padding:10px}.modal fieldset legend{color:var(--dim);text-transform:uppercase;letter-spacing:.05em;padding:0 6px;font-size:11px}.cfgrow{flex-wrap:wrap;align-items:center;gap:6px;margin-bottom:6px;display:flex}.cfgrow input{background:var(--bg-darkest);border:1px solid var(--bg-light);color:var(--fg);border-radius:5px;flex:1;min-width:0;padding:5px 8px;font-size:12.5px}.cfgrow select{background:var(--bg-darkest);border:1px solid var(--bg-light);color:var(--fg);border-radius:5px;padding:5px 8px;font-size:12.5px}.cfgrow .cfgname{width:90px;color:var(--warn);flex:none;font-weight:600}.cfgrow button{background:var(--bg-mid);border:1px solid var(--bg-light);color:var(--fg);cursor:pointer;border-radius:5px;padding:4px 8px;font-size:12px}.cfgrow button:hover{background:var(--bg-light)}.cfgrow button.danger{background:var(--err);border-color:var(--err);color:#fff}.cfggrid{grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;display:grid}.cfggrid label,.cfgcol label{color:var(--dim);flex-direction:column;gap:3px;font-size:11px;display:flex}.cfggrid input{background:var(--bg-darkest);border:1px solid var(--bg-light);color:var(--fg);border-radius:5px;width:100%;padding:5px 8px}.cfgcol{margin-bottom:8px}.cfgcol textarea{background:var(--bg-darkest);border:1px solid var(--bg-light);color:var(--fg);font:inherit;border-radius:5px;width:100%;margin-top:4px;padding:5px 8px;font-size:12.5px}.presetbtn{background:var(--bg-light);color:var(--fg);cursor:pointer;border:1px solid #0000;border-radius:6px;flex:1;padding:7px 10px;font-size:13px}.presetbtn.active{background:var(--acc);color:#fff}.forkchip{white-space:nowrap;color:var(--dim);cursor:pointer;align-items:center;gap:4px;font-size:11.5px;display:flex}.dirlist{background:var(--bg-darkest);border-radius:6px;max-height:160px;padding:4px;overflow-y:auto}.direntry{cursor:pointer;border-radius:4px;padding:4px 8px;font-size:14px}.direntry:hover{background:var(--bg-light)}.filepreview{border-radius:8px;max-height:62vh;overflow:auto}.filetoolbar{gap:6px;margin-bottom:8px;display:flex}.filetoolbar button{background:var(--bg-light);color:var(--dim);cursor:pointer;border:none;border-radius:6px;padding:4px 12px;font-size:12.5px}.filetoolbar button:hover{color:var(--fg);filter:brightness(1.15)}.filetoolbar button.on{background:var(--acc);color:#fff;font-weight:600}.fileeditor{flex-direction:column;gap:8px;display:flex}.fileeditor textarea{background:var(--bg-darkest);min-height:55vh;max-height:65vh;color:var(--fg);border:1px solid var(--bg-light);white-space:pre;border-radius:8px;padding:10px;font-size:12.5px;line-height:1.45;overflow:auto}.filerow-actions{justify-content:flex-end;align-items:center;gap:10px;display:flex}.savebtn{background:var(--ok);color:#fff;cursor:pointer;border:none;border-radius:6px;padding:5px 14px;font-size:13px;font-weight:600}.savebtn:disabled{opacity:.4;cursor:default}.mdpreview{background:var(--bg-darkest);border-radius:8px;max-height:62vh;padding:14px 16px;overflow:auto}.filepreview .shiki,.filepreview .shiki span{color:var(--shiki-light);font-family:ui-monospace,Menlo,monospace;font-size:12.5px}html[data-appearance=dark] .filepreview .shiki,html[data-appearance=dark] .filepreview .shiki span{color:var(--shiki-dark)}.filepreview .shiki{background-color:var(--shiki-dark-bg);white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;margin:0;padding:10px 12px;font-size:12.5px}html[data-appearance=light] .filepreview .shiki{background-color:var(--shiki-light-bg)}.notifbtn{position:relative}.notifbtn.has-unread{color:var(--acc)}.notifdot{background:var(--err);color:#fff;border-radius:999px;padding:2px 4px;font-size:9px;font-style:normal;font-weight:700;line-height:1;position:absolute;top:-4px;right:-4px}.brand-row{align-items:center;margin-top:2px;display:flex}.notifpanel{z-index:8;background:var(--bg-mid);border:1px solid var(--bg-light);border-radius:10px;width:min(360px,88vw);position:absolute;bottom:76px;left:8px;overflow:hidden;box-shadow:0 -8px 30px #000a}.notifhead{border-bottom:1px solid var(--line);text-transform:uppercase;letter-spacing:.04em;color:var(--dim);justify-content:space-between;align-items:center;padding:8px 12px;font-size:11px;display:flex}.notiflist{max-height:46vh;overflow-y:auto}.notifitem{border-bottom:1px solid var(--line);cursor:pointer;padding:8px 12px}.notifitem:last-child{border-bottom:none}.notifitem:hover{background:var(--bg-light)}.notifitem .nhead{align-items:baseline;gap:6px;min-width:0;display:flex}.notifitem .ntitle{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;font-size:13px;font-weight:600;overflow:hidden}.notifitem.error .ntitle{color:var(--err)}.notifitem.finish .ntitle{color:var(--ok)}.notifitem.question .ntitle{color:var(--acc)}.notifitem .nbody{color:var(--dim);word-break:break-word;margin-top:3px;font-size:12px}.subcount{color:var(--dim);background:var(--bg-light);white-space:nowrap;border-radius:999px;flex-shrink:0;padding:1px 7px;font-size:10px}.subcount.live{color:var(--ok)}.xterm{cursor:text;-webkit-user-select:none;user-select:none;position:relative}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{z-index:5;position:absolute;top:0}.xterm .xterm-helper-textarea{opacity:0;z-index:-5;white-space:nowrap;resize:none;border:0;width:0;height:0;margin:0;padding:0;position:absolute;top:0;left:-9999em;overflow:hidden}.xterm .composition-view{color:#fff;white-space:nowrap;z-index:1;background:#000;display:none;position:absolute}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{cursor:default;background-color:#000;position:absolute;inset:0;overflow-y:scroll}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;top:0;left:0}.xterm-char-measure-element{visibility:hidden;line-height:normal;display:inline-block;position:absolute;top:0;left:-9999em}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{z-index:10;color:#0000;pointer-events:none;position:absolute;inset:0}.xterm .xterm-accessibility-tree:not(.debug) ::selection{color:#0000}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre;font-family:monospace}.xterm .xterm-accessibility-tree>div{transform-origin:0;width:fit-content}.xterm .live-region{width:1px;height:1px;position:absolute;left:-9999px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{-webkit-text-decoration:underline double;text-decoration:underline double}.xterm-underline-3{-webkit-text-decoration:underline wavy;text-decoration:underline wavy}.xterm-underline-4{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.xterm-underline-5{-webkit-text-decoration:underline dashed;text-decoration:underline dashed}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:underline overline}.xterm-overline.xterm-underline-2{-webkit-text-decoration:overline double underline;text-decoration:overline double underline}.xterm-overline.xterm-underline-3{-webkit-text-decoration:overline wavy underline;text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{-webkit-text-decoration:overline dotted underline;text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{-webkit-text-decoration:overline dashed underline;text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;pointer-events:none;position:absolute;top:0;right:0}.xterm-decoration-top{z-index:2;position:relative}.xterm .xterm-scrollable-element>.scrollbar{cursor:default}.xterm .xterm-scrollable-element>.scrollbar>.scra{cursor:pointer;font-size:11px!important}.xterm .xterm-scrollable-element>.visible{opacity:1;z-index:11;background:0 0;transition:opacity .1s linear}.xterm .xterm-scrollable-element>.invisible{opacity:0;pointer-events:none}.xterm .xterm-scrollable-element>.invisible.fade{transition:opacity .8s linear}.xterm .xterm-scrollable-element>.shadow{display:none;position:absolute}.xterm .xterm-scrollable-element>.shadow.top{width:100%;height:3px;box-shadow:var(--vscode-scrollbar-shadow,#000) 0 6px 6px -6px inset;display:block;top:0;left:3px}.xterm .xterm-scrollable-element>.shadow.left{width:3px;height:100%;box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset;display:block;top:3px;left:0}.xterm .xterm-scrollable-element>.shadow.top-left-corner{width:3px;height:3px;display:block;top:0;left:0}.xterm .xterm-scrollable-element>.shadow.top.left{box-shadow:var(--vscode-scrollbar-shadow,#000) 6px 0 6px -6px inset}