-
Notifications
You must be signed in to change notification settings - Fork 2.4k
fix(session): gate terminal session-end write behind an explicit final flag #1288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
4c99ba5
6a0f40d
31b48a8
c975828
a89cb78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| import { describe, it, expect, vi } from "vitest"; | ||
|
|
||
| vi.mock("../src/logger.js", () => ({ | ||
| logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, | ||
| })); | ||
|
Comment on lines
+3
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 3 'vi\.mock\(["'\'']iii-sdk|sdk\.trigger|kv\.(get|set|list)' \
test/crystallize.test.ts test/session-end-final-flag.test.tsRepository: rohitg00/agentmemory Length of output: 16655 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- applicable repository instructions ---'
find /tmp/coderabbit-repo-knowledge/rohitg00-agentmemory-ce01373c -maxdepth 2 -type f -name '*.md' -print \
-exec sh -c 'printf "\n--- %s ---\n" "$1"; head -120 "$1"' _ {} \;
printf '%s\n' '--- session test ---'
cat -n test/session-end-final-flag.test.ts | sed -n '1,130p'
printf '%s\n' '--- crystallize test setup ---'
cat -n test/crystallize.test.ts | sed -n '1,95p'
printf '%s\n' '--- iii-sdk bindings and test helper definitions ---'
rg -n -C 4 'from ["'\'']iii-sdk|mockKV|vi\.mock|const sdk|const kv' test srcRepository: rohitg00/agentmemory Length of output: 50377 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- test/session-end-final-flag.test.ts ---'
cat -n test/session-end-final-flag.test.ts
printf '%s\n' '--- test/crystallize.test.ts setup ---'
sed -n '1,90p' test/crystallize.test.ts | cat -n
printf '%s\n' '--- direct iii-sdk references in the two tests ---'
rg -n -C 3 'iii-sdk|vi\.mock|mockSdk|mockKV|registerFunction|trigger' \
test/session-end-final-flag.test.ts test/crystallize.test.ts
printf '%s\n' '--- scoped repository convention files ---'
for f in /tmp/coderabbit-repo-knowledge/rohitg00-agentmemory-ce01373c/*/*.md; do
if rg -q 'iii-sdk|session-end-final-flag|test/|mock' "$f"; then
printf '\n--- %s ---\n' "$f"
cat "$f"
fi
doneRepository: rohitg00/agentmemory Length of output: 29841 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- test helper bindings ---'
cat -n test/helpers/mocks.ts | sed -n '1,180p'
printf '%s\n' '--- API trigger bindings ---'
sed -n '1,120p' src/triggers/api.ts | cat -n
printf '%s\n' '--- runtime iii-sdk imports in the reviewed path ---'
rg -n -C 3 '(^|[^[:alnum:]_])import .*iii-sdk|from ["'\'']iii-sdk|require\(["'\'']iii-sdk' \
test/helpers/mocks.ts src/triggers/api.ts srcRepository: rohitg00/agentmemory Length of output: 27118 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- TriggerAction usage in src/triggers/api.ts ---'
rg -n -C 8 'TriggerAction|registerApiTriggers|registerTrigger|registerFunction' src/triggers/api.ts
printf '%s\n' '--- package declaration for iii-sdk ---'
rg -n -C 3 '"iii-sdk"|iii-sdk' package.json package-lock.json npm-shrinkwrap.json pnpm-lock.yaml yarn.lock 2>/dev/null || true
printf '%s\n' '--- all test mocks for runtime iii-sdk imports ---'
rg -n -C 4 'vi\.mock\(["'\'']iii-sdk|import \{[^}]*TriggerAction[^}]*\} from ["'\'']iii-sdk' test srcRepository: rohitg00/agentmemory Length of output: 50377 Mock
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| import { registerApiTriggers } from "../src/triggers/api.js"; | ||
| import { KV } from "../src/state/schema.js"; | ||
| import { mockKV, mockSdk } from "./helpers/mocks.js"; | ||
| import type { Session } from "../src/types.js"; | ||
|
|
||
| // #745: Claude Code fires Stop at the end of EVERY assistant turn, not only | ||
| // at genuine session end, and the Stop hook posts to the same | ||
| // /agentmemory/session/end endpoint as the real SessionEnd hook, with the | ||
| // same payload shape. Writing endedAt + status:"completed" unconditionally | ||
| // there marked every live session terminated on every turn, producing | ||
| // phantom "abandoned session" diagnostics. The terminal write is now gated | ||
| // on an explicit `final: true` flag that only the genuine SessionEnd hook | ||
| // sends; the per-turn Stop hook does not, and event::session::stopped keeps | ||
| // firing unconditionally on both so summarize/graph/consolidation still run | ||
| // every turn. | ||
| describe("api::session::end final flag (#745)", () => { | ||
| function seedSession(kv: ReturnType<typeof mockKV>, id = "s1") { | ||
| return kv.set(KV.sessions, id, { | ||
| id, | ||
| project: "p", | ||
| cwd: "/tmp", | ||
| startedAt: new Date().toISOString(), | ||
| status: "active", | ||
| observationCount: 3, | ||
| } satisfies Session); | ||
| } | ||
|
|
||
| it("a post without `final` does not write endedAt/status but still fans out event::session::stopped", async () => { | ||
| const kv = mockKV(); | ||
| await seedSession(kv); | ||
| const sdk = mockSdk(); | ||
| const stopped = vi.fn(async () => ({ success: true })); | ||
| sdk.registerFunction("event::session::stopped", stopped); | ||
| registerApiTriggers(sdk as never, kv as never); | ||
|
|
||
| const handler = sdk.fns.get("api::session::end")!; | ||
| const res = (await handler({ body: { sessionId: "s1" } } as never)) as { | ||
| status_code: number; | ||
| }; | ||
| expect(res.status_code).toBe(200); | ||
|
|
||
| const session = await kv.get<Session>(KV.sessions, "s1"); | ||
| expect(session?.status).toBe("active"); | ||
| expect(session?.endedAt).toBeUndefined(); | ||
|
|
||
| // Fan-out is fire-and-forget (not awaited by the handler); flush microtasks. | ||
| await new Promise((r) => setTimeout(r, 0)); | ||
| expect(stopped).toHaveBeenCalledWith({ sessionId: "s1" }); | ||
| }); | ||
|
|
||
| it("a post with final: true writes endedAt/status and still fans out event::session::stopped", async () => { | ||
| const kv = mockKV(); | ||
| await seedSession(kv); | ||
| const sdk = mockSdk(); | ||
| const stopped = vi.fn(async () => ({ success: true })); | ||
| sdk.registerFunction("event::session::stopped", stopped); | ||
| registerApiTriggers(sdk as never, kv as never); | ||
|
|
||
| const handler = sdk.fns.get("api::session::end")!; | ||
| const res = (await handler({ | ||
| body: { sessionId: "s1", final: true }, | ||
| } as never)) as { status_code: number }; | ||
| expect(res.status_code).toBe(200); | ||
|
|
||
| const session = await kv.get<Session>(KV.sessions, "s1"); | ||
| expect(session?.status).toBe("completed"); | ||
| expect(session?.endedAt).toBeDefined(); | ||
|
|
||
| await new Promise((r) => setTimeout(r, 0)); | ||
| expect(stopped).toHaveBeenCalledWith({ sessionId: "s1" }); | ||
| }); | ||
|
|
||
| it.each([["true" /* string */], [1], [{}], [[]], [null]])( | ||
| "a non-boolean final (%j) does not trigger the terminal write", | ||
| async (finalValue) => { | ||
| const kv = mockKV(); | ||
| await seedSession(kv); | ||
| const sdk = mockSdk(); | ||
| sdk.registerFunction("event::session::stopped", async () => ({ success: true })); | ||
| registerApiTriggers(sdk as never, kv as never); | ||
|
|
||
| const handler = sdk.fns.get("api::session::end")!; | ||
| const res = (await handler({ | ||
| body: { sessionId: "s1", final: finalValue }, | ||
| } as never)) as { status_code: number }; | ||
| expect(res.status_code).toBe(200); | ||
|
|
||
| const session = await kv.get<Session>(KV.sessions, "s1"); | ||
| expect(session?.status).toBe("active"); | ||
| expect(session?.endedAt).toBeUndefined(); | ||
| }, | ||
| ); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.