


























@@ -482,3 +482,159 @@ test("sessions.create without emitCommandHooks does not fire command:new hook (#
482482expect(sessionLifecycleHookMocks.runSessionEnd).not.toHaveBeenCalled();
483483expect(sessionLifecycleHookMocks.runSessionStart).not.toHaveBeenCalled();
484484});
485+486+test("sessions.reset drops cli session bindings so the next turn does not --resume the old claude-cli session", async () => {
487+const { dir } = await createSessionStoreDir();
488+await writeSingleLineSession(dir, "sess-with-binding", "hello");
489+490+await writeSessionStore({
491+entries: {
492+main: sessionStoreEntry("sess-with-binding", {
493+claudeCliSessionId: "claude-cli-old-session",
494+cliSessionBindings: {
495+"claude-cli": { sessionId: "claude-cli-old-session" },
496+},
497+cliSessionIds: { "claude-cli": "claude-cli-old-session" },
498+}),
499+},
500+});
501+502+const [{ getRuntimeConfig }, { resolveGatewaySessionStoreTarget }, { loadSessionStore }] =
503+await Promise.all([
504+import("../config/config.js"),
505+import("./session-utils.js"),
506+import("../config/sessions.js"),
507+]);
508+const gatewayStorePath = resolveGatewaySessionStoreTarget({
509+cfg: getRuntimeConfig(),
510+key: "main",
511+}).storePath;
512+513+const reset = await directSessionReq<{ ok: true; key: string }>("sessions.reset", {
514+key: "main",
515+reason: "new",
516+});
517+expect(reset.ok).toBe(true);
518+519+const store = loadSessionStore(gatewayStorePath, { skipCache: true });
520+const nextEntry = store["agent:main:main"];
521+expect(nextEntry).toBeDefined();
522+expect(nextEntry?.sessionId).not.toBe("sess-with-binding");
523+expect(nextEntry?.claudeCliSessionId).toBeUndefined();
524+expect(nextEntry?.cliSessionBindings).toBeUndefined();
525+expect(nextEntry?.cliSessionIds).toBeUndefined();
526+});
527+528+test("sessions.reset clears cli session bindings for parent-linked non-subagent sessions (e.g. dashboard children)", async () => {
529+const { dir } = await createSessionStoreDir();
530+const dashboardTranscript = path.join(dir, "sess-dashboard-child.jsonl");
531+await fs.writeFile(
532+dashboardTranscript,
533+`${JSON.stringify({
534+ type: "message",
535+ id: "m-dashboard",
536+ message: { role: "user", content: "hello from dashboard child" },
537+ })}\n`,
538+"utf-8",
539+);
540+541+await writeSessionStore({
542+entries: {
543+"dashboard:child:42": sessionStoreEntry("sess-dashboard-child", {
544+sessionFile: dashboardTranscript,
545+// parentSessionKey is set but the session key carries no `:subagent:`
546+// marker, so this is a user-facing parent-linked session, not a
547+// spawned subagent. The tighter predicate should still clear the
548+// CLI binding here so /reset matches user intuition.
549+parentSessionKey: "agent:main:main",
550+claudeCliSessionId: "claude-cli-dashboard-session",
551+cliSessionBindings: {
552+"claude-cli": { sessionId: "claude-cli-dashboard-session" },
553+},
554+cliSessionIds: { "claude-cli": "claude-cli-dashboard-session" },
555+}),
556+},
557+});
558+559+const [{ getRuntimeConfig }, { resolveGatewaySessionStoreTarget }, { loadSessionStore }] =
560+await Promise.all([
561+import("../config/config.js"),
562+import("./session-utils.js"),
563+import("../config/sessions.js"),
564+]);
565+const gatewayStorePath = resolveGatewaySessionStoreTarget({
566+cfg: getRuntimeConfig(),
567+key: "dashboard:child:42",
568+}).storePath;
569+570+const reset = await directSessionReq<{ ok: true; key: string }>("sessions.reset", {
571+key: "dashboard:child:42",
572+reason: "new",
573+});
574+expect(reset.ok).toBe(true);
575+576+const store = loadSessionStore(gatewayStorePath, { skipCache: true });
577+const nextEntry = store["agent:main:dashboard:child:42"];
578+expect(nextEntry).toBeDefined();
579+expect(nextEntry?.sessionId).not.toBe("sess-dashboard-child");
580+expect(nextEntry?.claudeCliSessionId).toBeUndefined();
581+expect(nextEntry?.cliSessionBindings).toBeUndefined();
582+expect(nextEntry?.cliSessionIds).toBeUndefined();
583+});
584+585+test("sessions.reset preserves cli session bindings for spawned subagents (Tak Hoffman's fa56682b3ced contract)", async () => {
586+const { dir } = await createSessionStoreDir();
587+const childTranscript = path.join(dir, "sess-spawned-child.jsonl");
588+await fs.writeFile(
589+childTranscript,
590+`${JSON.stringify({
591+ type: "message",
592+ id: "m-child",
593+ message: { role: "user", content: "hello from spawned child" },
594+ })}\n`,
595+"utf-8",
596+);
597+598+await writeSessionStore({
599+entries: {
600+"subagent:child": sessionStoreEntry("sess-spawned-child", {
601+sessionFile: childTranscript,
602+parentSessionKey: "agent:main:main",
603+spawnedBy: "agent:main:main",
604+subagentRole: "orchestrator",
605+claudeCliSessionId: "claude-cli-child-session",
606+cliSessionBindings: {
607+"claude-cli": { sessionId: "claude-cli-child-session" },
608+},
609+cliSessionIds: { "claude-cli": "claude-cli-child-session" },
610+}),
611+},
612+});
613+614+const [{ getRuntimeConfig }, { resolveGatewaySessionStoreTarget }, { loadSessionStore }] =
615+await Promise.all([
616+import("../config/config.js"),
617+import("./session-utils.js"),
618+import("../config/sessions.js"),
619+]);
620+const gatewayStorePath = resolveGatewaySessionStoreTarget({
621+cfg: getRuntimeConfig(),
622+key: "subagent:child",
623+}).storePath;
624+625+const reset = await directSessionReq<{ ok: true; key: string }>("sessions.reset", {
626+key: "subagent:child",
627+reason: "new",
628+});
629+expect(reset.ok).toBe(true);
630+631+const store = loadSessionStore(gatewayStorePath, { skipCache: true });
632+const nextEntry = store["agent:main:subagent:child"];
633+expect(nextEntry).toBeDefined();
634+expect(nextEntry?.sessionId).not.toBe("sess-spawned-child");
635+expect(nextEntry?.claudeCliSessionId).toBe("claude-cli-child-session");
636+expect(nextEntry?.cliSessionBindings).toEqual({
637+"claude-cli": { sessionId: "claude-cli-child-session" },
638+});
639+expect(nextEntry?.cliSessionIds).toEqual({ "claude-cli": "claude-cli-child-session" });
640+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。