agent-relay-server 0.11.4 → 0.11.6
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/package.json +5 -3
- package/public/index.html +319 -26
- package/runner/src/adapter.ts +4 -1
- package/src/db.ts +22 -0
- package/src/maintenance.ts +100 -1
- package/src/routes.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -32,14 +32,16 @@
|
|
|
32
32
|
"CONTRIBUTING.md"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"agent-relay-sdk": "0.2.
|
|
35
|
+
"agent-relay-sdk": "0.2.4"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
+
"prepack": "bun run build:dashboard >&2",
|
|
38
39
|
"postinstall": "node scripts/install-bin-shim.cjs",
|
|
39
40
|
"start": "bun run src/index.ts",
|
|
40
41
|
"dev": "bun --watch run src/index.ts",
|
|
41
42
|
"dev:dashboard": "cd dashboard && npx vite",
|
|
42
|
-
"build:
|
|
43
|
+
"build:sdk": "cd sdk && bun run build",
|
|
44
|
+
"build:dashboard": "bun run build:sdk && cd dashboard && npx vite build",
|
|
43
45
|
"test": "bun test",
|
|
44
46
|
"smoke:spawn": "bun run scripts/orchestrator-spawn-smoke.ts",
|
|
45
47
|
"typecheck": "tsc --noEmit",
|
package/public/index.html
CHANGED
|
@@ -10753,13 +10753,23 @@ function providerPendingApproval(value) {
|
|
|
10753
10753
|
id: choice.id,
|
|
10754
10754
|
label: typeof choice.label === "string" ? choice.label : String(choice.id || "")
|
|
10755
10755
|
})).filter((choice) => (choice.id === "approve" || choice.id === "approve-session" || choice.id === "deny" || choice.id === "abort") && Boolean(choice.label)) : [];
|
|
10756
|
+
const questions = Array.isArray(value.questions) ? value.questions.filter(isRecord$2).map((q) => ({
|
|
10757
|
+
question: typeof q.question === "string" ? q.question : "",
|
|
10758
|
+
header: typeof q.header === "string" ? q.header : void 0,
|
|
10759
|
+
multiSelect: q.multiSelect === true,
|
|
10760
|
+
options: Array.isArray(q.options) ? q.options.filter(isRecord$2).map((o) => ({
|
|
10761
|
+
label: typeof o.label === "string" ? o.label : String(o.label ?? ""),
|
|
10762
|
+
description: typeof o.description === "string" ? o.description : void 0
|
|
10763
|
+
})).filter((o) => Boolean(o.label)) : []
|
|
10764
|
+
})).filter((q) => Boolean(q.question) && q.options.length > 0) : void 0;
|
|
10756
10765
|
return {
|
|
10757
10766
|
id: value.id,
|
|
10758
10767
|
provider: typeof value.provider === "string" ? value.provider : void 0,
|
|
10759
10768
|
kind: typeof value.kind === "string" ? value.kind : void 0,
|
|
10760
10769
|
title: typeof value.title === "string" && value.title ? value.title : "Permission request",
|
|
10761
10770
|
body: typeof value.body === "string" ? value.body : "",
|
|
10762
|
-
choices
|
|
10771
|
+
choices,
|
|
10772
|
+
...questions && questions.length ? { questions } : {}
|
|
10763
10773
|
};
|
|
10764
10774
|
}
|
|
10765
10775
|
function providerBlockedState(agent) {
|
|
@@ -12883,14 +12893,15 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12883
12893
|
}
|
|
12884
12894
|
set({ chatSending: false });
|
|
12885
12895
|
},
|
|
12886
|
-
async sendPermissionDecision(agentId, approvalId, decision) {
|
|
12896
|
+
async sendPermissionDecision(agentId, approvalId, decision, answers) {
|
|
12887
12897
|
try {
|
|
12888
12898
|
await api("POST", "/agents/" + encodeURIComponent(agentId) + "/permission-decision", {
|
|
12889
12899
|
approvalId,
|
|
12890
|
-
decision
|
|
12900
|
+
decision,
|
|
12901
|
+
...answers ? { answers } : {}
|
|
12891
12902
|
});
|
|
12892
|
-
const label = decision === "approve" ? "Approved" : decision === "approve-session" ? "Approved for session" : decision === "abort" ? "Aborted" : "Denied";
|
|
12893
|
-
get().showNotification(`${label} permission request`);
|
|
12903
|
+
const label = decision === "approve" ? "Approved" : decision === "approve-session" ? "Approved for session" : decision === "abort" ? "Aborted" : decision === "answer" ? "Answer sent for" : "Denied";
|
|
12904
|
+
get().showNotification(`${label} ${decision === "answer" ? "Claude's question" : "permission request"}`);
|
|
12894
12905
|
await get().fetchAgents();
|
|
12895
12906
|
} catch (e) {
|
|
12896
12907
|
get().showError("Permission Failed", e.message);
|
|
@@ -119805,6 +119816,15 @@ function FileOverlay() {
|
|
|
119805
119816
|
});
|
|
119806
119817
|
}
|
|
119807
119818
|
//#endregion
|
|
119819
|
+
//#region src/components/ui/textarea.tsx
|
|
119820
|
+
function Textarea({ className, ...props }) {
|
|
119821
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", {
|
|
119822
|
+
"data-slot": "textarea",
|
|
119823
|
+
className: cn$1("flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className),
|
|
119824
|
+
...props
|
|
119825
|
+
});
|
|
119826
|
+
}
|
|
119827
|
+
//#endregion
|
|
119808
119828
|
//#region src/components/views/chat.tsx
|
|
119809
119829
|
function formatBytes(size) {
|
|
119810
119830
|
if (size < 1024) return `${size} B`;
|
|
@@ -120434,6 +120454,11 @@ function PermissionRequestBubble({ agentId, approval }) {
|
|
|
120434
120454
|
const sendPermissionDecision = useRelayStore((s) => s.sendPermissionDecision);
|
|
120435
120455
|
const [submitting, setSubmitting] = (0, import_react.useState)(null);
|
|
120436
120456
|
if (!approval) return null;
|
|
120457
|
+
if (approval.questions && approval.questions.length) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(QuestionForm, {
|
|
120458
|
+
agentId,
|
|
120459
|
+
approval,
|
|
120460
|
+
questions: approval.questions
|
|
120461
|
+
});
|
|
120437
120462
|
const currentApproval = approval;
|
|
120438
120463
|
const choices = approval.choices.length ? approval.choices : [{
|
|
120439
120464
|
id: "approve",
|
|
@@ -120485,6 +120510,217 @@ function PermissionRequestBubble({ agentId, approval }) {
|
|
|
120485
120510
|
})
|
|
120486
120511
|
});
|
|
120487
120512
|
}
|
|
120513
|
+
function questionAnswered(s) {
|
|
120514
|
+
if (!s) return false;
|
|
120515
|
+
if (s.otherActive && s.other.trim()) return true;
|
|
120516
|
+
return s.labels.length > 0;
|
|
120517
|
+
}
|
|
120518
|
+
function answerValue(s) {
|
|
120519
|
+
const parts = [...s.labels];
|
|
120520
|
+
if (s.otherActive && s.other.trim()) parts.push(s.other.trim());
|
|
120521
|
+
return parts.join(", ");
|
|
120522
|
+
}
|
|
120523
|
+
function QuestionForm({ agentId, approval, questions }) {
|
|
120524
|
+
const sendPermissionDecision = useRelayStore((s) => s.sendPermissionDecision);
|
|
120525
|
+
const [step, setStep] = (0, import_react.useState)(0);
|
|
120526
|
+
const [state, setState] = (0, import_react.useState)({});
|
|
120527
|
+
const [submitting, setSubmitting] = (0, import_react.useState)(null);
|
|
120528
|
+
const q = questions[Math.min(step, questions.length - 1)];
|
|
120529
|
+
const cur = state[step];
|
|
120530
|
+
if (!q) return null;
|
|
120531
|
+
const multi = Boolean(q.multiSelect);
|
|
120532
|
+
const total = questions.length;
|
|
120533
|
+
const isLast = step === total - 1;
|
|
120534
|
+
const answered = questionAnswered(cur);
|
|
120535
|
+
const allAnswered = questions.every((_, i) => questionAnswered(state[i]));
|
|
120536
|
+
const busy = Boolean(submitting);
|
|
120537
|
+
function update(patch) {
|
|
120538
|
+
setState((prev) => ({
|
|
120539
|
+
...prev,
|
|
120540
|
+
[step]: {
|
|
120541
|
+
labels: [],
|
|
120542
|
+
other: "",
|
|
120543
|
+
otherActive: false,
|
|
120544
|
+
...prev[step],
|
|
120545
|
+
...patch
|
|
120546
|
+
}
|
|
120547
|
+
}));
|
|
120548
|
+
}
|
|
120549
|
+
function pickOption(label) {
|
|
120550
|
+
if (multi) {
|
|
120551
|
+
const has = cur?.labels.includes(label);
|
|
120552
|
+
update({ labels: has ? (cur?.labels ?? []).filter((l) => l !== label) : [...cur?.labels ?? [], label] });
|
|
120553
|
+
} else update({
|
|
120554
|
+
labels: [label],
|
|
120555
|
+
otherActive: false
|
|
120556
|
+
});
|
|
120557
|
+
}
|
|
120558
|
+
function pickOther() {
|
|
120559
|
+
if (multi) update({ otherActive: !cur?.otherActive });
|
|
120560
|
+
else update({
|
|
120561
|
+
labels: [],
|
|
120562
|
+
otherActive: true
|
|
120563
|
+
});
|
|
120564
|
+
}
|
|
120565
|
+
async function submit() {
|
|
120566
|
+
if (!allAnswered || busy) return;
|
|
120567
|
+
setSubmitting("answer");
|
|
120568
|
+
try {
|
|
120569
|
+
const answers = {};
|
|
120570
|
+
questions.forEach((qq, i) => {
|
|
120571
|
+
const s = state[i];
|
|
120572
|
+
if (s) answers[qq.question] = answerValue(s);
|
|
120573
|
+
});
|
|
120574
|
+
await sendPermissionDecision(agentId, approval.id, "answer", answers);
|
|
120575
|
+
} finally {
|
|
120576
|
+
setSubmitting(null);
|
|
120577
|
+
}
|
|
120578
|
+
}
|
|
120579
|
+
async function dismiss() {
|
|
120580
|
+
if (busy) return;
|
|
120581
|
+
setSubmitting("dismiss");
|
|
120582
|
+
try {
|
|
120583
|
+
await sendPermissionDecision(agentId, approval.id, "deny");
|
|
120584
|
+
} finally {
|
|
120585
|
+
setSubmitting(null);
|
|
120586
|
+
}
|
|
120587
|
+
}
|
|
120588
|
+
const otherActive = Boolean(cur?.otherActive);
|
|
120589
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
120590
|
+
className: "flex justify-start mb-3",
|
|
120591
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
120592
|
+
className: "w-full max-w-[92%] md:max-w-[78%] rounded-2xl rounded-bl-sm bg-card ring-1 ring-amber-500/35 px-3.5 py-3 text-sm",
|
|
120593
|
+
children: [
|
|
120594
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
120595
|
+
className: "flex items-center gap-2",
|
|
120596
|
+
children: [
|
|
120597
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageCircleQuestionMark, { className: "h-4 w-4 shrink-0 text-amber-400" }),
|
|
120598
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
120599
|
+
className: "text-sm font-medium text-amber-100",
|
|
120600
|
+
children: approval.title
|
|
120601
|
+
}),
|
|
120602
|
+
total > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Badge$1, {
|
|
120603
|
+
variant: "outline",
|
|
120604
|
+
className: "ml-auto h-5 px-1.5 text-[10px] text-muted-foreground",
|
|
120605
|
+
children: [
|
|
120606
|
+
step + 1,
|
|
120607
|
+
" / ",
|
|
120608
|
+
total
|
|
120609
|
+
]
|
|
120610
|
+
})
|
|
120611
|
+
]
|
|
120612
|
+
}),
|
|
120613
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
120614
|
+
className: "mt-3",
|
|
120615
|
+
children: [
|
|
120616
|
+
q.header && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
120617
|
+
className: "text-[11px] font-semibold uppercase tracking-wide text-amber-300/80",
|
|
120618
|
+
children: q.header
|
|
120619
|
+
}),
|
|
120620
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
120621
|
+
className: "mt-0.5 text-sm text-foreground whitespace-pre-wrap break-words",
|
|
120622
|
+
children: q.question
|
|
120623
|
+
}),
|
|
120624
|
+
multi && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
120625
|
+
className: "mt-0.5 text-[11px] text-muted-foreground",
|
|
120626
|
+
children: "Select all that apply"
|
|
120627
|
+
}),
|
|
120628
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
120629
|
+
className: "mt-2 flex flex-col gap-1.5",
|
|
120630
|
+
children: [
|
|
120631
|
+
q.options.map((opt) => {
|
|
120632
|
+
const selected = Boolean(cur?.labels.includes(opt.label));
|
|
120633
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
120634
|
+
type: "button",
|
|
120635
|
+
disabled: busy,
|
|
120636
|
+
onClick: () => pickOption(opt.label),
|
|
120637
|
+
className: cn$1("flex items-start gap-2 rounded-lg border px-2.5 py-2 text-left transition-colors", selected ? "border-amber-500/60 bg-amber-500/10" : "border-border bg-background/60 hover:bg-background", busy && "opacity-60"),
|
|
120638
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
120639
|
+
className: cn$1("mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center border text-amber-300", multi ? "rounded" : "rounded-full", selected ? "border-amber-400 bg-amber-400/20" : "border-muted-foreground/40"),
|
|
120640
|
+
children: selected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3 w-3" })
|
|
120641
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
120642
|
+
className: "min-w-0",
|
|
120643
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
120644
|
+
className: "text-sm text-foreground",
|
|
120645
|
+
children: opt.label
|
|
120646
|
+
}), opt.description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
120647
|
+
className: "block text-xs text-muted-foreground",
|
|
120648
|
+
children: opt.description
|
|
120649
|
+
})]
|
|
120650
|
+
})]
|
|
120651
|
+
}, opt.label);
|
|
120652
|
+
}),
|
|
120653
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
|
|
120654
|
+
type: "button",
|
|
120655
|
+
disabled: busy,
|
|
120656
|
+
onClick: pickOther,
|
|
120657
|
+
className: cn$1("flex items-start gap-2 rounded-lg border px-2.5 py-2 text-left transition-colors", otherActive ? "border-amber-500/60 bg-amber-500/10" : "border-border bg-background/60 hover:bg-background", busy && "opacity-60"),
|
|
120658
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
120659
|
+
className: cn$1("mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center border text-amber-300", multi ? "rounded" : "rounded-full", otherActive ? "border-amber-400 bg-amber-400/20" : "border-muted-foreground/40"),
|
|
120660
|
+
children: otherActive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3 w-3" })
|
|
120661
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
120662
|
+
className: "text-sm text-foreground",
|
|
120663
|
+
children: "Other…"
|
|
120664
|
+
})]
|
|
120665
|
+
}),
|
|
120666
|
+
otherActive && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Textarea, {
|
|
120667
|
+
autoFocus: true,
|
|
120668
|
+
rows: 2,
|
|
120669
|
+
disabled: busy,
|
|
120670
|
+
value: cur?.other ?? "",
|
|
120671
|
+
onChange: (e) => update({
|
|
120672
|
+
other: e.target.value,
|
|
120673
|
+
otherActive: true
|
|
120674
|
+
}),
|
|
120675
|
+
placeholder: "Type your answer",
|
|
120676
|
+
className: "mt-1 text-sm"
|
|
120677
|
+
})
|
|
120678
|
+
]
|
|
120679
|
+
})
|
|
120680
|
+
]
|
|
120681
|
+
}),
|
|
120682
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
120683
|
+
className: "mt-3 flex items-center gap-1.5",
|
|
120684
|
+
children: [
|
|
120685
|
+
step > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
120686
|
+
size: "sm",
|
|
120687
|
+
variant: "outline",
|
|
120688
|
+
disabled: busy,
|
|
120689
|
+
onClick: () => setStep((s) => s - 1),
|
|
120690
|
+
className: "h-7 px-2 text-xs",
|
|
120691
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronLeft, { className: "mr-1 h-3 w-3" }), "Back"]
|
|
120692
|
+
}),
|
|
120693
|
+
!isLast ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
120694
|
+
size: "sm",
|
|
120695
|
+
disabled: busy || !answered,
|
|
120696
|
+
onClick: () => setStep((s) => s + 1),
|
|
120697
|
+
className: "h-7 px-2 text-xs",
|
|
120698
|
+
children: ["Next", /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChevronRight, { className: "ml-1 h-3 w-3" })]
|
|
120699
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
120700
|
+
size: "sm",
|
|
120701
|
+
disabled: busy || !allAnswered,
|
|
120702
|
+
onClick: () => void submit(),
|
|
120703
|
+
className: "h-7 px-2 text-xs",
|
|
120704
|
+
children: [
|
|
120705
|
+
submitting === "answer" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "mr-1.5 h-3 w-3 animate-spin" }),
|
|
120706
|
+
"Send answer",
|
|
120707
|
+
total > 1 ? "s" : ""
|
|
120708
|
+
]
|
|
120709
|
+
}),
|
|
120710
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
120711
|
+
size: "sm",
|
|
120712
|
+
variant: "ghost",
|
|
120713
|
+
disabled: busy,
|
|
120714
|
+
onClick: () => void dismiss(),
|
|
120715
|
+
className: "ml-auto h-7 px-2 text-xs text-muted-foreground",
|
|
120716
|
+
children: [submitting === "dismiss" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoaderCircle, { className: "mr-1.5 h-3 w-3 animate-spin" }), "Dismiss"]
|
|
120717
|
+
})
|
|
120718
|
+
]
|
|
120719
|
+
})
|
|
120720
|
+
]
|
|
120721
|
+
})
|
|
120722
|
+
});
|
|
120723
|
+
}
|
|
120488
120724
|
function DateSeparator({ date }) {
|
|
120489
120725
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
120490
120726
|
className: "flex items-center justify-center gap-3 py-3 my-1",
|
|
@@ -123101,7 +123337,9 @@ function WorkspaceActions({ workspace, expanded, onToggleDetails }) {
|
|
|
123101
123337
|
const terminal = workspace.status === "cleaned" || workspace.status === "merged" || workspace.status === "abandoned";
|
|
123102
123338
|
const disabled = terminal || workspace.status === "cleanup_requested";
|
|
123103
123339
|
const openPath = workspace.worktreePath || workspace.sourceCwd || workspace.repoRoot;
|
|
123104
|
-
const
|
|
123340
|
+
const gitState = useRelayStore((s) => s.workspaceGitState[workspace.id]);
|
|
123341
|
+
const landed = !!gitState && gitState.available !== false && gitState.landed === true;
|
|
123342
|
+
const mergeable = workspace.mode === "isolated" && Boolean(workspace.worktreePath) && MERGEABLE_STATUSES.has(workspace.status) && !landed;
|
|
123105
123343
|
async function copyPath() {
|
|
123106
123344
|
await navigator.clipboard?.writeText(openPath);
|
|
123107
123345
|
}
|
|
@@ -123162,7 +123400,7 @@ function WorkspaceActions({ workspace, expanded, onToggleDetails }) {
|
|
|
123162
123400
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
123163
123401
|
size: "icon-sm",
|
|
123164
123402
|
variant: "default",
|
|
123165
|
-
title: "Merge & land work",
|
|
123403
|
+
title: landed ? "Already merged into base — nothing to land" : "Merge & land work",
|
|
123166
123404
|
disabled: !mergeable,
|
|
123167
123405
|
onClick: () => void merge(),
|
|
123168
123406
|
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(GitMerge, { className: "h-3.5 w-3.5" })
|
|
@@ -123185,10 +123423,10 @@ function WorkspaceActions({ workspace, expanded, onToggleDetails }) {
|
|
|
123185
123423
|
]
|
|
123186
123424
|
});
|
|
123187
123425
|
}
|
|
123188
|
-
function MergePreviewHint({ workspace,
|
|
123426
|
+
function MergePreviewHint({ workspace, unmerged }) {
|
|
123189
123427
|
const preview = useRelayStore((s) => s.workspaceMergePreview[workspace.id]);
|
|
123190
123428
|
const fetchWorkspaceMergePreview = useRelayStore((s) => s.fetchWorkspaceMergePreview);
|
|
123191
|
-
const eligible =
|
|
123429
|
+
const eligible = unmerged > 0 && MERGEABLE_STATUSES.has(workspace.status);
|
|
123192
123430
|
(0, import_react.useEffect)(() => {
|
|
123193
123431
|
if (eligible && preview === void 0) fetchWorkspaceMergePreview(workspace.id);
|
|
123194
123432
|
}, [
|
|
@@ -123251,9 +123489,11 @@ function GitState({ workspace }) {
|
|
|
123251
123489
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: "git error" }), refresh]
|
|
123252
123490
|
});
|
|
123253
123491
|
const ahead = gitState.ahead ?? 0;
|
|
123492
|
+
const landed = gitState.landed === true;
|
|
123493
|
+
const unmerged = landed ? 0 : gitState.unmergedAhead ?? ahead;
|
|
123254
123494
|
const behind = gitState.behind ?? 0;
|
|
123255
123495
|
const dirty = gitState.dirtyCount ?? 0;
|
|
123256
|
-
const clean =
|
|
123496
|
+
const clean = unmerged === 0 && dirty === 0 && !landed;
|
|
123257
123497
|
const commit = gitState.lastCommit;
|
|
123258
123498
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
123259
123499
|
className: "space-y-0.5",
|
|
@@ -123263,10 +123503,14 @@ function GitState({ workspace }) {
|
|
|
123263
123503
|
className: "text-muted-foreground",
|
|
123264
123504
|
children: "no unmerged work"
|
|
123265
123505
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
123266
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
123267
|
-
className:
|
|
123268
|
-
title:
|
|
123269
|
-
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
123506
|
+
landed ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
123507
|
+
className: "flex items-center gap-0.5 text-emerald-400",
|
|
123508
|
+
title: `Work already landed in ${gitState.baseRef || "base"} via squash/cherry-pick — safe to clean up`,
|
|
123509
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Check, { className: "h-3 w-3" }), "landed"]
|
|
123510
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
123511
|
+
className: unmerged > 0 ? "flex items-center text-emerald-400" : "flex items-center text-muted-foreground",
|
|
123512
|
+
title: ahead !== unmerged ? `${unmerged} unmerged commit(s) (${ahead} ahead of base)` : `${unmerged} commit(s) ahead of base`,
|
|
123513
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowUp, { className: "h-3 w-3" }), unmerged]
|
|
123270
123514
|
}),
|
|
123271
123515
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
123272
123516
|
className: behind > 0 ? "flex items-center text-amber-400" : "flex items-center text-muted-foreground",
|
|
@@ -123278,9 +123522,9 @@ function GitState({ workspace }) {
|
|
|
123278
123522
|
title: `${dirty} uncommitted change(s)`,
|
|
123279
123523
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FilePen, { className: "h-3 w-3" }), dirty]
|
|
123280
123524
|
}),
|
|
123281
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MergePreviewHint, {
|
|
123525
|
+
!landed && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MergePreviewHint, {
|
|
123282
123526
|
workspace,
|
|
123283
|
-
|
|
123527
|
+
unmerged
|
|
123284
123528
|
})
|
|
123285
123529
|
] }), refresh]
|
|
123286
123530
|
}), commit && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
@@ -124287,15 +124531,6 @@ function IntegrationsView() {
|
|
|
124287
124531
|
});
|
|
124288
124532
|
}
|
|
124289
124533
|
//#endregion
|
|
124290
|
-
//#region src/components/ui/textarea.tsx
|
|
124291
|
-
function Textarea({ className, ...props }) {
|
|
124292
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("textarea", {
|
|
124293
|
-
"data-slot": "textarea",
|
|
124294
|
-
className: cn$1("flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className),
|
|
124295
|
-
...props
|
|
124296
|
-
});
|
|
124297
|
-
}
|
|
124298
|
-
//#endregion
|
|
124299
124534
|
//#region src/components/views/security.tsx
|
|
124300
124535
|
var scopeOptions = [
|
|
124301
124536
|
"admin:*",
|
|
@@ -153328,6 +153563,10 @@ if ("serviceWorker" in navigator) {
|
|
|
153328
153563
|
border-style: dashed;
|
|
153329
153564
|
}
|
|
153330
153565
|
|
|
153566
|
+
.border-amber-400 {
|
|
153567
|
+
border-color: var(--color-amber-400);
|
|
153568
|
+
}
|
|
153569
|
+
|
|
153331
153570
|
.border-amber-500\/15 {
|
|
153332
153571
|
border-color: #f99c0026;
|
|
153333
153572
|
}
|
|
@@ -153368,6 +153607,16 @@ if ("serviceWorker" in navigator) {
|
|
|
153368
153607
|
}
|
|
153369
153608
|
}
|
|
153370
153609
|
|
|
153610
|
+
.border-amber-500\/60 {
|
|
153611
|
+
border-color: #f99c0099;
|
|
153612
|
+
}
|
|
153613
|
+
|
|
153614
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
153615
|
+
.border-amber-500\/60 {
|
|
153616
|
+
border-color: color-mix(in oklab, var(--color-amber-500) 60%, transparent);
|
|
153617
|
+
}
|
|
153618
|
+
}
|
|
153619
|
+
|
|
153371
153620
|
.border-amber-800\/60 {
|
|
153372
153621
|
border-color: #953d0099;
|
|
153373
153622
|
}
|
|
@@ -153522,6 +153771,16 @@ if ("serviceWorker" in navigator) {
|
|
|
153522
153771
|
border-color: var(--input);
|
|
153523
153772
|
}
|
|
153524
153773
|
|
|
153774
|
+
.border-muted-foreground\/40 {
|
|
153775
|
+
border-color: var(--muted-foreground);
|
|
153776
|
+
}
|
|
153777
|
+
|
|
153778
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
153779
|
+
.border-muted-foreground\/40 {
|
|
153780
|
+
border-color: color-mix(in oklab, var(--muted-foreground) 40%, transparent);
|
|
153781
|
+
}
|
|
153782
|
+
}
|
|
153783
|
+
|
|
153525
153784
|
.border-orange-500\/20 {
|
|
153526
153785
|
border-color: #fe6e0033;
|
|
153527
153786
|
}
|
|
@@ -153776,6 +154035,16 @@ if ("serviceWorker" in navigator) {
|
|
|
153776
154035
|
background-color: var(--color-amber-400);
|
|
153777
154036
|
}
|
|
153778
154037
|
|
|
154038
|
+
.bg-amber-400\/20 {
|
|
154039
|
+
background-color: #fcbb0033;
|
|
154040
|
+
}
|
|
154041
|
+
|
|
154042
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
154043
|
+
.bg-amber-400\/20 {
|
|
154044
|
+
background-color: color-mix(in oklab, var(--color-amber-400) 20%, transparent);
|
|
154045
|
+
}
|
|
154046
|
+
}
|
|
154047
|
+
|
|
153779
154048
|
.bg-amber-500\/10 {
|
|
153780
154049
|
background-color: #f99c001a;
|
|
153781
154050
|
}
|
|
@@ -153796,7 +154065,17 @@ if ("serviceWorker" in navigator) {
|
|
|
153796
154065
|
}
|
|
153797
154066
|
}
|
|
153798
154067
|
|
|
153799
|
-
.bg-background, .bg-background\/
|
|
154068
|
+
.bg-background, .bg-background\/60 {
|
|
154069
|
+
background-color: var(--background);
|
|
154070
|
+
}
|
|
154071
|
+
|
|
154072
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
154073
|
+
.bg-background\/60 {
|
|
154074
|
+
background-color: color-mix(in oklab, var(--background) 60%, transparent);
|
|
154075
|
+
}
|
|
154076
|
+
}
|
|
154077
|
+
|
|
154078
|
+
.bg-background\/80 {
|
|
153800
154079
|
background-color: var(--background);
|
|
153801
154080
|
}
|
|
153802
154081
|
|
|
@@ -154822,6 +155101,16 @@ if ("serviceWorker" in navigator) {
|
|
|
154822
155101
|
color: var(--color-amber-300);
|
|
154823
155102
|
}
|
|
154824
155103
|
|
|
155104
|
+
.text-amber-300\/80 {
|
|
155105
|
+
color: #ffd236cc;
|
|
155106
|
+
}
|
|
155107
|
+
|
|
155108
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
155109
|
+
.text-amber-300\/80 {
|
|
155110
|
+
color: color-mix(in oklab, var(--color-amber-300) 80%, transparent);
|
|
155111
|
+
}
|
|
155112
|
+
}
|
|
155113
|
+
|
|
154825
155114
|
.text-amber-400 {
|
|
154826
155115
|
color: var(--color-amber-400);
|
|
154827
155116
|
}
|
|
@@ -155582,6 +155871,10 @@ if ("serviceWorker" in navigator) {
|
|
|
155582
155871
|
}
|
|
155583
155872
|
}
|
|
155584
155873
|
|
|
155874
|
+
.hover\:bg-background:hover {
|
|
155875
|
+
background-color: var(--background);
|
|
155876
|
+
}
|
|
155877
|
+
|
|
155585
155878
|
.hover\:bg-black\/80:hover {
|
|
155586
155879
|
background-color: #000c;
|
|
155587
155880
|
}
|
package/runner/src/adapter.ts
CHANGED
|
@@ -89,12 +89,15 @@ export interface TerminalAttachSpec {
|
|
|
89
89
|
ttlMs?: number;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export type ProviderPermissionDecision = "approve" | "approve-session" | "deny" | "abort";
|
|
92
|
+
export type ProviderPermissionDecision = "approve" | "approve-session" | "deny" | "abort" | "answer";
|
|
93
93
|
|
|
94
94
|
export interface ProviderPermissionDecisionInput {
|
|
95
95
|
approvalId: string;
|
|
96
96
|
decision: ProviderPermissionDecision;
|
|
97
97
|
reason?: string;
|
|
98
|
+
// For "answer" decisions (Claude AskUserQuestion): maps each question's text
|
|
99
|
+
// to the chosen option label(s). Multi-select labels are comma-joined.
|
|
100
|
+
answers?: Record<string, string>;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
export interface ProviderAdapter {
|
package/src/db.ts
CHANGED
|
@@ -5065,6 +5065,28 @@ export function deleteWorkspace(id: string): boolean {
|
|
|
5065
5065
|
return db.prepare("DELETE FROM workspaces WHERE id = ?").run(id).changes > 0;
|
|
5066
5066
|
}
|
|
5067
5067
|
|
|
5068
|
+
// Shared-mode rows are pure occupancy markers (no worktree on disk) that only
|
|
5069
|
+
// mean something while their owner is online. Deletion is normally driven by
|
|
5070
|
+
// the reaper's onAgentDisappeared hook, but agents also leave via clean
|
|
5071
|
+
// SessionEnd (setStatus offline) and via pruneOfflineAgents (raw agent delete),
|
|
5072
|
+
// neither of which touches workspaces — so orphaned shared rows accumulate and
|
|
5073
|
+
// bloat the workspace panel. This sweep is the catch-all: drop any non-cleaned
|
|
5074
|
+
// shared row whose owner is missing or offline, regardless of how it leaked.
|
|
5075
|
+
export function pruneOrphanedSharedWorkspaces(): string[] {
|
|
5076
|
+
return db.transaction(() => {
|
|
5077
|
+
const orphanCondition = `
|
|
5078
|
+
mode = 'shared' AND status != 'cleaned' AND (
|
|
5079
|
+
owner_agent_id IS NULL
|
|
5080
|
+
OR owner_agent_id NOT IN (SELECT id FROM agents)
|
|
5081
|
+
OR owner_agent_id IN (SELECT id FROM agents WHERE status = 'offline')
|
|
5082
|
+
)`;
|
|
5083
|
+
const rows = db.prepare(`SELECT id FROM workspaces WHERE ${orphanCondition}`).all() as Array<{ id: string }>;
|
|
5084
|
+
if (!rows.length) return [];
|
|
5085
|
+
db.prepare(`DELETE FROM workspaces WHERE ${orphanCondition}`).run();
|
|
5086
|
+
return rows.map((r) => r.id);
|
|
5087
|
+
})();
|
|
5088
|
+
}
|
|
5089
|
+
|
|
5068
5090
|
export function updateWorkspaceStatus(id: string, status: WorkspaceStatus, metadata: Record<string, unknown> = {}): WorkspaceRecord | null {
|
|
5069
5091
|
const existing = getWorkspace(id);
|
|
5070
5092
|
if (!existing) return null;
|
package/src/maintenance.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { getArtifactStorage } from "./artifact-storage";
|
|
3
3
|
import { expireStaleBusAgents } from "./bus";
|
|
4
4
|
import { pruneOutbox } from "./bus-outbox";
|
|
5
|
-
import { expireCommands } from "./commands-db";
|
|
5
|
+
import { createCommand, expireCommands } from "./commands-db";
|
|
6
6
|
import { DAY_MS, OFFLINE_PRUNE_MS, REAP_INTERVAL_MS, STALE_TTL_MS } from "./config";
|
|
7
7
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
8
8
|
import {
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
listWorkspaces,
|
|
15
15
|
pruneOfflineAgents,
|
|
16
16
|
pruneOldMessages,
|
|
17
|
+
deleteWorkspace,
|
|
18
|
+
pruneOrphanedSharedWorkspaces,
|
|
17
19
|
reapStaleAgents,
|
|
18
20
|
reapStaleOrchestrators,
|
|
19
21
|
releaseExpiredClaims,
|
|
@@ -44,9 +46,13 @@ const SCHEDULER_TICK_MS = 10_000;
|
|
|
44
46
|
const OUTBOX_RETENTION_MS = Number(process.env.AGENT_RELAY_OUTBOX_RETENTION_MS) || 60 * 60 * 1000;
|
|
45
47
|
const TOKEN_RECORD_RETENTION_SECONDS = Number(process.env.AGENT_RELAY_TOKEN_RECORD_RETENTION_SECONDS) || 7 * 24 * 60 * 60;
|
|
46
48
|
const CONFLICT_SCAN_INTERVAL_MS = Number(process.env.AGENT_RELAY_CONFLICT_SCAN_INTERVAL_MS) || 2 * 60 * 1000;
|
|
49
|
+
const WORKSPACE_RETENTION_MS = Number(process.env.AGENT_RELAY_WORKSPACE_RETENTION_MS) || DAY_MS;
|
|
50
|
+
const WORKSPACE_REVIEW_TTL_MS = Number(process.env.AGENT_RELAY_WORKSPACE_REVIEW_TTL_MS) || 3 * DAY_MS;
|
|
51
|
+
const WORKSPACE_GC_INTERVAL_MS = Number(process.env.AGENT_RELAY_WORKSPACE_GC_INTERVAL_MS) || 60 * 60 * 1000;
|
|
47
52
|
// Live statuses worth scanning. Terminal (cleaned/merged/abandoned) and
|
|
48
53
|
// in-flight (cleanup_requested) states are skipped.
|
|
49
54
|
const CONFLICT_SCAN_STATUSES = new Set<WorkspaceStatus>(["active", "ready", "review_requested", "merge_planned", "conflict"]);
|
|
55
|
+
const TERMINAL_WORKSPACE_STATUSES = new Set<WorkspaceStatus>(["cleaned", "merged", "abandoned"]);
|
|
50
56
|
|
|
51
57
|
interface MaintenanceJobDefinition {
|
|
52
58
|
id: string;
|
|
@@ -279,6 +285,17 @@ const definitions: MaintenanceJobDefinition[] = [
|
|
|
279
285
|
return { prunedTokenJtis };
|
|
280
286
|
},
|
|
281
287
|
},
|
|
288
|
+
{
|
|
289
|
+
id: "workspace-orphan-sweep",
|
|
290
|
+
title: "Workspace orphan sweep",
|
|
291
|
+
description: "Delete shared-mode workspace occupancy rows whose owner agent is offline or gone.",
|
|
292
|
+
intervalMs: REAP_INTERVAL_MS,
|
|
293
|
+
runOnStart: true,
|
|
294
|
+
handler() {
|
|
295
|
+
const prunedSharedWorkspaceIds = pruneOrphanedSharedWorkspaces();
|
|
296
|
+
return { prunedSharedWorkspaceIds };
|
|
297
|
+
},
|
|
298
|
+
},
|
|
282
299
|
{
|
|
283
300
|
id: "workspace-conflict-scan",
|
|
284
301
|
title: "Workspace conflict scan",
|
|
@@ -288,6 +305,15 @@ const definitions: MaintenanceJobDefinition[] = [
|
|
|
288
305
|
timeoutMs: 60 * 1000,
|
|
289
306
|
handler: scanWorkspaceConflicts,
|
|
290
307
|
},
|
|
308
|
+
{
|
|
309
|
+
id: "workspace-gc",
|
|
310
|
+
title: "Workspace GC",
|
|
311
|
+
description: "Prune terminal workspace rows past retention, auto-abandon stale review_requested worktrees, and trigger git worktree prune on orchestrators.",
|
|
312
|
+
intervalMs: WORKSPACE_GC_INTERVAL_MS,
|
|
313
|
+
runOnStart: false,
|
|
314
|
+
timeoutMs: 60 * 1000,
|
|
315
|
+
handler: workspaceGC,
|
|
316
|
+
},
|
|
291
317
|
];
|
|
292
318
|
|
|
293
319
|
function workspacePathWithinBase(path: string | undefined, baseDir: string | undefined): boolean {
|
|
@@ -387,6 +413,79 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
|
|
|
387
413
|
return { scanned: candidates.length, flagged, cleared, notifiedStewards };
|
|
388
414
|
}
|
|
389
415
|
|
|
416
|
+
async function workspaceGC(): Promise<Record<string, unknown>> {
|
|
417
|
+
const now = Date.now();
|
|
418
|
+
const cutoff = now - WORKSPACE_RETENTION_MS;
|
|
419
|
+
const reviewCutoff = now - WORKSPACE_REVIEW_TTL_MS;
|
|
420
|
+
|
|
421
|
+
// 1. Prune terminal rows past retention
|
|
422
|
+
const all = listWorkspaces();
|
|
423
|
+
const terminalIds: string[] = [];
|
|
424
|
+
for (const ws of all) {
|
|
425
|
+
if (TERMINAL_WORKSPACE_STATUSES.has(ws.status) && ws.updatedAt < cutoff) {
|
|
426
|
+
deleteWorkspace(ws.id);
|
|
427
|
+
terminalIds.push(ws.id);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 2. Auto-abandon stale review_requested worktrees
|
|
432
|
+
const abandonedIds: string[] = [];
|
|
433
|
+
const notifiedStewards: string[] = [];
|
|
434
|
+
for (const ws of all) {
|
|
435
|
+
if (ws.status === "review_requested" && ws.updatedAt < reviewCutoff) {
|
|
436
|
+
updateWorkspaceStatus(ws.id, "abandoned", { autoAbandoned: true, abandonedReason: "review_requested TTL exceeded", abandonedAt: now });
|
|
437
|
+
abandonedIds.push(ws.id);
|
|
438
|
+
if (ws.stewardAgentId) {
|
|
439
|
+
try {
|
|
440
|
+
const msg = sendMessage({
|
|
441
|
+
from: "system",
|
|
442
|
+
to: ws.stewardAgentId,
|
|
443
|
+
kind: "system",
|
|
444
|
+
subject: "Workspace auto-abandoned",
|
|
445
|
+
body: `Workspace \`${ws.branch ?? ws.id}\` in ${ws.repoRoot} was auto-abandoned after ${Math.round(WORKSPACE_REVIEW_TTL_MS / DAY_MS)}d without steward action. Run workspace cleanup to reclaim the worktree.`,
|
|
446
|
+
payload: { kind: "workspace.auto-abandoned", workspaceId: ws.id, repoRoot: ws.repoRoot, branch: ws.branch },
|
|
447
|
+
});
|
|
448
|
+
emitNewMessage(msg);
|
|
449
|
+
notifiedStewards.push(ws.stewardAgentId);
|
|
450
|
+
} catch {
|
|
451
|
+
// Steward gone — activity event is enough.
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
createActivityEvent({
|
|
455
|
+
clientId: `workspace-gc-abandon-${ws.id}-${now}`,
|
|
456
|
+
kind: "state",
|
|
457
|
+
title: "Workspace auto-abandoned",
|
|
458
|
+
body: `${ws.branch ?? ws.id} in ${ws.repoRoot} — review_requested for ${Math.round((now - ws.updatedAt) / DAY_MS)}d`,
|
|
459
|
+
meta: ws.branch ?? ws.id,
|
|
460
|
+
icon: "ti-clock-x",
|
|
461
|
+
view: "orchestrators",
|
|
462
|
+
metadata: { source: "server", maintenanceJobId: "workspace-gc", workspaceId: ws.id },
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 3. Trigger git worktree prune on orchestrators that own isolated workspaces
|
|
468
|
+
const orchestrators = listOrchestrators().filter((orch) => orch.status === "online" && orch.agentId);
|
|
469
|
+
const reposWithWorkspaces = new Set(
|
|
470
|
+
all.filter((ws) => ws.mode === "isolated" && Boolean(ws.worktreePath)).map((ws) => ws.repoRoot),
|
|
471
|
+
);
|
|
472
|
+
const pruneCommands: string[] = [];
|
|
473
|
+
for (const repoRoot of reposWithWorkspaces) {
|
|
474
|
+
const orch = orchestrators.find((candidate) => workspacePathWithinBase(repoRoot, candidate.baseDir));
|
|
475
|
+
if (!orch?.agentId) continue;
|
|
476
|
+
const command = createCommand({
|
|
477
|
+
type: "workspace.prune",
|
|
478
|
+
source: "system",
|
|
479
|
+
target: orch.agentId,
|
|
480
|
+
params: { repoRoot, requestedBy: "workspace-gc", requestedAt: now },
|
|
481
|
+
});
|
|
482
|
+
emitCommand(command);
|
|
483
|
+
pruneCommands.push(command.id);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { prunedTerminal: terminalIds, autoAbandoned: abandonedIds, notifiedStewards, pruneCommands };
|
|
487
|
+
}
|
|
488
|
+
|
|
390
489
|
let timer: Timer | null = null;
|
|
391
490
|
|
|
392
491
|
export function startMaintenanceScheduler(): void {
|
package/src/routes.ts
CHANGED
|
@@ -2416,9 +2416,20 @@ const postAgentPermissionDecision: Handler = async (req, params) => {
|
|
|
2416
2416
|
try {
|
|
2417
2417
|
if (!isRecord(parsed.body)) return error("permission decision required");
|
|
2418
2418
|
const approvalId = cleanString(parsed.body.approvalId, "approvalId", { required: true, max: 240 })!;
|
|
2419
|
-
const decision = cleanEnum(parsed.body.decision, "decision", ["approve", "approve-session", "deny", "abort"] as const);
|
|
2419
|
+
const decision = cleanEnum(parsed.body.decision, "decision", ["approve", "approve-session", "deny", "abort", "answer"] as const);
|
|
2420
2420
|
if (!decision) return error("decision required");
|
|
2421
2421
|
const reason = cleanString(parsed.body.reason, "reason", { max: 500 });
|
|
2422
|
+
// AskUserQuestion answers: { "<question text>": "<chosen label(s)>" }
|
|
2423
|
+
const answers = decision === "answer" && isRecord(parsed.body.answers)
|
|
2424
|
+
? Object.fromEntries(
|
|
2425
|
+
Object.entries(parsed.body.answers)
|
|
2426
|
+
.filter(([, v]) => typeof v === "string")
|
|
2427
|
+
.map(([k, v]) => [k.slice(0, 2000), (v as string).slice(0, 4000)]),
|
|
2428
|
+
)
|
|
2429
|
+
: undefined;
|
|
2430
|
+
if (decision === "answer" && (!answers || Object.keys(answers).length === 0)) {
|
|
2431
|
+
return error("answers required for answer decision");
|
|
2432
|
+
}
|
|
2422
2433
|
const agent = getAgent(params.id!);
|
|
2423
2434
|
if (!agent) return error("agent not found", 404);
|
|
2424
2435
|
if (!agentIsControlEligible(agent)) return error("agent cannot receive permission decisions", 400);
|
|
@@ -2445,6 +2456,7 @@ const postAgentPermissionDecision: Handler = async (req, params) => {
|
|
|
2445
2456
|
approvalId,
|
|
2446
2457
|
decision,
|
|
2447
2458
|
...(reason ? { reason } : {}),
|
|
2459
|
+
...(answers ? { answers } : {}),
|
|
2448
2460
|
requestedBy: "dashboard",
|
|
2449
2461
|
requestedAt: Date.now(),
|
|
2450
2462
|
},
|