feat: session source in session_meta and an Automated sidebar folder (#40)

Co-authored-by: Alice <alice@prismshadow.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-07-23 00:58:43 +08:00
1 parent e0bf1706b9
commit c380113e26
30 files changed
+1023 -74

No files matched your search

+10
View File
@@ -126,6 +126,8 @@ export interface CreateSessionOptions {
baseUrl?: string;
/** Internal use: this Session's depth in the subagent spawn chain (0 at the top level), used to cap spawn depth. */
subagentDepth?: number;
/** Session origin recorded in session_meta (absent = user-created); the subagent spawn site passes "subagent", callers driven by a scheduled task pass "schedule". */
source?: "subagent" | "schedule";
}
export interface ResumeSessionOptions {
@@ -298,6 +300,7 @@ export class Agent {
thinking_level: thinkingLevel ?? "default",
agent_state: this.state.stateDir,
workspace: workspaceDir,
...(opts.source !== undefined ? { source: opts.source } : {}),
},
llm: rt.llm,
environment: rt.environment,
@@ -444,6 +447,12 @@ export class Agent {
thinking_level: thinkingLevel ?? "default",
agent_state: this.state.stateDir,
workspace: workspaceDir,
// The origin carries over from the original session_meta (a resumed scheduled/subagent
// Session stays marked). The on-disk value is untrusted: only the exact known origins
// pass; junk written by a third party is dropped rather than cast through.
...(meta.source === "subagent" || meta.source === "schedule"
? { source: meta.source }
: {}),
},
llm: rt.llm,
environment: rt.environment,
@@ -573,6 +582,7 @@ export class Agent {
...childModel,
thinkingLevel: thinkingLevel ?? null,
subagentDepth: subagentDepth + 1,
source: "subagent",
});
// All child-session messages are tagged with an origin (the child Session id,
// prepended as one hop from outer to inner); the first turn forwards the
+2
View File
@@ -92,6 +92,8 @@ export interface SessionMetaPayload {
agent_state: string;
/** Absolute path to the Workspace. */
workspace: string;
/** Session origin: spawned by a subagent / triggered by a scheduled task; absent = user-created. */
source?: "subagent" | "schedule";
}
// ---------------------------------------------------------------------------
+36 -1
View File
@@ -220,6 +220,31 @@ describe("Agent.createSession model reference ((provider, model_id) pair)", () =
});
});
describe("Agent.createSession session source (session_meta origin marker)", () => {
it("records the given source in session_meta; a user-created session carries no source key", async () => {
const agent = await createAgent();
const ws = path.join(tmpRoot, "ws-source");
await fs.mkdir(ws, { recursive: true });
const scheduled = await agent.createSession({ workspaceDir: ws, source: "schedule" });
try {
expect((scheduled.metaMessage.payload as { source?: string }).source).toBe("schedule");
} finally {
scheduled.dispose();
}
// Absent = user-created: the key must not appear at all (Trace consumers treat absence as the default).
const plain = await agent.createSession({ workspaceDir: ws });
try {
expect("source" in (plain.metaMessage.payload as unknown as Record<string, unknown>)).toBe(
false,
);
} finally {
plain.dispose();
}
});
});
describe("Agent.createSession thinking level (explicit option wins over the Agent config)", () => {
const uniThinkingOf = (llm: unknown): unknown =>
((llm as { uniConfig?: { thinking_level?: unknown } }).uniConfig ?? {}).thinking_level;
@@ -276,7 +301,13 @@ describe("run_subagent spawning follows the PARENT session (never the Project de
async function spawnedChildMeta(
runner: SubagentRunner,
input: Parameters<SubagentRunner["spawn"]>[0],
): Promise<{ provider: string; model_id: string; thinking_level: string; workspace: string }> {
): Promise<{
provider: string;
model_id: string;
thinking_level: string;
workspace: string;
source?: string;
}> {
const handle = await runner.spawn(input);
try {
const gen = handle.run({ prompt: "noop" });
@@ -292,6 +323,7 @@ describe("run_subagent spawning follows the PARENT session (never the Project de
model_id: string;
thinking_level: string;
workspace: string;
source?: string;
};
} finally {
handle.dispose();
@@ -320,6 +352,9 @@ describe("run_subagent spawning follows the PARENT session (never the Project de
expect(child.thinking_level).toBe("high");
// Workspace inheritance (behavior that predates model/thinking inheritance): locked here.
expect(child.workspace).toBe(ws);
// The spawn site marks the child's own session_meta as subagent-created — the single
// source of truth the server derives from (its registration fallback cannot mask this).
expect(child.source).toBe("subagent");
} finally {
parent.dispose();
}
+44 -1
View File
@@ -77,7 +77,12 @@ async function writeTraceFile(
return file;
}
function metaFor(sessionId: string, workspaceDir: string, model = MODEL): OmniMessage {
function metaFor(
sessionId: string,
workspaceDir: string,
model = MODEL,
source?: "subagent" | "schedule",
): OmniMessage {
return sessionMeta({
session_id: sessionId,
provider: model.provider,
@@ -88,6 +93,7 @@ function metaFor(sessionId: string, workspaceDir: string, model = MODEL): OmniMe
thinking_level: "default",
agent_state: "/agent/state",
workspace: workspaceDir,
...(source !== undefined ? { source } : {}),
});
}
@@ -116,6 +122,43 @@ describe("agent.resumeSession", () => {
expect(texts).toEqual(["hello", "hi there"]);
});
it("preserves the stored session source across resume (and its absence for user sessions)", async () => {
const agent = await createAgent({});
await writeTraceFile(tmpRoot, SID, [
metaFor(SID, workspace, MODEL, "subagent"),
userText("child task"),
requestBegin(),
assistantText("done"),
requestEnd("completed"),
]);
const session = await agent.resumeSession({ sessionId: SID });
// The rebuilt session_meta carries the origin over (a rotated Trace file must re-record it).
expect((session.metaMessage.payload as { source?: string }).source).toBe("subagent");
session.dispose();
// A user-created session's meta has no source key, and resume must not invent one.
const SID2 = "session-2026-07-06-11-00-00-abcdef02";
await writeTraceFile(tmpRoot, SID2, [metaFor(SID2, workspace), userText("hi")]);
const plain = await agent.resumeSession({ sessionId: SID2 });
expect("source" in (plain.metaMessage.payload as unknown as Record<string, unknown>)).toBe(
false,
);
plain.dispose();
// On-disk values are untrusted: a junk source written by a third party is dropped on
// resume (only the exact known origins pass), not cast through into the rebuilt meta.
const SID3 = "session-2026-07-06-12-00-00-abcdef03";
await writeTraceFile(tmpRoot, SID3, [
metaFor(SID3, workspace, MODEL, "weird-origin" as unknown as "subagent"),
userText("hi"),
]);
const junk = await agent.resumeSession({ sessionId: SID3 });
expect("source" in (junk.metaMessage.payload as unknown as Record<string, unknown>)).toBe(
false,
);
junk.dispose();
});
it("keeps abort events in resumed render history", async () => {
const agent = await createAgent({});
await writeTraceFile(tmpRoot, SID, [