Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `session_close` now closes the session you name, or refuses (GIT-122). On 2026-10-04 a close that
named one session landed on another, already-closed one and overwrote its close. The Claude
remote-devices bridge strips a parameter called `session_id`, so the server received no id and
silently fell back to its own bound session. Now: `session_close` accepts the same id as
`gitmem_session_id`, a name a proxy will not claim (if both are given and differ, it refuses);
closing a session that is already closed is refused unless you pass `reclose: true`; a call with no
id is refused while several sessions are live; a resolved id that differs from the requested one is
refused before anything is written; the result and display show the full id actually written; and
the incoming id is logged to stderr. `closing-payload.json` now lives in the session's own folder
(`sessions/<id>/closing-payload.json`) and is refused if it names another session. The old shared
file at the store root is still read for one release when it carries a `session_id` naming the
session being closed (the Stop hook, `gitmem init` and the agent templates now tell agents to
include it), or, with no `session_id`, only while a single session is live.
- Running the e2e suite no longer deletes your real `~/.gitmem`. `cursor-cross-tool.test.ts` ran
`gitmem uninstall --all` with your real HOME, and `--all` removes the memory store (session
registry, per-session files, local `threads.json`, closing payloads). Those tests now run against
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md.template
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ On "closing", "done for now", or "wrapping up":
Payload schema (`.gitmem/closing-payload.json`):
```json
{
"session_id": "<your session id from session_start>",
"closing_reflection": {
"what_broke": "...",
"what_took_longer": "...",
Expand Down
2 changes: 2 additions & 0 deletions bin/init-wizard.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ async function stepMemoryStore() {
const payloadPath = join(storeDir, "closing-payload.json");
if (!existsSync(payloadPath)) {
writeJson(payloadPath, {
session_id: "",
closing_reflection: {
what_broke: "",
what_took_longer: "",
Expand Down Expand Up @@ -525,6 +526,7 @@ async function stepMemoryStore() {
const templatePath = join(storeDir, "closing-payload-template.json");
if (!existsSync(templatePath)) {
writeJson(templatePath, {
session_id: "your session id from session_start (required)",
closing_reflection: {
what_broke: "", what_took_longer: "", do_differently: "",
what_worked: "", wrong_assumption: "", scars_applied: [],
Expand Down
1 change: 1 addition & 0 deletions copilot-instructions.template
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Safe alternatives: `env | grep -c VARNAME` (count only), `[ -n "$VARNAME" ] && e
Payload schema (`.gitmem/closing-payload.json`):
```json
{
"session_id": "<your session id from session_start>",
"closing_reflection": {
"what_broke": "...",
"what_took_longer": "...",
Expand Down
1 change: 1 addition & 0 deletions cursorrules.template
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ On "closing", "done for now", or "wrapping up":
Payload schema (`.gitmem/closing-payload.json`):
```json
{
"session_id": "<your session id from session_start>",
"closing_reflection": {
"what_broke": "...",
"what_took_longer": "...",
Expand Down
2 changes: 1 addition & 1 deletion hooks/scripts/session-close-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ if [ "$SESSION_STARTED" = "true" ]; then
cat <<HOOKJSON
{
"decision": "block",
"reason": "GITMEM SESSION STILL OPEN — Run the standard closing ceremony:\n\n1. YOU (the agent) ANSWER these 7 reflection questions based on the session. Display your answers to the human:\n - what_broke: What broke that you didn't expect?\n - what_took_longer: What took longer than it should have?\n - do_differently: What would you do differently next time?\n - what_worked: What pattern or approach worked well?\n - wrong_assumption: What assumption was wrong?\n - scars_applied: Which scars or institutional knowledge did you apply?\n - institutional_memory: What from this session should be captured?\n\n2. ASK the human: 'Any corrections or additions to my answers?' WAIT for their response.\n\n3. WRITE structured payload to ${PAYLOAD_JSON} (this exact absolute path — it is where session_close reads) with closing_reflection (7 fields above, incorporating human corrections), task_completion (timestamps), and human_corrections.\n\n4. CALL session_close with session_id and close_type: 'standard'.\n\nFor trivial sessions (< 30min, exploratory only), use close_type: 'quick' instead — no questions needed."
"reason": "GITMEM SESSION STILL OPEN — Run the standard closing ceremony:\n\n1. YOU (the agent) ANSWER these 7 reflection questions based on the session. Display your answers to the human:\n - what_broke: What broke that you didn't expect?\n - what_took_longer: What took longer than it should have?\n - do_differently: What would you do differently next time?\n - what_worked: What pattern or approach worked well?\n - wrong_assumption: What assumption was wrong?\n - scars_applied: Which scars or institutional knowledge did you apply?\n - institutional_memory: What from this session should be captured?\n\n2. ASK the human: 'Any corrections or additions to my answers?' WAIT for their response.\n\n3. WRITE structured payload to ${PAYLOAD_JSON} (this exact absolute path — it is where session_close reads) with \"session_id\" (YOUR session id from session_start — required, so a close in another concurrent session cannot read this payload), closing_reflection (7 fields above, incorporating human corrections), task_completion (timestamps), and human_corrections.\n\n4. CALL session_close with session_id and close_type: 'standard'.\n\nFor trivial sessions (< 30min, exploratory only), use close_type: 'quick' instead — no questions needed."
}
HOOKJSON
exit 0
Expand Down
5 changes: 5 additions & 0 deletions hooks/tests/test-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,11 @@ PAYLOAD_EXPECTED="$GITMEM_DIR/closing-payload.json"
echo "$OUTPUT" | grep -qF "WRITE structured payload to $PAYLOAD_EXPECTED (this exact absolute path" \
&& pass "Stop hook text prints the resolved absolute payload path" \
|| fail "Stop hook payload path" "$PAYLOAD_EXPECTED" "$OUTPUT"
# GIT-122: the instruction tells the agent to put its own session_id in the
# payload, because the shared root file is only read when it names its session.
echo "$OUTPUT" | grep -qF 'with \"session_id\" (YOUR session id from session_start' \
&& pass "Stop hook text tells the agent to include session_id in the payload" \
|| fail "Stop hook session_id instruction" 'with "session_id" (YOUR session id' "$OUTPUT"
echo "$OUTPUT" | (command -v node >/dev/null && node -e 'let d="";process.stdin.on("data",c=>d+=c).on("end",()=>{JSON.parse(d)})' 2>/dev/null) \
&& pass "Stop hook output with the path is valid JSON" \
|| fail "Stop hook JSON" "parseable JSON" "$OUTPUT"
Expand Down
4 changes: 4 additions & 0 deletions src/schemas/session-close.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ export const SessionCloseParamsSchema = z.object({
// could never execute. The agent had to already know the id, which is exactly
// what a restart (and context compaction) takes away.
session_id: SessionIdSchema.optional(),
// GIT-122: same id under a name a proxy will not claim (the remote-devices
// bridge strips `session_id`). Both given and different is refused at close.
gitmem_session_id: SessionIdSchema.optional(),
close_type: CloseTypeSchema,
task_completion: TaskCompletionSchema.optional(),
closing_reflection: ClosingReflectionSchema.optional(),
Expand All @@ -107,6 +110,7 @@ export const SessionCloseParamsSchema = z.object({
scars_to_record: z.array(ScarUsageEntrySchema).optional(),
capture_transcript: z.boolean().optional(),
transcript_path: SafeTranscriptPathSchema.optional(),
reclose: z.boolean().optional(),
});

export type SessionCloseParams = z.infer<typeof SessionCloseParamsSchema>;
Expand Down
10 changes: 9 additions & 1 deletion src/tools/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ export const TOOLS = [
type: "string",
description: "Session ID from session_start",
},
gitmem_session_id: {
type: "string",
description: "Same as session_id — prefer this one. Some MCP clients and proxies strip a parameter named session_id, so the close would not know which session you mean. If both are given and differ, the close is refused.",
},
close_type: {
type: "string",
enum: ["standard", "quick", "autonomous"],
Expand All @@ -205,8 +209,12 @@ export const TOOLS = [
type: "number",
description: "End-to-end ceremony duration from agent perspective (in milliseconds)",
},
reclose: {
type: "boolean",
description: "Replace the close of a session that is already closed. Without it, closing a closed session is refused.",
},
},
required: ["session_id", "close_type"],
required: ["close_type"],
},
},
{
Expand Down
Loading
Loading