mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-08 09:24:41 +00:00
* feat(hosts): connect to saved hosts through a daemon * fix(hosts): validate safe targets and verify encrypted relay routes * test(hosts): include relay coverage in existing CI contracts * fix(cli): preserve validated addresses in daemon command hints * fix(hosts): share daemon links across isolated routed sessions Multiplex logical sessions over the monitored daemon connection and wrap original binary bytes in routing headers. Admit channels under their authenticated parent and current grants; preserve scoped delivery and physical send completion. * fix(hosts): bound encrypted overload and initial acquisition Check encrypted wire capacity before the async channel converts a resource limit into a physical connection error. Bound initial routed acquisition with the existing client timeout, then enable reconnect after admission. * fix(hosts): keep compact row actions clear of host labels Give compact connected-host rows separate text and action widths so long names and producer labels cannot compete with primary controls. * refactor(hosts): remove management settings UI from phase one Keep connected-host management in the CLI and shared SDK while preserving app saved connections and plugin access. * test(hosts): control physical write backpressure across platforms Hold real TCP writes until assertions finish instead of assuming a paused receiver exhausts every platform receive buffer. Preserve physical send settlement, byte identity, capacity and sibling checks. * refactor(hosts): use the ordinary client contract over channel transports Keep queuing, permission codes and reconnection independent of transport selection. Track submitted request outcomes generically while retaining connection error codes and structured causes. * test(e2e): keep transient developer toasts from stalling checks Concurrent locator handlers can wait for a dismiss button another handler removed. Dismiss existing toast DOM at document lifetime, including its open shadow root, without intercepting app actions. * Create, prompt, and monitor agents across connected hosts (#7507) * feat(hosts): delegate agents across connected hosts Resolve MCP host operations through in-process commands for self and the existing routed client transport for other destinations. Keep ordinary parent labels, app sessions and idle finish notifications. * test(app): normalize subagent fixtures through snapshots * docs(protocol): clarify same-host parent labels * fix(hosts): resolve subagents through destination sessions A child address identifies its destination regardless of the connection used. Reuse the existing host candidate selection for discovered routes and direct connections, and keep navigation and row actions independent of the parent transport. * fix(hosts): keep routed destinations distinct during discovery A routed candidate names only the origin daemon. Match it within the named destination profile so two destinations discovered through the same origin retain separate identities and connections, including existing saved routes. * feat(hosts): prompt and query agents on selected hosts Extend the existing MCP host operation boundary with prompt, status, activity and list operations. Local calls retain their handlers and notification owner; selected destinations use ordinary client RPCs and the existing idle finish wait. * test(e2e): preserve toast coverage with CSS hiding * docs(hosts): translate connected host references
644 lines
19 KiB
TypeScript
644 lines
19 KiB
TypeScript
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
createSubagentsForParentSelector,
|
|
selectProviderSubagentsForParent,
|
|
selectSubagentsForParent,
|
|
} from "./select";
|
|
import { createWorkspaceAgentVisibilitySelector } from "@/workspace-tabs/agent-visibility";
|
|
import { useProviderSubagentStore } from "./provider-store";
|
|
import { useSessionStore, type Agent } from "@/stores/session-store";
|
|
|
|
const SERVER_ID = "server-1";
|
|
const AGENT_TIMESTAMP = new Date("2026-03-08T10:00:00.000Z");
|
|
const EMPTY_PENDING_ARCHIVE_IDS = new Set<string>();
|
|
|
|
const AGENT_DEFAULTS: Agent = {
|
|
serverId: SERVER_ID,
|
|
id: "agent",
|
|
provider: "codex",
|
|
status: "idle",
|
|
turn: { phase: "idle", cancellationRequestId: null },
|
|
createdAt: AGENT_TIMESTAMP,
|
|
updatedAt: AGENT_TIMESTAMP,
|
|
lastUserMessageAt: null,
|
|
lastActivityAt: AGENT_TIMESTAMP,
|
|
capabilities: {
|
|
supportsStreaming: true,
|
|
supportsSessionPersistence: true,
|
|
supportsDynamicModes: true,
|
|
supportsMcpServers: true,
|
|
supportsReasoningStream: true,
|
|
supportsToolInvocations: true,
|
|
},
|
|
currentModeId: null,
|
|
availableModes: [],
|
|
pendingPermissions: [],
|
|
persistence: null,
|
|
runtimeInfo: undefined,
|
|
lastUsage: undefined,
|
|
lastError: null,
|
|
title: "Agent",
|
|
cwd: "/tmp/project",
|
|
model: null,
|
|
features: undefined,
|
|
thinkingOptionId: undefined,
|
|
requiresAttention: false,
|
|
attentionReason: null,
|
|
attentionTimestamp: null,
|
|
archivedAt: null,
|
|
parentAgentId: null,
|
|
labels: {},
|
|
projectPlacement: null,
|
|
};
|
|
|
|
function makeAgent(input: Partial<Agent> & Pick<Agent, "id">): Agent {
|
|
return {
|
|
...AGENT_DEFAULTS,
|
|
...input,
|
|
labels: {
|
|
...(input.parentAgentId ? { "paseo.parent-agent-id": input.parentAgentId } : {}),
|
|
...input.labels,
|
|
},
|
|
};
|
|
}
|
|
|
|
function setAgents(agents: Agent[]): void {
|
|
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
|
|
useSessionStore
|
|
.getState()
|
|
.setAgents(SERVER_ID, new Map(agents.map((agent) => [agent.id, agent])));
|
|
}
|
|
|
|
afterEach(() => {
|
|
useSessionStore.getState().clearSession(SERVER_ID);
|
|
useProviderSubagentStore.setState({
|
|
descriptors: new Map(),
|
|
timelines: new Map(),
|
|
hiddenFromTrack: new Set(),
|
|
});
|
|
});
|
|
|
|
describe("selectSubagentsForParent", () => {
|
|
it("keeps equal child IDs on different hosts qualified to the complete parent address", () => {
|
|
const parentId = "shared-parent";
|
|
const childId = "shared-child";
|
|
const local = makeAgent({
|
|
id: childId,
|
|
parentAgentId: parentId,
|
|
labels: { "paseo.parent-agent-id": parentId, "paseo.parent-server-id": SERVER_ID },
|
|
});
|
|
const remote = makeAgent({
|
|
id: childId,
|
|
serverId: "server-2",
|
|
parentAgentId: null,
|
|
labels: { "paseo.parent-agent-id": parentId, "paseo.parent-server-id": SERVER_ID },
|
|
});
|
|
setAgents([local]);
|
|
useSessionStore.getState().initializeSession("server-2", null as unknown as DaemonClient);
|
|
useSessionStore.getState().setAgents("server-2", new Map([[childId, remote]]));
|
|
try {
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{ serverId: SERVER_ID, parentAgentId: parentId },
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
expect(rows).toHaveLength(2);
|
|
expect(rows).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ address: { serverId: SERVER_ID, agentId: childId } }),
|
|
expect.objectContaining({ address: { serverId: "server-2", agentId: childId } }),
|
|
]),
|
|
);
|
|
} finally {
|
|
useSessionStore.getState().clearSession("server-2");
|
|
}
|
|
});
|
|
|
|
it("hides cached provider children when the host does not support them", () => {
|
|
useProviderSubagentStore.getState().applyUpdate(SERVER_ID, {
|
|
kind: "upsert",
|
|
subagent: {
|
|
id: "provider-child",
|
|
parentAgentId: "parent-a",
|
|
provider: "codex",
|
|
title: "Provider child",
|
|
description: null,
|
|
subtitle: "Codex worker · 4.2k tokens",
|
|
status: "completed",
|
|
createdAt: "2026-03-08T10:01:00.000Z",
|
|
updatedAt: "2026-03-08T10:02:00.000Z",
|
|
toolCallId: "call-1",
|
|
},
|
|
});
|
|
const params = { serverId: SERVER_ID, parentAgentId: "parent-a" };
|
|
|
|
expect(
|
|
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, false),
|
|
).toEqual([]);
|
|
expect(
|
|
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, true).map(
|
|
(row) => row.id,
|
|
),
|
|
).toEqual(["provider-child"]);
|
|
expect(
|
|
selectProviderSubagentsForParent(useProviderSubagentStore.getState(), params, true)[0]
|
|
?.subtitle,
|
|
).toBe("Codex worker · 4.2k tokens");
|
|
});
|
|
|
|
it("hides locally dismissed provider children while retaining their descriptor", () => {
|
|
const store = useProviderSubagentStore.getState();
|
|
store.applyUpdate(SERVER_ID, {
|
|
kind: "upsert",
|
|
subagent: {
|
|
id: "provider-child",
|
|
parentAgentId: "parent-a",
|
|
provider: "codex",
|
|
title: "Provider child",
|
|
description: null,
|
|
status: "completed",
|
|
createdAt: "2026-03-08T10:01:00.000Z",
|
|
updatedAt: "2026-03-08T10:02:00.000Z",
|
|
toolCallId: "call-1",
|
|
},
|
|
});
|
|
store.hideFromTrack(SERVER_ID, "parent-a", ["provider-child"]);
|
|
|
|
expect(
|
|
selectProviderSubagentsForParent(
|
|
useProviderSubagentStore.getState(),
|
|
{ serverId: SERVER_ID, parentAgentId: "parent-a" },
|
|
true,
|
|
),
|
|
).toEqual([]);
|
|
expect(useProviderSubagentStore.getState().descriptors.size).toBe(1);
|
|
});
|
|
|
|
it("places nested provider children only beneath their direct provider parent", () => {
|
|
const store = useProviderSubagentStore.getState();
|
|
const base = {
|
|
parentAgentId: "parent-a",
|
|
provider: "claude" as const,
|
|
title: "general-purpose",
|
|
subtitle: null,
|
|
status: "running" as const,
|
|
createdAt: "2026-09-04T10:00:00.000Z",
|
|
updatedAt: "2026-09-04T10:00:00.000Z",
|
|
toolCallId: null,
|
|
};
|
|
store.applyUpdate(SERVER_ID, {
|
|
kind: "upsert",
|
|
subagent: { ...base, id: "direct", description: "Direct", parentSubagentId: null },
|
|
});
|
|
store.applyUpdate(SERVER_ID, {
|
|
kind: "upsert",
|
|
subagent: {
|
|
...base,
|
|
id: "nested",
|
|
description: "Nested",
|
|
parentSubagentId: "direct",
|
|
},
|
|
});
|
|
|
|
expect(
|
|
selectProviderSubagentsForParent(
|
|
useProviderSubagentStore.getState(),
|
|
{ serverId: SERVER_ID, parentAgentId: "parent-a" },
|
|
true,
|
|
true,
|
|
).map((row) => row.id),
|
|
).toEqual(["direct"]);
|
|
expect(
|
|
selectProviderSubagentsForParent(
|
|
useProviderSubagentStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
providerParentSubagentId: "direct",
|
|
},
|
|
true,
|
|
true,
|
|
).map((row) => row.id),
|
|
).toEqual(["nested"]);
|
|
});
|
|
|
|
it("returns only non-archived children for the requested parent", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent-a" }),
|
|
makeAgent({ id: "child-a", parentAgentId: "parent-a" }),
|
|
makeAgent({
|
|
id: "archived-child",
|
|
parentAgentId: "parent-a",
|
|
archivedAt: new Date("2026-03-08T12:00:00.000Z"),
|
|
}),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
|
});
|
|
|
|
it("excludes siblings, unrelated agents, and grandchildren", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent-a" }),
|
|
makeAgent({ id: "parent-b" }),
|
|
makeAgent({ id: "child-a", parentAgentId: "parent-a" }),
|
|
makeAgent({ id: "sibling-b", parentAgentId: "parent-b" }),
|
|
makeAgent({ id: "grandchild-a", parentAgentId: "child-a" }),
|
|
makeAgent({ id: "unrelated" }),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
|
});
|
|
|
|
it("shows only direct children for each parent", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({ id: "child", parentAgentId: "parent" }),
|
|
makeAgent({ id: "grandchild", parentAgentId: "child" }),
|
|
]);
|
|
|
|
const parentRows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
const childRows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "child",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(parentRows.map((row) => row.id)).toEqual(["child"]);
|
|
expect(childRows.map((row) => row.id)).toEqual(["grandchild"]);
|
|
});
|
|
|
|
it("sorts by createdAt ascending", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({
|
|
id: "third",
|
|
parentAgentId: "parent",
|
|
createdAt: new Date("2026-03-08T10:03:00.000Z"),
|
|
}),
|
|
makeAgent({
|
|
id: "first",
|
|
parentAgentId: "parent",
|
|
createdAt: new Date("2026-03-08T10:01:00.000Z"),
|
|
}),
|
|
makeAgent({
|
|
id: "second",
|
|
parentAgentId: "parent",
|
|
createdAt: new Date("2026-03-08T10:02:00.000Z"),
|
|
}),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["first", "second", "third"]);
|
|
});
|
|
|
|
it("maps only row-rendered fields and does not expose onOpen", () => {
|
|
const createdAt = new Date("2026-03-08T10:01:00.000Z");
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({
|
|
id: "child",
|
|
parentAgentId: "parent",
|
|
provider: "claude",
|
|
title: "Review child",
|
|
status: "running",
|
|
requiresAttention: true,
|
|
createdAt,
|
|
model: "should-not-leak",
|
|
cwd: "/private/project",
|
|
}),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
);
|
|
|
|
expect(rows).toEqual([
|
|
{
|
|
kind: "paseo",
|
|
address: { serverId: SERVER_ID, agentId: "child" },
|
|
id: "child",
|
|
provider: "claude",
|
|
title: "Review child",
|
|
description: null,
|
|
subtitle: null,
|
|
status: "running",
|
|
turn: { phase: "idle", cancellationRequestId: null },
|
|
requiresAttention: true,
|
|
createdAt,
|
|
},
|
|
]);
|
|
expect(Object.keys(rows[0] ?? {}).sort()).toEqual([
|
|
"address",
|
|
"createdAt",
|
|
"description",
|
|
"id",
|
|
"kind",
|
|
"provider",
|
|
"requiresAttention",
|
|
"status",
|
|
"subtitle",
|
|
"title",
|
|
"turn",
|
|
]);
|
|
expect(rows[0]).not.toHaveProperty("onOpen");
|
|
expect(rows[0]).not.toHaveProperty("model");
|
|
expect(rows[0]).not.toHaveProperty("cwd");
|
|
});
|
|
|
|
it("moves a child when parentAgentId changes", () => {
|
|
const child = makeAgent({ id: "child", parentAgentId: "parent-a" });
|
|
setAgents([makeAgent({ id: "parent-a" }), makeAgent({ id: "parent-b" }), child]);
|
|
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
).map((row) => row.id),
|
|
).toEqual(["child"]);
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-b",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
).map((row) => row.id),
|
|
).toEqual([]);
|
|
|
|
setAgents([
|
|
makeAgent({ id: "parent-a" }),
|
|
makeAgent({ id: "parent-b" }),
|
|
{ ...child, parentAgentId: "parent-b", labels: { "paseo.parent-agent-id": "parent-b" } },
|
|
]);
|
|
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-a",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
).map((row) => row.id),
|
|
).toEqual([]);
|
|
expect(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent-b",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
).map((row) => row.id),
|
|
).toEqual(["child"]);
|
|
});
|
|
|
|
it("excludes children whose archive is pending", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({ id: "child-a", parentAgentId: "parent" }),
|
|
makeAgent({ id: "child-b", parentAgentId: "parent" }),
|
|
]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
new Set([`${SERVER_ID}:child-b`]),
|
|
);
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
|
});
|
|
|
|
it("returns the shared empty array when pending archive hides the last child", () => {
|
|
setAgents([makeAgent({ id: "parent" }), makeAgent({ id: "child", parentAgentId: "parent" })]);
|
|
|
|
const rows = selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "parent",
|
|
},
|
|
new Set([`${SERVER_ID}:child`]),
|
|
);
|
|
|
|
expect(rows).toEqual([]);
|
|
expect(rows).toBe(
|
|
selectSubagentsForParent(
|
|
useSessionStore.getState(),
|
|
{
|
|
serverId: SERVER_ID,
|
|
parentAgentId: "missing-parent",
|
|
},
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("createSubagentsForParentSelector", () => {
|
|
class IterationCountingMap<K, V> extends Map<K, V> {
|
|
iterations = 0;
|
|
override values() {
|
|
this.iterations += 1;
|
|
return super.values();
|
|
}
|
|
override entries() {
|
|
this.iterations += 1;
|
|
return super.entries();
|
|
}
|
|
override [Symbol.iterator]() {
|
|
this.iterations += 1;
|
|
return super[Symbol.iterator]();
|
|
}
|
|
}
|
|
|
|
const params = { serverId: SERVER_ID, parentAgentId: "parent" };
|
|
|
|
function streamTick(text: string): void {
|
|
useSessionStore.getState().setAgentStreamState(SERVER_ID, "parent", {
|
|
head: [
|
|
{
|
|
kind: "assistant_message",
|
|
id: "message-1",
|
|
text,
|
|
timestamp: new Date("2026-03-08T10:05:00.000Z"),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
it("returns the cached rows without walking agents when a stream tick leaves them unchanged", () => {
|
|
const agents = new IterationCountingMap<string, Agent>(
|
|
[
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({ id: "child", parentAgentId: "parent" }),
|
|
makeAgent({ id: "unrelated" }),
|
|
].map((agent) => [agent.id, agent]),
|
|
);
|
|
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
|
|
useSessionStore.getState().setAgents(SERVER_ID, agents);
|
|
const select = createSubagentsForParentSelector(params, EMPTY_PENDING_ARCHIVE_IDS);
|
|
const before = select(useSessionStore.getState());
|
|
const iterationsAfterFirstSelect = agents.iterations;
|
|
|
|
for (let tick = 0; tick < 100; tick += 1) {
|
|
streamTick(`token ${tick}`);
|
|
expect(select(useSessionStore.getState())).toBe(before);
|
|
}
|
|
|
|
expect(useSessionStore.getState().sessions[SERVER_ID]?.agents).toBe(agents);
|
|
expect(agents.iterations).toBe(iterationsAfterFirstSelect);
|
|
expect(before.map((row) => row.id)).toEqual(["child"]);
|
|
});
|
|
|
|
it("recomputes when the agents map changes", () => {
|
|
setAgents([makeAgent({ id: "parent" }), makeAgent({ id: "child-a", parentAgentId: "parent" })]);
|
|
const select = createSubagentsForParentSelector(params, EMPTY_PENDING_ARCHIVE_IDS);
|
|
const before = select(useSessionStore.getState());
|
|
|
|
useSessionStore.getState().setAgents(SERVER_ID, (current) => {
|
|
const child = makeAgent({
|
|
id: "child-b",
|
|
parentAgentId: "parent",
|
|
createdAt: new Date("2026-03-08T10:01:00.000Z"),
|
|
});
|
|
return new Map(current).set(child.id, child);
|
|
});
|
|
const after = select(useSessionStore.getState());
|
|
|
|
expect(after).not.toBe(before);
|
|
expect(after.map((row) => row.id)).toEqual(["child-a", "child-b"]);
|
|
});
|
|
|
|
it("keeps the previous rows when a changed agents map yields the same rows", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({ id: "child", parentAgentId: "parent" }),
|
|
makeAgent({ id: "unrelated" }),
|
|
]);
|
|
const select = createSubagentsForParentSelector(params, EMPTY_PENDING_ARCHIVE_IDS);
|
|
const before = select(useSessionStore.getState());
|
|
|
|
useSessionStore.getState().setAgents(SERVER_ID, (current) => {
|
|
return new Map(current).set("unrelated", makeAgent({ id: "unrelated", status: "running" }));
|
|
});
|
|
|
|
expect(select(useSessionStore.getState())).toBe(before);
|
|
});
|
|
|
|
it("applies the pending archive ids it was created with", () => {
|
|
setAgents([
|
|
makeAgent({ id: "parent" }),
|
|
makeAgent({ id: "child-a", parentAgentId: "parent" }),
|
|
makeAgent({ id: "child-b", parentAgentId: "parent" }),
|
|
]);
|
|
|
|
const rows = createSubagentsForParentSelector(
|
|
params,
|
|
new Set([`${SERVER_ID}:child-b`]),
|
|
)(useSessionStore.getState());
|
|
|
|
expect(rows.map((row) => row.id)).toEqual(["child-a"]);
|
|
});
|
|
it("does no agent map walks for 100 stream commits with unchanged source maps", () => {
|
|
const agents = new IterationCountingMap<string, Agent>(
|
|
Array.from({ length: 200 }, (_, i) => {
|
|
const agent = makeAgent({
|
|
id: `agent-${i}`,
|
|
workspaceId: "workspace",
|
|
parentAgentId: i < 3 ? null : `agent-${i % 3}`,
|
|
});
|
|
return [agent.id, agent] as const;
|
|
}),
|
|
);
|
|
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
|
|
useSessionStore.getState().setAgents(SERVER_ID, agents);
|
|
const selectVisibility = createWorkspaceAgentVisibilitySelector({
|
|
serverId: SERVER_ID,
|
|
workspaceId: "workspace",
|
|
});
|
|
const selectChildren = Array.from({ length: 3 }, (_, i) =>
|
|
createSubagentsForParentSelector(
|
|
{ serverId: SERVER_ID, parentAgentId: `agent-${i}` },
|
|
EMPTY_PENDING_ARCHIVE_IDS,
|
|
),
|
|
);
|
|
const select = (state: ReturnType<typeof useSessionStore.getState>) => {
|
|
selectVisibility(state);
|
|
selectChildren.forEach((selector) => selector(state));
|
|
};
|
|
select(useSessionStore.getState());
|
|
const before = agents.iterations;
|
|
const unsubscribe = useSessionStore.subscribe(select);
|
|
try {
|
|
for (let tick = 0; tick < 100; tick++) {
|
|
useSessionStore.getState().setAgentStreamState(SERVER_ID, "agent-0", {
|
|
head: [
|
|
{
|
|
kind: "assistant_message",
|
|
id: "message",
|
|
text: `token ${tick}`,
|
|
timestamp: AGENT_TIMESTAMP,
|
|
},
|
|
],
|
|
});
|
|
}
|
|
expect(useSessionStore.getState().sessions[SERVER_ID]?.agents).toBe(agents);
|
|
expect(agents.iterations - before).toBe(0);
|
|
} finally {
|
|
unsubscribe();
|
|
useSessionStore.getState().clearSession(SERVER_ID);
|
|
}
|
|
});
|
|
});
|