-
Notifications
You must be signed in to change notification settings - Fork 0
feat(mutils): Add session-id persistence across Auto Compact #19
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
Changes from all commits
f1a5ef0
f5f474a
4ffb948
3275857
d5f0c2c
a3a3504
01dd83e
3cf1d9c
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,123 @@ | ||
| #!/usr/bin/env bun | ||
| import { existsSync } from "node:fs"; | ||
| import path from "node:path"; | ||
| import { | ||
| findGitRoot, | ||
| HookLogger, | ||
| MutilsDB, | ||
| wrapRun, | ||
| } from "@r_masseater/cc-plugin-lib"; | ||
| import { defineHook, runHook } from "cc-hooks-ts"; | ||
|
|
||
| using logger = HookLogger.fromFile(import.meta.filename); | ||
|
|
||
| type WorkspaceMapping = { | ||
| id?: number; | ||
| workspace_id: string; | ||
| feature_name: string; | ||
| created_at: string; | ||
| }; | ||
|
|
||
| const WORKSPACE_MAPPINGS_SCHEMA = { | ||
| columns: ` | ||
| id INTEGER PRIMARY KEY AUTOINCREMENT, | ||
| workspace_id TEXT NOT NULL UNIQUE, | ||
| feature_name TEXT NOT NULL, | ||
| created_at TEXT NOT NULL | ||
| `, | ||
| indexes: [ | ||
| "CREATE UNIQUE INDEX IF NOT EXISTS idx_workspace_mappings_workspace_id ON workspace_mappings(workspace_id)", | ||
| ], | ||
| } as const; | ||
|
|
||
| /** | ||
| * Get the latest workspace mapping from DB | ||
| */ | ||
| function getLatestWorkspaceMapping(cwd: string): WorkspaceMapping | null { | ||
| try { | ||
| using db = MutilsDB.open(cwd); | ||
| const table = db.table<WorkspaceMapping>( | ||
| "workspace_mappings", | ||
| WORKSPACE_MAPPINGS_SCHEMA, | ||
| ); | ||
| // Get the most recent entry | ||
| const all = table.findAll("1=1 ORDER BY id DESC LIMIT 1", []); | ||
| return all[0] ?? null; | ||
| } catch (error) { | ||
| logger.error(`Failed to get workspace mapping: ${error}`); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| const hook = defineHook({ | ||
| trigger: { | ||
| SessionStart: true, | ||
| }, | ||
| run: wrapRun(logger, (context) => { | ||
| const cwd = process.cwd(); | ||
| const eventType = context.input.hook_event_name; | ||
|
|
||
| // Handle SessionStart - restore workspace-id after compact or resume | ||
| if (eventType === "SessionStart") { | ||
| // Check git repository and .agents directory | ||
| const gitRoot = findGitRoot(cwd); | ||
| if (gitRoot === null) { | ||
| logger.debug("Skipping: not a git repository"); | ||
| return context.success({}); | ||
| } | ||
|
|
||
| const agentsDir = path.join(gitRoot, ".agents"); | ||
| if (!existsSync(agentsDir)) { | ||
| logger.debug("Skipping: .agents directory does not exist"); | ||
| return context.success({}); | ||
| } | ||
|
|
||
| const source = | ||
| "source" in context.input && typeof context.input.source === "string" | ||
| ? context.input.source | ||
| : "startup"; | ||
|
|
||
| logger.debug(`SessionStart source: ${source}`); | ||
|
|
||
| // Only restore on compact or resume | ||
| if (source !== "compact" && source !== "resume") { | ||
| logger.debug("Not a compact/resume session, skipping"); | ||
| return context.success({}); | ||
| } | ||
|
|
||
| const mapping = getLatestWorkspaceMapping(gitRoot); | ||
|
|
||
| if (!mapping) { | ||
| logger.debug("No workspace mapping found"); | ||
| return context.success({}); | ||
| } | ||
|
|
||
| logger.info(`Restored workspace-id: ${mapping.workspace_id}`); | ||
|
|
||
| const sourceDescription = | ||
| source === "compact" ? "Auto Compact" : "session resume"; | ||
|
|
||
| // Provide restored workspace-id to Claude | ||
| return context.json({ | ||
| event: "SessionStart", | ||
| output: { | ||
| hookSpecificOutput: { | ||
| hookEventName: "SessionStart", | ||
| additionalContext: `Workspace ID Restored: ${mapping.workspace_id} | ||
|
|
||
| The previous workspace-id has been restored after ${sourceDescription}. Continue using this workspace-id for your work. | ||
|
|
||
| Workspace directory: .agents/workspaces/${mapping.workspace_id}/`, | ||
| }, | ||
| suppressOutput: true, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| return context.success({}); | ||
| }), | ||
| }); | ||
|
|
||
| if (import.meta.main) { | ||
| await runHook(hook); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| import * as fs from "node:fs"; | ||
| import * as os from "node:os"; | ||
| import * as path from "node:path"; | ||
| import { parseWorkspaceId } from "../workspace-id/patterns.js"; | ||
|
|
||
| // --- Constants --- | ||
|
|
||
|
|
@@ -446,6 +447,51 @@ function detectSubagents( | |
| return fs.existsSync(subagentDir); | ||
| } | ||
|
|
||
| // --- Session ID extraction from .agents/sessions/ --- | ||
|
|
||
| interface UserSessionId { | ||
| fullId: string; | ||
| featureName: string; | ||
| } | ||
|
|
||
| function extractUserSessionId(projectPath: string): UserSessionId | null { | ||
| const workspacesDir = path.join(projectPath, ".agents", "workspaces"); | ||
| if (!fs.existsSync(workspacesDir)) { | ||
| return null; | ||
| } | ||
|
|
||
| // Get the most recently modified workspace directory | ||
| const entries = fs | ||
| .readdirSync(workspacesDir, { withFileTypes: true }) | ||
| .filter((d) => d.isDirectory()) | ||
| .map((d) => { | ||
| const fullPath = path.join(workspacesDir, d.name); | ||
| const stat = fs.statSync(fullPath); | ||
| return { name: d.name, mtime: stat.mtimeMs }; | ||
| }) | ||
| .sort((a, b) => b.mtime - a.mtime); | ||
|
|
||
| if (entries.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| const workspaceId = entries[0]?.name; | ||
| if (!workspaceId) { | ||
| return null; | ||
| } | ||
|
|
||
| // Validate workspace-id format using shared pattern | ||
| const parsed = parseWorkspaceId(workspaceId); | ||
| if (!parsed) { | ||
| return null; | ||
| } | ||
|
|
||
| return { | ||
| fullId: workspaceId, | ||
| featureName: parsed.featureName, | ||
| }; | ||
| } | ||
|
Comment on lines
+457
to
+493
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. Avoid using “latest modified workspace directory” as the session mapping source. Selecting 🤖 Prompt for AI Agents |
||
|
|
||
| // --- Plan file extraction --- | ||
|
|
||
| function isPlanFileToolUse( | ||
|
|
@@ -495,6 +541,7 @@ function formatMarkdown( | |
| todos: PendingTodo[], | ||
| hasSubagents: boolean, | ||
| planFiles: string[], | ||
| userSessionId: UserSessionId | null, | ||
| ): string { | ||
| const lines: string[] = []; | ||
|
|
||
|
|
@@ -529,6 +576,26 @@ function formatMarkdown( | |
| lines.push(summaryText); | ||
| lines.push(""); | ||
|
|
||
| // User Session ID (for handoff) | ||
| lines.push("## User Session ID"); | ||
| lines.push(""); | ||
| if (userSessionId) { | ||
| lines.push(`**Session ID**: \`${userSessionId.fullId}\``); | ||
| lines.push(""); | ||
| lines.push(`**Feature Name**: ${userSessionId.featureName}`); | ||
| lines.push(""); | ||
| lines.push( | ||
| "To continue using this session-id, create the session directory:", | ||
| ); | ||
| lines.push(""); | ||
| lines.push("```bash"); | ||
| lines.push(`mkdir -p .agents/sessions/${userSessionId.fullId}`); | ||
| lines.push("```"); | ||
| } else { | ||
| lines.push("No user session-id found in .agents/sessions/"); | ||
| } | ||
| lines.push(""); | ||
|
|
||
| // First Prompt | ||
| lines.push("## First Prompt"); | ||
| lines.push(""); | ||
|
|
@@ -617,6 +684,11 @@ async function main(): Promise<void> { | |
| ); | ||
| const planFiles = extractPlanFilePaths(sessionInfo.jsonlPath); | ||
|
|
||
| // Extract user session-id for handoff | ||
| const userSessionId = sessionInfo.projectPath | ||
| ? extractUserSessionId(sessionInfo.projectPath) | ||
| : null; | ||
|
|
||
| const markdown = formatMarkdown( | ||
| sessionInfo, | ||
| messages, | ||
|
|
@@ -625,6 +697,7 @@ async function main(): Promise<void> { | |
| todos, | ||
| hasSubagents, | ||
| planFiles, | ||
| userSessionId, | ||
| ); | ||
|
|
||
| console.log(markdown); | ||
|
|
||
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Complete the session→workspace rename in handoff output and instructions.
Line 580/583/588/595 and Line 592 still refer to
session-idand.agents/sessions/..., which conflicts with the new workspace model and points users to the wrong directory.🔧 Suggested fix
Also applies to: 579-596
🤖 Prompt for AI Agents