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:
30 files changed
+1023
-74
No files matched your search
@@ -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
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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, [
|
||||
|
||||
Reference in new issue
Block a user