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.
- package/dist/agent/agent.js +202 -16
- package/dist/agent/tools.js +6 -1
- package/dist/config-schema.js +2 -1
- package/dist/log/events.js +5 -1
- package/dist/master.js +16 -6
- package/dist/server/api.js +62 -7
- package/package.json +1 -1
- package/public/assets/index-BQv6XAjp.css +1 -0
- package/public/assets/index-BhS7khnw.js +28 -0
- package/public/assets/index-BhS7khnw.js.map +1 -0
- package/public/assets/{shiki-BBO6_R27.js → shiki-DgiMDhfC.js} +2 -2
- package/public/assets/{shiki-BBO6_R27.js.map → shiki-DgiMDhfC.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-BXyhr5r5.css +0 -1
- package/public/assets/index-CG4aaYbK.js +0 -28
- package/public/assets/index-CG4aaYbK.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -469,7 +469,17 @@ export class Agent {
|
|
|
469
469
|
break;
|
|
470
470
|
body = stripped;
|
|
471
471
|
}
|
|
472
|
-
|
|
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
|
|
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: {
|
|
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, {
|
|
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
|
-
/**
|
|
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
|
|
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({
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
1664
|
-
|
|
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;
|
package/dist/agent/tools.js
CHANGED
|
@@ -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);
|
package/dist/config-schema.js
CHANGED
|
@@ -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
|
-
|
|
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(),
|
package/dist/log/events.js
CHANGED
|
@@ -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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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) =>
|
|
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
|
}
|
package/dist/server/api.js
CHANGED
|
@@ -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 = (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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}
|