macroclaw 0.15.0 → 0.17.0
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 +1 -1
- package/src/app.test.ts +46 -0
- package/src/app.ts +16 -0
- package/src/orchestrator.test.ts +192 -10
- package/src/orchestrator.ts +62 -11
package/package.json
CHANGED
package/src/app.test.ts
CHANGED
|
@@ -556,6 +556,29 @@ describe("App", () => {
|
|
|
556
556
|
expect((config.claude as Claude & { calls: CallInfo[] }).calls).toHaveLength(0);
|
|
557
557
|
});
|
|
558
558
|
|
|
559
|
+
it("handles detail: callback by routing to orchestrator.handleDetail", async () => {
|
|
560
|
+
const config = makeConfig();
|
|
561
|
+
const app = new App(config);
|
|
562
|
+
const bot = app.bot as any;
|
|
563
|
+
const handler = bot.filterHandlers.get("callback_query:data")![0];
|
|
564
|
+
|
|
565
|
+
const ctx = {
|
|
566
|
+
chat: { id: 12345 },
|
|
567
|
+
callbackQuery: { data: "detail:test-session-123" },
|
|
568
|
+
answerCallbackQuery: mock(async () => {}),
|
|
569
|
+
editMessageReplyMarkup: mock(async () => {}),
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
await handler(ctx);
|
|
573
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
574
|
+
|
|
575
|
+
expect(ctx.answerCallbackQuery).toHaveBeenCalled();
|
|
576
|
+
expect(ctx.editMessageReplyMarkup).toHaveBeenCalledWith({ reply_markup: { inline_keyboard: [[{ text: "✓ Opened", callback_data: "_noop" }]] } });
|
|
577
|
+
const calls = (bot.api.sendMessage as any).mock.calls;
|
|
578
|
+
const text = calls[calls.length - 1][1];
|
|
579
|
+
expect(text).toBe("Agent not found or already finished.");
|
|
580
|
+
});
|
|
581
|
+
|
|
559
582
|
it("handles peek: callback by routing to orchestrator.handlePeek", async () => {
|
|
560
583
|
const config = makeConfig();
|
|
561
584
|
const app = new App(config);
|
|
@@ -579,6 +602,29 @@ describe("App", () => {
|
|
|
579
602
|
expect(text).toBe("Agent not found or already finished.");
|
|
580
603
|
});
|
|
581
604
|
|
|
605
|
+
it("handles kill: callback by routing to orchestrator.handleKill", async () => {
|
|
606
|
+
const config = makeConfig();
|
|
607
|
+
const app = new App(config);
|
|
608
|
+
const bot = app.bot as any;
|
|
609
|
+
const handler = bot.filterHandlers.get("callback_query:data")![0];
|
|
610
|
+
|
|
611
|
+
const ctx = {
|
|
612
|
+
chat: { id: 12345 },
|
|
613
|
+
callbackQuery: { data: "kill:test-session-123" },
|
|
614
|
+
answerCallbackQuery: mock(async () => {}),
|
|
615
|
+
editMessageReplyMarkup: mock(async () => {}),
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
await handler(ctx);
|
|
619
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
620
|
+
|
|
621
|
+
expect(ctx.answerCallbackQuery).toHaveBeenCalled();
|
|
622
|
+
expect(ctx.editMessageReplyMarkup).toHaveBeenCalledWith({ reply_markup: { inline_keyboard: [[{ text: "✓ Killed", callback_data: "_noop" }]] } });
|
|
623
|
+
const calls = (bot.api.sendMessage as any).mock.calls;
|
|
624
|
+
const text = calls[calls.length - 1][1];
|
|
625
|
+
expect(text).toBe("Agent not found or already finished.");
|
|
626
|
+
});
|
|
627
|
+
|
|
582
628
|
it("ignores callback_query from unauthorized chats", async () => {
|
|
583
629
|
const config = makeConfig();
|
|
584
630
|
const app = new App(config);
|
package/src/app.ts
CHANGED
|
@@ -149,6 +149,14 @@ export class App {
|
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
if (data.startsWith("detail:")) {
|
|
153
|
+
const sessionId = data.slice(7);
|
|
154
|
+
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [[{ text: "✓ Opened", callback_data: "_noop" }]] } });
|
|
155
|
+
log.debug({ sessionId }, "Detail requested");
|
|
156
|
+
this.#orchestrator.handleDetail(sessionId);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
152
160
|
if (data.startsWith("peek:")) {
|
|
153
161
|
const sessionId = data.slice(5);
|
|
154
162
|
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [[{ text: "✓ Peeked", callback_data: "_noop" }]] } });
|
|
@@ -157,6 +165,14 @@ export class App {
|
|
|
157
165
|
return;
|
|
158
166
|
}
|
|
159
167
|
|
|
168
|
+
if (data.startsWith("kill:")) {
|
|
169
|
+
const sessionId = data.slice(5);
|
|
170
|
+
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [[{ text: "✓ Killed", callback_data: "_noop" }]] } });
|
|
171
|
+
log.debug({ sessionId }, "Kill requested");
|
|
172
|
+
this.#orchestrator.handleKill(sessionId);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
160
176
|
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [[{ text: `✓ ${data}`, callback_data: "_noop" }]] } });
|
|
161
177
|
log.debug({ label: data }, "Button clicked");
|
|
162
178
|
this.#orchestrator.handleButton(data);
|
package/src/orchestrator.test.ts
CHANGED
|
@@ -559,7 +559,7 @@ describe("Orchestrator", () => {
|
|
|
559
559
|
expect(responses[0].message).toBe("No background agents running.");
|
|
560
560
|
});
|
|
561
561
|
|
|
562
|
-
it("includes
|
|
562
|
+
it("includes detail buttons and dismiss when agents are running", async () => {
|
|
563
563
|
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
564
564
|
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
565
565
|
sessionId: `bg-${Date.now()}`,
|
|
@@ -578,11 +578,11 @@ describe("Orchestrator", () => {
|
|
|
578
578
|
const listResponse = responses[responses.length - 1];
|
|
579
579
|
expect(listResponse.message).toContain("long-task");
|
|
580
580
|
expect(listResponse.buttons).toBeDefined();
|
|
581
|
-
expect(listResponse.buttons!.length).toBe(2); // 1
|
|
582
|
-
const
|
|
583
|
-
expect(typeof
|
|
584
|
-
expect((
|
|
585
|
-
expect((
|
|
581
|
+
expect(listResponse.buttons!.length).toBe(2); // 1 detail + dismiss
|
|
582
|
+
const detailBtn = listResponse.buttons![0];
|
|
583
|
+
expect(typeof detailBtn).toBe("object");
|
|
584
|
+
expect((detailBtn as any).data).toMatch(/^detail:/);
|
|
585
|
+
expect((detailBtn as any).text).toContain("long-task");
|
|
586
586
|
expect(listResponse.buttons![1]).toEqual({ text: "Dismiss", data: "_dismiss" });
|
|
587
587
|
});
|
|
588
588
|
});
|
|
@@ -624,8 +624,8 @@ describe("Orchestrator", () => {
|
|
|
624
624
|
orch.handleBackgroundList();
|
|
625
625
|
await waitForProcessing();
|
|
626
626
|
const listResponse = responses[responses.length - 1];
|
|
627
|
-
const
|
|
628
|
-
const sessionId =
|
|
627
|
+
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
628
|
+
const sessionId = detailBtn.data.slice(7); // strip "detail:"
|
|
629
629
|
|
|
630
630
|
await orch.handlePeek(sessionId);
|
|
631
631
|
await waitForProcessing();
|
|
@@ -663,8 +663,8 @@ describe("Orchestrator", () => {
|
|
|
663
663
|
orch.handleBackgroundList();
|
|
664
664
|
await waitForProcessing();
|
|
665
665
|
const listResponse = responses[responses.length - 1];
|
|
666
|
-
const
|
|
667
|
-
const sessionId =
|
|
666
|
+
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
667
|
+
const sessionId = detailBtn.data.slice(7); // strip "detail:"
|
|
668
668
|
|
|
669
669
|
await orch.handlePeek(sessionId);
|
|
670
670
|
await waitForProcessing();
|
|
@@ -674,6 +674,188 @@ describe("Orchestrator", () => {
|
|
|
674
674
|
});
|
|
675
675
|
});
|
|
676
676
|
|
|
677
|
+
describe("handleDetail", () => {
|
|
678
|
+
it("returns 'not found' for unknown sessionId", async () => {
|
|
679
|
+
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
680
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
681
|
+
|
|
682
|
+
orch.handleDetail("nonexistent-session");
|
|
683
|
+
await waitForProcessing();
|
|
684
|
+
|
|
685
|
+
expect(responses[0].message).toBe("Agent not found or already finished.");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
it("shows agent details with peek/kill/dismiss buttons", async () => {
|
|
689
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
690
|
+
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
691
|
+
sessionId: "bg-sid",
|
|
692
|
+
startedAt: new Date(),
|
|
693
|
+
result: new Promise(() => {}),
|
|
694
|
+
kill: mock(async () => {}),
|
|
695
|
+
}));
|
|
696
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
697
|
+
|
|
698
|
+
orch.handleBackgroundCommand("research pricing");
|
|
699
|
+
await waitForProcessing();
|
|
700
|
+
|
|
701
|
+
orch.handleBackgroundList();
|
|
702
|
+
await waitForProcessing();
|
|
703
|
+
const listResponse = responses[responses.length - 1];
|
|
704
|
+
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
705
|
+
const sessionId = detailBtn.data.slice(7);
|
|
706
|
+
|
|
707
|
+
orch.handleDetail(sessionId);
|
|
708
|
+
await waitForProcessing();
|
|
709
|
+
|
|
710
|
+
const detailResponse = responses[responses.length - 1];
|
|
711
|
+
expect(detailResponse.message).toContain("research-pricing");
|
|
712
|
+
expect(detailResponse.message).toContain("research pricing");
|
|
713
|
+
expect(detailResponse.message).toContain("default");
|
|
714
|
+
expect(detailResponse.message).toContain("Status: running");
|
|
715
|
+
expect(detailResponse.buttons).toHaveLength(3);
|
|
716
|
+
expect(detailResponse.buttons![0]).toEqual({ text: "Peek", data: `peek:${sessionId}` });
|
|
717
|
+
expect(detailResponse.buttons![1]).toEqual({ text: "Kill", data: `kill:${sessionId}` });
|
|
718
|
+
expect(detailResponse.buttons![2]).toEqual({ text: "Dismiss", data: "_dismiss" });
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("truncates prompt at 300 chars", async () => {
|
|
722
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
723
|
+
const longPrompt = "a".repeat(500);
|
|
724
|
+
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
725
|
+
sessionId: "bg-sid",
|
|
726
|
+
startedAt: new Date(),
|
|
727
|
+
result: new Promise(() => {}),
|
|
728
|
+
kill: mock(async () => {}),
|
|
729
|
+
}));
|
|
730
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
731
|
+
|
|
732
|
+
orch.handleBackgroundCommand(longPrompt);
|
|
733
|
+
await waitForProcessing();
|
|
734
|
+
|
|
735
|
+
orch.handleBackgroundList();
|
|
736
|
+
await waitForProcessing();
|
|
737
|
+
const listResponse = responses[responses.length - 1];
|
|
738
|
+
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
739
|
+
const sessionId = detailBtn.data.slice(7);
|
|
740
|
+
|
|
741
|
+
orch.handleDetail(sessionId);
|
|
742
|
+
await waitForProcessing();
|
|
743
|
+
|
|
744
|
+
const detailResponse = responses[responses.length - 1];
|
|
745
|
+
// 300 chars + ellipsis
|
|
746
|
+
expect(detailResponse.message).toContain("a".repeat(300));
|
|
747
|
+
expect(detailResponse.message).toContain("…");
|
|
748
|
+
expect(detailResponse.message).not.toContain("a".repeat(301));
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("shows model when specified", async () => {
|
|
752
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
753
|
+
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
754
|
+
sessionId: "bg-sid",
|
|
755
|
+
startedAt: new Date(),
|
|
756
|
+
result: new Promise(() => {}),
|
|
757
|
+
kill: mock(async () => {}),
|
|
758
|
+
}));
|
|
759
|
+
const { orch, responses } = makeOrchestrator(claude, { model: "opus" });
|
|
760
|
+
|
|
761
|
+
orch.handleBackgroundCommand("research");
|
|
762
|
+
await waitForProcessing();
|
|
763
|
+
|
|
764
|
+
orch.handleBackgroundList();
|
|
765
|
+
await waitForProcessing();
|
|
766
|
+
const listResponse = responses[responses.length - 1];
|
|
767
|
+
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
768
|
+
const sessionId = detailBtn.data.slice(7);
|
|
769
|
+
|
|
770
|
+
orch.handleDetail(sessionId);
|
|
771
|
+
await waitForProcessing();
|
|
772
|
+
|
|
773
|
+
const detailResponse = responses[responses.length - 1];
|
|
774
|
+
expect(detailResponse.message).toContain("Model: opus");
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
describe("handleKill", () => {
|
|
779
|
+
it("returns 'not found' for unknown sessionId", async () => {
|
|
780
|
+
const claude = mockClaude({ action: "send", message: "ok", actionReason: "ok" });
|
|
781
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
782
|
+
|
|
783
|
+
await orch.handleKill("nonexistent-session");
|
|
784
|
+
await waitForProcessing();
|
|
785
|
+
|
|
786
|
+
expect(responses[0].message).toBe("Agent not found or already finished.");
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("kills running agent and sends confirmation", async () => {
|
|
790
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
791
|
+
const killMock = mock(async () => {});
|
|
792
|
+
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
793
|
+
sessionId: "bg-sid",
|
|
794
|
+
startedAt: new Date(),
|
|
795
|
+
result: new Promise(() => {}),
|
|
796
|
+
kill: killMock,
|
|
797
|
+
}));
|
|
798
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
799
|
+
|
|
800
|
+
orch.handleBackgroundCommand("research pricing");
|
|
801
|
+
await waitForProcessing();
|
|
802
|
+
|
|
803
|
+
orch.handleBackgroundList();
|
|
804
|
+
await waitForProcessing();
|
|
805
|
+
const listResponse = responses[responses.length - 1];
|
|
806
|
+
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
807
|
+
const sessionId = detailBtn.data.slice(7);
|
|
808
|
+
|
|
809
|
+
await orch.handleKill(sessionId);
|
|
810
|
+
await waitForProcessing();
|
|
811
|
+
|
|
812
|
+
expect(killMock).toHaveBeenCalled();
|
|
813
|
+
const killResponse = responses[responses.length - 1];
|
|
814
|
+
expect(killResponse.message).toContain("Killed");
|
|
815
|
+
expect(killResponse.message).toContain("research-pricing");
|
|
816
|
+
|
|
817
|
+
// Agent should be removed from list
|
|
818
|
+
orch.handleBackgroundList();
|
|
819
|
+
await waitForProcessing();
|
|
820
|
+
expect(responses[responses.length - 1].message).toBe("No background agents running.");
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it("does not feed error back to queue after kill", async () => {
|
|
824
|
+
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
|
825
|
+
let rejectBg: (err: Error) => void;
|
|
826
|
+
const bgResult = new Promise<never>((_, r) => { rejectBg = r; });
|
|
827
|
+
const claude = mockClaude((): RunningQuery<unknown> => ({
|
|
828
|
+
sessionId: "bg-sid",
|
|
829
|
+
startedAt: new Date(),
|
|
830
|
+
result: bgResult,
|
|
831
|
+
kill: mock(async () => {}),
|
|
832
|
+
}));
|
|
833
|
+
const { orch, responses } = makeOrchestrator(claude);
|
|
834
|
+
|
|
835
|
+
orch.handleBackgroundCommand("task");
|
|
836
|
+
await waitForProcessing();
|
|
837
|
+
|
|
838
|
+
orch.handleBackgroundList();
|
|
839
|
+
await waitForProcessing();
|
|
840
|
+
const listResponse = responses[responses.length - 1];
|
|
841
|
+
const detailBtn = listResponse.buttons![0] as { text: string; data: string };
|
|
842
|
+
const sessionId = detailBtn.data.slice(7);
|
|
843
|
+
|
|
844
|
+
await orch.handleKill(sessionId);
|
|
845
|
+
await waitForProcessing();
|
|
846
|
+
const countAfterKill = responses.length;
|
|
847
|
+
|
|
848
|
+
// Simulate the bg process rejecting after kill
|
|
849
|
+
rejectBg!(new Error("process killed"));
|
|
850
|
+
await waitForProcessing(100);
|
|
851
|
+
|
|
852
|
+
// No additional error responses should have been added
|
|
853
|
+
// Only the "Killed" message, no "[Error]" from the bg handler
|
|
854
|
+
const newResponses = responses.slice(countAfterKill);
|
|
855
|
+
expect(newResponses.every((r) => !r.message.includes("[Error]"))).toBe(true);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
677
859
|
describe("handleBackgroundCommand", () => {
|
|
678
860
|
it("spawns background agent and sends started message", async () => {
|
|
679
861
|
saveSessions({ mainSessionId: "test-session" }, tmpSettingsDir);
|
package/src/orchestrator.ts
CHANGED
|
@@ -67,8 +67,9 @@ function escapeHtml(text: string): string {
|
|
|
67
67
|
|
|
68
68
|
interface BackgroundInfo {
|
|
69
69
|
name: string;
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
prompt: string;
|
|
71
|
+
model?: string;
|
|
72
|
+
query: RunningQuery<AgentOutput>;
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
export interface OrchestratorConfig {
|
|
@@ -123,18 +124,42 @@ export class Orchestrator {
|
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
126
|
const lines = agents.map((a) => {
|
|
126
|
-
const elapsed = Math.round((Date.now() - a.
|
|
127
|
+
const elapsed = Math.round((Date.now() - a.query.startedAt.getTime()) / 1000);
|
|
127
128
|
return `- ${escapeHtml(a.name)} (${elapsed}s)`;
|
|
128
129
|
});
|
|
129
130
|
const buttons: ButtonSpec[] = agents.map((a) => {
|
|
130
|
-
const elapsed = Math.round((Date.now() - a.
|
|
131
|
+
const elapsed = Math.round((Date.now() - a.query.startedAt.getTime()) / 1000);
|
|
131
132
|
const text = `${a.name} (${elapsed}s)`.slice(0, 27);
|
|
132
|
-
return { text, data: `
|
|
133
|
+
return { text, data: `detail:${a.query.sessionId}` };
|
|
133
134
|
});
|
|
134
135
|
buttons.push({text: "Dismiss", data: "_dismiss"});
|
|
135
136
|
this.#callOnResponse({ message: lines.join("\n"), buttons });
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
handleDetail(sessionId: string): void {
|
|
140
|
+
const agent = this.#backgroundAgents.get(sessionId);
|
|
141
|
+
if (!agent) {
|
|
142
|
+
this.#callOnResponse({ message: "Agent not found or already finished." });
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const elapsed = Math.round((Date.now() - agent.query.startedAt.getTime()) / 1000);
|
|
147
|
+
const truncatedPrompt = agent.prompt.length > 300 ? `${agent.prompt.slice(0, 300)}…` : agent.prompt;
|
|
148
|
+
const lines = [
|
|
149
|
+
`<b>${escapeHtml(agent.name)}</b>`,
|
|
150
|
+
`Prompt: ${escapeHtml(truncatedPrompt)}`,
|
|
151
|
+
`Model: ${agent.model ?? "default"}`,
|
|
152
|
+
`Elapsed: ${elapsed}s`,
|
|
153
|
+
"Status: running",
|
|
154
|
+
];
|
|
155
|
+
const buttons: ButtonSpec[] = [
|
|
156
|
+
{ text: "Peek", data: `peek:${sessionId}` },
|
|
157
|
+
{ text: "Kill", data: `kill:${sessionId}` },
|
|
158
|
+
{ text: "Dismiss", data: "_dismiss" },
|
|
159
|
+
];
|
|
160
|
+
this.#callOnResponse({ message: lines.join("\n"), buttons });
|
|
161
|
+
}
|
|
162
|
+
|
|
138
163
|
async handlePeek(sessionId: string): Promise<void> {
|
|
139
164
|
const agent = this.#backgroundAgents.get(sessionId);
|
|
140
165
|
if (!agent) {
|
|
@@ -145,9 +170,10 @@ export class Orchestrator {
|
|
|
145
170
|
this.#callOnResponse({ message: `Peeking at <b>${escapeHtml(agent.name)}</b>...` });
|
|
146
171
|
|
|
147
172
|
try {
|
|
173
|
+
const startedAt = agent.query.startedAt.toISOString();
|
|
148
174
|
const query = this.#claude.forkSession(
|
|
149
175
|
sessionId,
|
|
150
|
-
|
|
176
|
+
`This session started at ${startedAt}. Only consider events after that time. Give a brief status update: what has been done so far, what's currently happening, and what's remaining. 2-3 sentences max.`,
|
|
151
177
|
textResultType,
|
|
152
178
|
{ model: "haiku" },
|
|
153
179
|
);
|
|
@@ -158,6 +184,24 @@ export class Orchestrator {
|
|
|
158
184
|
}
|
|
159
185
|
}
|
|
160
186
|
|
|
187
|
+
async handleKill(sessionId: string): Promise<void> {
|
|
188
|
+
const agent = this.#backgroundAgents.get(sessionId);
|
|
189
|
+
if (!agent) {
|
|
190
|
+
this.#callOnResponse({ message: "Agent not found or already finished." });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.#backgroundAgents.delete(sessionId);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await agent.query.kill();
|
|
198
|
+
} catch (err) {
|
|
199
|
+
log.error({ err, name: agent.name }, "Kill failed");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.#callOnResponse({ message: `Killed <b>${escapeHtml(agent.name)}</b>.` });
|
|
203
|
+
}
|
|
204
|
+
|
|
161
205
|
handleSessionCommand(): void {
|
|
162
206
|
this.#callOnResponse({ message: `Session: <code>${this.#mainSessionId ?? "none"}</code>` });
|
|
163
207
|
}
|
|
@@ -298,9 +342,11 @@ export class Orchestrator {
|
|
|
298
342
|
const name = request.type === "user" ? request.message.slice(0, 30).replace(/\s+/g, "-")
|
|
299
343
|
: request.type === "cron" ? `cron-${request.name}`
|
|
300
344
|
: "task";
|
|
345
|
+
const prompt = this.#formatPrompt(request);
|
|
346
|
+
const model = request.type === "cron" ? (request.model ?? this.#config.model) : this.#config.model;
|
|
301
347
|
log.info({ name, sessionId: query.sessionId }, "Request backgrounded due to timeout");
|
|
302
348
|
this.#callOnResponse({ message: "This is taking longer, continuing in the background." });
|
|
303
|
-
this.#adoptBackground(name,
|
|
349
|
+
this.#adoptBackground(name, prompt, model, query);
|
|
304
350
|
return null;
|
|
305
351
|
}
|
|
306
352
|
|
|
@@ -327,18 +373,20 @@ export class Orchestrator {
|
|
|
327
373
|
? this.#claude.forkSession(this.#mainSessionId, bgPrompt, responseResultType, { model })
|
|
328
374
|
: this.#claude.newSession(bgPrompt, responseResultType, { model });
|
|
329
375
|
const sessionId = query.sessionId;
|
|
330
|
-
const info: BackgroundInfo = { name,
|
|
376
|
+
const info: BackgroundInfo = { name, prompt, model, query };
|
|
331
377
|
this.#backgroundAgents.set(sessionId, info);
|
|
332
378
|
|
|
333
379
|
log.debug({ name, sessionId }, "Starting background agent");
|
|
334
380
|
|
|
335
381
|
query.result.then(
|
|
336
382
|
async ({ value: response }) => {
|
|
383
|
+
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
337
384
|
this.#backgroundAgents.delete(sessionId);
|
|
338
385
|
log.debug({ name, message: response.message }, "Background agent finished");
|
|
339
386
|
this.#queue.push({ type: "background-agent-result", name, response });
|
|
340
387
|
},
|
|
341
388
|
(err) => {
|
|
389
|
+
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
342
390
|
this.#backgroundAgents.delete(sessionId);
|
|
343
391
|
log.error({ name, err }, "Background agent failed");
|
|
344
392
|
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-agent-failed" } });
|
|
@@ -346,19 +394,22 @@ export class Orchestrator {
|
|
|
346
394
|
);
|
|
347
395
|
}
|
|
348
396
|
|
|
349
|
-
#adoptBackground(name: string,
|
|
350
|
-
const
|
|
397
|
+
#adoptBackground(name: string, prompt: string, model: string | undefined, query: RunningQuery<AgentOutput>) {
|
|
398
|
+
const sessionId = query.sessionId;
|
|
399
|
+
const info: BackgroundInfo = { name, prompt, model, query };
|
|
351
400
|
this.#backgroundAgents.set(sessionId, info);
|
|
352
401
|
|
|
353
402
|
log.debug({ name, sessionId }, "Adopting backgrounded task");
|
|
354
403
|
|
|
355
|
-
|
|
404
|
+
query.result.then(
|
|
356
405
|
({ value: response }) => {
|
|
406
|
+
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
357
407
|
this.#backgroundAgents.delete(sessionId);
|
|
358
408
|
log.debug({ name }, "Adopted task finished");
|
|
359
409
|
this.#queue.push({ type: "background-agent-result", name, response, sessionId });
|
|
360
410
|
},
|
|
361
411
|
(err) => {
|
|
412
|
+
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
362
413
|
this.#backgroundAgents.delete(sessionId);
|
|
363
414
|
log.error({ name, err }, "Adopted task failed");
|
|
364
415
|
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "deferred-failed" }, sessionId });
|