mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
fix(sessions): hide stale subagent child links
This commit is contained in:
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Sessions/subagents: stop stale ended runs and old store-only child reverse links from reappearing in `childSessions`, while keeping live descendants and recently-ended children visible. Fixes #57920.
|
||||
- Subagents: stop stale unended runs from counting as active or pending forever, while preserving restart-aborted recovery for recoverable child sessions. Fixes #71252. Thanks @hclsys.
|
||||
- Gateway/tools: allow `POST /tools/invoke` to reach plugin-backed catalog tools such as `browser` when no core implementation exists, while still preferring built-in tools for real core names. Thanks @chat2way.
|
||||
- Browser/security: require `operator.admin` for the `browser.request` gateway method, matching the host/browser-node control authority exposed by that route. Thanks @RichardCao.
|
||||
|
||||
@@ -214,6 +214,11 @@ Operational guidance:
|
||||
- Start child work once and wait for completion events instead of building poll
|
||||
loops around `sessions_list`, `sessions_history`, `/subagents list`, or
|
||||
`exec` sleep commands.
|
||||
- `sessions_list` and `/subagents list` keep child-session relationships focused
|
||||
on live work: live children remain attached, ended children stay visible for a
|
||||
short recent window, and stale store-only child links are ignored after their
|
||||
freshness window. This prevents old `spawnedBy` / `parentSessionKey` metadata
|
||||
from resurrecting ghost children after restart.
|
||||
- If a child completion event arrives after you already sent the final answer,
|
||||
the correct follow-up is the exact silent token `NO_REPLY` / `no_reply`.
|
||||
|
||||
|
||||
@@ -115,9 +115,52 @@ describe("buildSubagentList", () => {
|
||||
});
|
||||
|
||||
expect(list.active[0]?.status).toBe("active (waiting on 1 child)");
|
||||
expect(list.active[0]?.childSessions).toEqual([
|
||||
"agent:main:subagent:orchestrator-ended:subagent:child",
|
||||
]);
|
||||
expect(list.recent).toEqual([]);
|
||||
});
|
||||
|
||||
it("omits old ended descendants from child session summaries", () => {
|
||||
const now = Date.now();
|
||||
const parentRun = {
|
||||
runId: "run-parent-active-old-child",
|
||||
childSessionKey: "agent:main:subagent:parent-active-old-child",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "parent active",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 120_000,
|
||||
startedAt: now - 120_000,
|
||||
} satisfies SubagentRunRecord;
|
||||
addSubagentRunForTests(parentRun);
|
||||
addSubagentRunForTests({
|
||||
runId: "run-old-ended-child-summary",
|
||||
childSessionKey: `${parentRun.childSessionKey}:subagent:old-ended-child`,
|
||||
requesterSessionKey: parentRun.childSessionKey,
|
||||
requesterDisplayKey: "subagent:parent-active-old-child",
|
||||
task: "old ended child",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 60 * 60_000,
|
||||
startedAt: now - 59 * 60_000,
|
||||
endedAt: now - 31 * 60_000,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
const cfg = {
|
||||
commands: { text: true },
|
||||
channels: { whatsapp: { allowFrom: ["*"] } },
|
||||
} as OpenClawConfig;
|
||||
|
||||
const list = buildSubagentList({
|
||||
cfg,
|
||||
runs: [parentRun],
|
||||
recentMinutes: 30,
|
||||
taskMaxChars: 110,
|
||||
});
|
||||
|
||||
expect(list.active[0]?.childSessions).toBeUndefined();
|
||||
});
|
||||
|
||||
it("formats io and prompt/cache usage from session entries", async () => {
|
||||
const run = {
|
||||
runId: "run-usage",
|
||||
|
||||
@@ -13,14 +13,21 @@ import {
|
||||
} from "../shared/subagents-format.js";
|
||||
import { resolveModelDisplayName, resolveModelDisplayRef } from "./model-selection-display.js";
|
||||
import { subagentRuns } from "./subagent-registry-memory.js";
|
||||
import { countPendingDescendantRunsFromRuns } from "./subagent-registry-queries.js";
|
||||
import {
|
||||
countActiveDescendantRunsFromRuns,
|
||||
countPendingDescendantRunsFromRuns,
|
||||
} from "./subagent-registry-queries.js";
|
||||
import {
|
||||
getSubagentSessionRuntimeMs,
|
||||
getSubagentSessionStartedAt,
|
||||
} from "./subagent-registry-read.js";
|
||||
import { getSubagentRunsSnapshotForRead } from "./subagent-registry-state.js";
|
||||
import type { SubagentRunRecord } from "./subagent-registry.types.js";
|
||||
import { hasSubagentRunEnded, isLiveUnendedSubagentRun } from "./subagent-run-liveness.js";
|
||||
import {
|
||||
hasSubagentRunEnded,
|
||||
isLiveUnendedSubagentRun,
|
||||
shouldKeepSubagentRunChildLink,
|
||||
} from "./subagent-run-liveness.js";
|
||||
|
||||
export type SubagentListItem = {
|
||||
index: number;
|
||||
@@ -80,7 +87,11 @@ export function resolveSessionEntryForKey(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildLatestSubagentRunIndex(runs: Map<string, SubagentRunRecord>) {
|
||||
export function buildLatestSubagentRunIndex(
|
||||
runs: Map<string, SubagentRunRecord>,
|
||||
options?: { now?: number },
|
||||
) {
|
||||
const now = options?.now ?? Date.now();
|
||||
const latestByChildSessionKey = new Map<string, SubagentRunRecord>();
|
||||
for (const entry of runs.values()) {
|
||||
const childSessionKey = entry.childSessionKey?.trim();
|
||||
@@ -100,6 +111,14 @@ export function buildLatestSubagentRunIndex(runs: Map<string, SubagentRunRecord>
|
||||
if (!controllerSessionKey) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!shouldKeepSubagentRunChildLink(entry, {
|
||||
activeDescendants: countActiveDescendantRunsFromRuns(runs, childSessionKey),
|
||||
now,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const existing = childSessionsByController.get(controllerSessionKey);
|
||||
if (existing) {
|
||||
existing.push(childSessionKey);
|
||||
|
||||
@@ -166,7 +166,7 @@ describe("subagent registry persistence", () => {
|
||||
const waitForRegistryWork = async (predicate: () => boolean | Promise<boolean>) => {
|
||||
await vi.waitFor(async () => expect(await predicate()).toBe(true), {
|
||||
interval: 1,
|
||||
timeout: 1_000,
|
||||
timeout: 5_000,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
isLiveUnendedSubagentRun,
|
||||
RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS,
|
||||
isStaleUnendedSubagentRun,
|
||||
STALE_UNENDED_SUBAGENT_RUN_MS,
|
||||
shouldKeepSubagentRunChildLink,
|
||||
} from "./subagent-run-liveness.js";
|
||||
|
||||
describe("subagent run liveness", () => {
|
||||
@@ -72,4 +74,43 @@ describe("subagent run liveness", () => {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps child links only while live, recently ended, or waiting on descendants", () => {
|
||||
expect(shouldKeepSubagentRunChildLink({ createdAt: now - 60_000 }, { now })).toBe(true);
|
||||
expect(
|
||||
shouldKeepSubagentRunChildLink(
|
||||
{
|
||||
createdAt: now - RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS - 60_000,
|
||||
endedAt: now - RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS + 1,
|
||||
},
|
||||
{ now },
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldKeepSubagentRunChildLink(
|
||||
{
|
||||
createdAt: now - RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS - 60_000,
|
||||
endedAt: now - RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS - 1,
|
||||
},
|
||||
{ now },
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldKeepSubagentRunChildLink(
|
||||
{
|
||||
createdAt: now - RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS - 60_000,
|
||||
endedAt: now - RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS - 1,
|
||||
},
|
||||
{ activeDescendants: 1, now },
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldKeepSubagentRunChildLink(
|
||||
{
|
||||
createdAt: now - STALE_UNENDED_SUBAGENT_RUN_MS - 1,
|
||||
},
|
||||
{ now },
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,13 @@ import type { SubagentRunRecord } from "./subagent-registry.types.js";
|
||||
import { getSubagentSessionStartedAt } from "./subagent-session-metrics.js";
|
||||
|
||||
export const STALE_UNENDED_SUBAGENT_RUN_MS = 2 * 60 * 60 * 1_000;
|
||||
export const RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS = 30 * 60 * 1_000;
|
||||
const EXPLICIT_TIMEOUT_STALE_GRACE_MS = 60_000;
|
||||
const MIN_REALISTIC_RUN_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
|
||||
|
||||
export function hasSubagentRunEnded(entry: Pick<SubagentRunRecord, "endedAt">): boolean {
|
||||
export function hasSubagentRunEnded<T extends Pick<SubagentRunRecord, "endedAt">>(
|
||||
entry: T,
|
||||
): entry is T & { endedAt: number } {
|
||||
return typeof entry.endedAt === "number" && Number.isFinite(entry.endedAt);
|
||||
}
|
||||
|
||||
@@ -50,3 +53,32 @@ export function isLiveUnendedSubagentRun(
|
||||
): boolean {
|
||||
return !hasSubagentRunEnded(entry) && !isStaleUnendedSubagentRun(entry, now);
|
||||
}
|
||||
|
||||
export function isRecentlyEndedSubagentRun(
|
||||
entry: Pick<SubagentRunRecord, "endedAt">,
|
||||
now = Date.now(),
|
||||
recentMs = RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS,
|
||||
): boolean {
|
||||
if (!hasSubagentRunEnded(entry)) {
|
||||
return false;
|
||||
}
|
||||
return now - entry.endedAt <= recentMs;
|
||||
}
|
||||
|
||||
export function shouldKeepSubagentRunChildLink(
|
||||
entry: Pick<
|
||||
SubagentRunRecord,
|
||||
"createdAt" | "startedAt" | "sessionStartedAt" | "endedAt" | "runTimeoutSeconds"
|
||||
>,
|
||||
options?: {
|
||||
activeDescendants?: number;
|
||||
now?: number;
|
||||
},
|
||||
): boolean {
|
||||
const now = options?.now ?? Date.now();
|
||||
return (
|
||||
isLiveUnendedSubagentRun(entry, now) ||
|
||||
(options?.activeDescendants ?? 0) > 0 ||
|
||||
isRecentlyEndedSubagentRun(entry, now)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -726,6 +726,184 @@ describe("listSessionsFromStore subagent metadata", () => {
|
||||
expect(result.sessions.map((session) => session.key)).toEqual(["agent:main:dashboard:child"]);
|
||||
});
|
||||
|
||||
test("does not reattach stale terminal store-only child links", () => {
|
||||
resetSubagentRegistryForTests({ persist: false });
|
||||
const now = Date.now();
|
||||
const staleAt = now - 2 * 60 * 60_000;
|
||||
const store: Record<string, SessionEntry> = {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: now,
|
||||
} as SessionEntry,
|
||||
"agent:claude:acp:done-child": {
|
||||
sessionId: "sess-done-child",
|
||||
updatedAt: staleAt,
|
||||
spawnedBy: "agent:main:main",
|
||||
status: "done",
|
||||
endedAt: staleAt,
|
||||
} as SessionEntry,
|
||||
};
|
||||
|
||||
const all = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: {},
|
||||
});
|
||||
const main = all.sessions.find((session) => session.key === "agent:main:main");
|
||||
expect(main?.childSessions).toBeUndefined();
|
||||
|
||||
const filtered = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: {
|
||||
spawnedBy: "agent:main:main",
|
||||
},
|
||||
});
|
||||
expect(filtered.sessions.map((session) => session.key)).toEqual([]);
|
||||
});
|
||||
|
||||
test("does not reattach stale orphan store-only child links without lifecycle fields", () => {
|
||||
resetSubagentRegistryForTests({ persist: false });
|
||||
const now = Date.now();
|
||||
const staleAt = now - 2 * 60 * 60_000;
|
||||
const store: Record<string, SessionEntry> = {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: now,
|
||||
} as SessionEntry,
|
||||
"agent:main:subagent:orphan": {
|
||||
sessionId: "sess-orphan",
|
||||
updatedAt: staleAt,
|
||||
parentSessionKey: "agent:main:main",
|
||||
} as SessionEntry,
|
||||
};
|
||||
|
||||
const all = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: {},
|
||||
});
|
||||
const main = all.sessions.find((session) => session.key === "agent:main:main");
|
||||
expect(main?.childSessions).toBeUndefined();
|
||||
|
||||
const filtered = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: {
|
||||
spawnedBy: "agent:main:main",
|
||||
},
|
||||
});
|
||||
expect(filtered.sessions.map((session) => session.key)).toEqual([]);
|
||||
});
|
||||
|
||||
test("does not keep old ended registry runs attached as child sessions", () => {
|
||||
const now = Date.now();
|
||||
const store: Record<string, SessionEntry> = {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: now,
|
||||
} as SessionEntry,
|
||||
"agent:main:subagent:old-ended": {
|
||||
sessionId: "sess-old-ended",
|
||||
updatedAt: now - 60 * 60_000,
|
||||
spawnedBy: "agent:main:main",
|
||||
} as SessionEntry,
|
||||
};
|
||||
|
||||
addSubagentRunForTests({
|
||||
runId: "run-old-ended",
|
||||
childSessionKey: "agent:main:subagent:old-ended",
|
||||
controllerSessionKey: "agent:main:main",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "old ended task",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 60 * 60_000,
|
||||
startedAt: now - 59 * 60_000,
|
||||
endedAt: now - 31 * 60_000,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
|
||||
const all = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: {},
|
||||
});
|
||||
const main = all.sessions.find((session) => session.key === "agent:main:main");
|
||||
expect(main?.childSessions).toBeUndefined();
|
||||
|
||||
const filtered = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: {
|
||||
spawnedBy: "agent:main:main",
|
||||
},
|
||||
});
|
||||
expect(filtered.sessions.map((session) => session.key)).toEqual([]);
|
||||
});
|
||||
|
||||
test("keeps ended parents attached while live descendants are still running", () => {
|
||||
const now = Date.now();
|
||||
const parentKey = "agent:main:subagent:ended-parent";
|
||||
const childKey = "agent:main:subagent:ended-parent:subagent:live-child";
|
||||
const store: Record<string, SessionEntry> = {
|
||||
"agent:main:main": {
|
||||
sessionId: "sess-main",
|
||||
updatedAt: now,
|
||||
} as SessionEntry,
|
||||
[parentKey]: {
|
||||
sessionId: "sess-ended-parent",
|
||||
updatedAt: now - 31 * 60_000,
|
||||
spawnedBy: "agent:main:main",
|
||||
} as SessionEntry,
|
||||
[childKey]: {
|
||||
sessionId: "sess-live-child",
|
||||
updatedAt: now,
|
||||
spawnedBy: parentKey,
|
||||
} as SessionEntry,
|
||||
};
|
||||
|
||||
addSubagentRunForTests({
|
||||
runId: "run-ended-parent",
|
||||
childSessionKey: parentKey,
|
||||
controllerSessionKey: "agent:main:main",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
task: "ended parent task",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 60 * 60_000,
|
||||
startedAt: now - 59 * 60_000,
|
||||
endedAt: now - 31 * 60_000,
|
||||
outcome: { status: "ok" },
|
||||
});
|
||||
addSubagentRunForTests({
|
||||
runId: "run-live-child",
|
||||
childSessionKey: childKey,
|
||||
controllerSessionKey: parentKey,
|
||||
requesterSessionKey: parentKey,
|
||||
requesterDisplayKey: "ended-parent",
|
||||
task: "live child task",
|
||||
cleanup: "keep",
|
||||
createdAt: now - 1_000,
|
||||
startedAt: now - 900,
|
||||
});
|
||||
|
||||
const result = listSessionsFromStore({
|
||||
cfg,
|
||||
storePath: "/tmp/sessions.json",
|
||||
store,
|
||||
opts: {},
|
||||
});
|
||||
const main = result.sessions.find((session) => session.key === "agent:main:main");
|
||||
expect(main?.childSessions).toEqual([parentKey]);
|
||||
});
|
||||
|
||||
test("falls back to persisted subagent timing after run archival", () => {
|
||||
const now = Date.now();
|
||||
const store: Record<string, SessionEntry> = {
|
||||
|
||||
@@ -19,12 +19,17 @@ import {
|
||||
resolvePersistedSelectedModelRef,
|
||||
} from "../agents/model-selection.js";
|
||||
import {
|
||||
countActiveDescendantRuns,
|
||||
getSessionDisplaySubagentRunByChildSessionKey,
|
||||
getSubagentSessionRuntimeMs,
|
||||
getSubagentSessionStartedAt,
|
||||
listSubagentRunsForController,
|
||||
resolveSubagentSessionStatus,
|
||||
} from "../agents/subagent-registry-read.js";
|
||||
import {
|
||||
RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS,
|
||||
shouldKeepSubagentRunChildLink,
|
||||
} from "../agents/subagent-run-liveness.js";
|
||||
import {
|
||||
listThinkingLevelOptions,
|
||||
resolveThinkingDefaultForModel,
|
||||
@@ -81,6 +86,7 @@ import type {
|
||||
GatewayAgentRow,
|
||||
GatewaySessionRow,
|
||||
GatewaySessionsDefaults,
|
||||
SessionRunStatus,
|
||||
SessionsListResult,
|
||||
} from "./session-utils.types.js";
|
||||
|
||||
@@ -291,9 +297,36 @@ function resolveEstimatedSessionCostUsd(params: {
|
||||
return resolveNonNegativeNumber(estimated);
|
||||
}
|
||||
|
||||
const STALE_STORE_ONLY_CHILD_LINK_MS = 60 * 60 * 1_000;
|
||||
|
||||
function isFinitePositiveTimestamp(value: unknown): value is number {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
||||
}
|
||||
|
||||
function isTerminalSessionStatus(status: unknown): status is Exclude<SessionRunStatus, "running"> {
|
||||
return status === "done" || status === "failed" || status === "killed" || status === "timeout";
|
||||
}
|
||||
|
||||
function shouldKeepStoreOnlyChildLink(entry: SessionEntry, now: number): boolean {
|
||||
if (isTerminalSessionStatus(entry.status) || isFinitePositiveTimestamp(entry.endedAt)) {
|
||||
const endedAt = isFinitePositiveTimestamp(entry.endedAt) ? entry.endedAt : entry.updatedAt;
|
||||
return (
|
||||
isFinitePositiveTimestamp(endedAt) && now - endedAt <= RECENT_ENDED_SUBAGENT_CHILD_SESSION_MS
|
||||
);
|
||||
}
|
||||
if (entry.status === "running" || isFinitePositiveTimestamp(entry.startedAt)) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
isFinitePositiveTimestamp(entry.updatedAt) &&
|
||||
now - entry.updatedAt <= STALE_STORE_ONLY_CHILD_LINK_MS
|
||||
);
|
||||
}
|
||||
|
||||
function resolveChildSessionKeys(
|
||||
controllerSessionKey: string,
|
||||
store: Record<string, SessionEntry>,
|
||||
now = Date.now(),
|
||||
): string[] | undefined {
|
||||
const childSessionKeys = new Set<string>();
|
||||
for (const entry of listSubagentRunsForController(controllerSessionKey)) {
|
||||
@@ -302,12 +335,23 @@ function resolveChildSessionKeys(
|
||||
continue;
|
||||
}
|
||||
const latest = getSessionDisplaySubagentRunByChildSessionKey(childSessionKey);
|
||||
if (!latest) {
|
||||
continue;
|
||||
}
|
||||
const latestControllerSessionKey =
|
||||
normalizeOptionalString(latest?.controllerSessionKey) ||
|
||||
normalizeOptionalString(latest?.requesterSessionKey);
|
||||
if (latestControllerSessionKey !== controllerSessionKey) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!shouldKeepSubagentRunChildLink(latest, {
|
||||
activeDescendants: countActiveDescendantRuns(childSessionKey),
|
||||
now,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
childSessionKeys.add(childSessionKey);
|
||||
}
|
||||
for (const [key, entry] of Object.entries(store)) {
|
||||
@@ -327,6 +371,16 @@ function resolveChildSessionKeys(
|
||||
if (latestControllerSessionKey !== controllerSessionKey) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!shouldKeepSubagentRunChildLink(latest, {
|
||||
activeDescendants: countActiveDescendantRuns(key),
|
||||
now,
|
||||
})
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} else if (!shouldKeepStoreOnlyChildLink(entry, now)) {
|
||||
continue;
|
||||
}
|
||||
childSessionKeys.add(key);
|
||||
}
|
||||
@@ -1262,7 +1316,7 @@ export function buildGatewaySessionRow(params: {
|
||||
typeof totalTokens === "number" && Number.isFinite(totalTokens) && totalTokens > 0
|
||||
? true
|
||||
: transcriptUsage?.totalTokensFresh === true;
|
||||
const childSessions = resolveChildSessionKeys(key, store);
|
||||
const childSessions = resolveChildSessionKeys(key, store, now);
|
||||
const latestCompactionCheckpoint = resolveLatestCompactionCheckpoint(entry);
|
||||
const estimatedCostUsd =
|
||||
resolveEstimatedSessionCostUsd({
|
||||
@@ -1445,9 +1499,18 @@ export function listSessionsFromStore(params: {
|
||||
const latestControllerSessionKey =
|
||||
normalizeOptionalString(latest.controllerSessionKey) ||
|
||||
normalizeOptionalString(latest.requesterSessionKey);
|
||||
return latestControllerSessionKey === spawnedBy;
|
||||
return (
|
||||
latestControllerSessionKey === spawnedBy &&
|
||||
shouldKeepSubagentRunChildLink(latest, {
|
||||
activeDescendants: countActiveDescendantRuns(key),
|
||||
now,
|
||||
})
|
||||
);
|
||||
}
|
||||
return entry?.spawnedBy === spawnedBy || entry?.parentSessionKey === spawnedBy;
|
||||
return (
|
||||
shouldKeepStoreOnlyChildLink(entry, now) &&
|
||||
(entry?.spawnedBy === spawnedBy || entry?.parentSessionKey === spawnedBy)
|
||||
);
|
||||
})
|
||||
.filter(([, entry]) => {
|
||||
if (!label) {
|
||||
|
||||
Reference in New Issue
Block a user