macroclaw 0.15.0 → 0.16.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 +60 -10
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) {
|
|
@@ -158,6 +183,24 @@ export class Orchestrator {
|
|
|
158
183
|
}
|
|
159
184
|
}
|
|
160
185
|
|
|
186
|
+
async handleKill(sessionId: string): Promise<void> {
|
|
187
|
+
const agent = this.#backgroundAgents.get(sessionId);
|
|
188
|
+
if (!agent) {
|
|
189
|
+
this.#callOnResponse({ message: "Agent not found or already finished." });
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.#backgroundAgents.delete(sessionId);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
await agent.query.kill();
|
|
197
|
+
} catch (err) {
|
|
198
|
+
log.error({ err, name: agent.name }, "Kill failed");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.#callOnResponse({ message: `Killed <b>${escapeHtml(agent.name)}</b>.` });
|
|
202
|
+
}
|
|
203
|
+
|
|
161
204
|
handleSessionCommand(): void {
|
|
162
205
|
this.#callOnResponse({ message: `Session: <code>${this.#mainSessionId ?? "none"}</code>` });
|
|
163
206
|
}
|
|
@@ -298,9 +341,11 @@ export class Orchestrator {
|
|
|
298
341
|
const name = request.type === "user" ? request.message.slice(0, 30).replace(/\s+/g, "-")
|
|
299
342
|
: request.type === "cron" ? `cron-${request.name}`
|
|
300
343
|
: "task";
|
|
344
|
+
const prompt = this.#formatPrompt(request);
|
|
345
|
+
const model = request.type === "cron" ? (request.model ?? this.#config.model) : this.#config.model;
|
|
301
346
|
log.info({ name, sessionId: query.sessionId }, "Request backgrounded due to timeout");
|
|
302
347
|
this.#callOnResponse({ message: "This is taking longer, continuing in the background." });
|
|
303
|
-
this.#adoptBackground(name,
|
|
348
|
+
this.#adoptBackground(name, prompt, model, query);
|
|
304
349
|
return null;
|
|
305
350
|
}
|
|
306
351
|
|
|
@@ -327,18 +372,20 @@ export class Orchestrator {
|
|
|
327
372
|
? this.#claude.forkSession(this.#mainSessionId, bgPrompt, responseResultType, { model })
|
|
328
373
|
: this.#claude.newSession(bgPrompt, responseResultType, { model });
|
|
329
374
|
const sessionId = query.sessionId;
|
|
330
|
-
const info: BackgroundInfo = { name,
|
|
375
|
+
const info: BackgroundInfo = { name, prompt, model, query };
|
|
331
376
|
this.#backgroundAgents.set(sessionId, info);
|
|
332
377
|
|
|
333
378
|
log.debug({ name, sessionId }, "Starting background agent");
|
|
334
379
|
|
|
335
380
|
query.result.then(
|
|
336
381
|
async ({ value: response }) => {
|
|
382
|
+
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
337
383
|
this.#backgroundAgents.delete(sessionId);
|
|
338
384
|
log.debug({ name, message: response.message }, "Background agent finished");
|
|
339
385
|
this.#queue.push({ type: "background-agent-result", name, response });
|
|
340
386
|
},
|
|
341
387
|
(err) => {
|
|
388
|
+
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
342
389
|
this.#backgroundAgents.delete(sessionId);
|
|
343
390
|
log.error({ name, err }, "Background agent failed");
|
|
344
391
|
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "bg-agent-failed" } });
|
|
@@ -346,19 +393,22 @@ export class Orchestrator {
|
|
|
346
393
|
);
|
|
347
394
|
}
|
|
348
395
|
|
|
349
|
-
#adoptBackground(name: string,
|
|
350
|
-
const
|
|
396
|
+
#adoptBackground(name: string, prompt: string, model: string | undefined, query: RunningQuery<AgentOutput>) {
|
|
397
|
+
const sessionId = query.sessionId;
|
|
398
|
+
const info: BackgroundInfo = { name, prompt, model, query };
|
|
351
399
|
this.#backgroundAgents.set(sessionId, info);
|
|
352
400
|
|
|
353
401
|
log.debug({ name, sessionId }, "Adopting backgrounded task");
|
|
354
402
|
|
|
355
|
-
|
|
403
|
+
query.result.then(
|
|
356
404
|
({ value: response }) => {
|
|
405
|
+
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
357
406
|
this.#backgroundAgents.delete(sessionId);
|
|
358
407
|
log.debug({ name }, "Adopted task finished");
|
|
359
408
|
this.#queue.push({ type: "background-agent-result", name, response, sessionId });
|
|
360
409
|
},
|
|
361
410
|
(err) => {
|
|
411
|
+
if (!this.#backgroundAgents.has(sessionId)) return;
|
|
362
412
|
this.#backgroundAgents.delete(sessionId);
|
|
363
413
|
log.error({ name, err }, "Adopted task failed");
|
|
364
414
|
this.#queue.push({ type: "background-agent-result", name, response: { action: "send", message: `[Error] ${err}`, actionReason: "deferred-failed" }, sessionId });
|