Skip to content

feat(mutils): Add session-id persistence across Auto Compact#19

Closed
masseater wants to merge 8 commits intomasterfrom
feature/session-id-persistence
Closed

feat(mutils): Add session-id persistence across Auto Compact#19
masseater wants to merge 8 commits intomasterfrom
feature/session-id-persistence

Conversation

@masseater
Copy link
Owner

@masseater masseater commented Mar 2, 2026

Summary

  • Add session-id-persist.ts hook to save/restore session-id across Auto Compact and resume
  • Add shared patterns.ts for regex pattern reuse across generate.ts and ccs-handoff.ts
  • Update ccs-handoff.ts to show session-id for handoff scenarios
  • Update SKILL.md with persistence documentation

Test plan

  • Run generate.ts to create a session-id
  • Verify mapping is saved in .agents/mutils/mutils.db
  • Run /compact and verify session-id is restored via additionalContext
  • Test ccs-handoff shows session-id in output

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added workspace-id management system with persistence capabilities across Claude sessions.
    • Introduced TanStack Query plugin with comprehensive best practices documentation.
  • Documentation

    • Added TanStack Query reference guides covering error handling, mutations, query keys, render optimization, testing, and data transformations.
    • Updated workspace-id documentation with generation and integration guidance.
  • Chores

    • Updated mutils plugin version to 0.13.0.
    • Migrated project terminology from session-id to workspace-id throughout configuration and rules.

- Add session-id-persist.ts hook to save/restore session-id
- Add shared patterns.ts for regex pattern reuse
- Update generate.ts to use shared patterns
- Update ccs-handoff.ts to show session-id for handoff
- Update SKILL.md with persistence documentation

The hook captures session-id generation via PostToolUse[Bash] and
restores it on SessionStart when source is "compact" or "resume".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces workspace-id management capabilities to replace session-id terminology, adds a TanStack Query plugin with comprehensive documentation, enhances the CCS handoff skill with user session ID extraction, and implements workspace-id persistence via SQLite. Version bumped to 0.13.0 for mutils plugin.

Changes

Cohort / File(s) Summary
Plugin version management
.claude-plugin/marketplace.json, plugins/mutils/plugin.json
Updated mutils plugin version from 0.12.1 to 0.13.0.
Workspace-ID infrastructure
plugins/mutils/skills/workspace-id/patterns.ts, plugins/mutils/skills/workspace-id/generate.ts, plugins/mutils/hooks/workspace-id-persist.ts
New workspace-id generation and persistence layer: patterns module exports regex validators and parsers for workspace-id format (yyyymmdd-HHmm-featureName); generate.ts creates workspace IDs and persists mappings in SQLite via MutilsDB; persist hook restores workspace context from DB on SessionStart.
Workspace-ID documentation and hook config
plugins/mutils/skills/workspace-id/SKILL.md, plugins/mutils/hooks/hooks.json, .claude/rules/ai-generated/project-context.md
Replaces all session-id terminology with workspace-id across documentation and rules; adds workspace-id-persist.ts hook to SessionStart lifecycle; updates directory structure references from .agents/sessions/ to .agents/workspaces/.
CCS handoff skill enhancement
plugins/mutils/skills/ccs-handoff/ccs-handoff.ts
Adds UserSessionId type and extractUserSessionId() helper to extract workspace ID information from .agents/workspaces/; extends formatMarkdown() to render user session ID details in handoff output.
TanStack Query plugin
plugins/tanstack-query/plugin.json, plugins/tanstack-query/skills/tanstack-query/SKILL.md
New TanStack Query plugin manifest and comprehensive skill documentation covering core concepts, query keys, mutations, data transformations, and best practices.
TanStack Query reference documentation
plugins/tanstack-query/skills/tanstack-query/references/error-handling.md, mutations.md, query-keys.md, render-optimization.md, testing.md, transformations.md
Extensive reference guides for TanStack Query covering error handling patterns, mutation lifecycle, query key organization, render optimization strategies, testing setups, and data transformation approaches with code examples and best practices.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as generate.ts
    participant Validation as patterns.ts
    participant FileSystem as File System
    participant DB as MutilsDB
    
    User->>CLI: provide feature-name
    CLI->>Validation: isValidFeatureName()
    alt Invalid feature-name
        Validation-->>CLI: false
        CLI-->>User: error & usage
    else Valid feature-name
        Validation-->>CLI: true
        CLI->>CLI: compute workspaceId<br/>(yyyymmdd-HHmm-featureName)
        CLI-->>User: output workspaceId to stdout
        CLI->>FileSystem: locate git root
        FileSystem-->>CLI: git root path
        CLI->>FileSystem: check .agents directory
        alt .agents exists
            CLI->>DB: open MutilsDB
            DB-->>CLI: connection
            CLI->>DB: ensure workspace_mappings table
            DB-->>CLI: table ready
            CLI->>DB: upsert mapping<br/>(workspace_id, feature_name, created_at)
            DB-->>CLI: record persisted
        else .agents not found
            CLI->>CLI: silently skip DB write
        end
    end
Loading
sequenceDiagram
    participant Session as Claude Session
    participant Hook as workspace-id-persist.ts
    participant FileSystem as File System
    participant DB as MutilsDB
    participant Context as Additional Context
    
    Session->>Hook: SessionStart event
    Hook->>FileSystem: verify git root
    alt Git root not found
        FileSystem-->>Hook: error
        Hook-->>Session: exit gracefully
    else Git root found
        Hook->>FileSystem: check .agents directory
        alt .agents not found
            FileSystem-->>Hook: not found
            Hook-->>Session: exit gracefully
        else .agents exists
            Hook->>Hook: determine session source<br/>(default: startup)
            alt Not compact/resume session
                Hook-->>Session: exit gracefully
            else Compact or resume
                Hook->>DB: getLatestWorkspaceMapping()
                alt Mapping found
                    DB-->>Hook: workspace_id, feature_name
                    Hook->>Context: format restoration context
                    Context-->>Hook: detailed message with path
                    Hook-->>Session: return JSON with<br/>additionalContext
                    Session->>Session: restore workspace
                else No mapping found
                    DB-->>Hook: null
                    Hook-->>Session: exit gracefully
                end
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Workspace IDs hop through time,
Persisted in databases so fine,
Query keys dance in TanStack's gleam,
Sessions restored—a developer's dream!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title references 'session-id' persistence, but the changeset consistently uses 'workspace-id' terminology throughout all modified and new files, representing a systematic terminology shift. Update the PR title to 'feat(mutils): Add workspace-id persistence across Auto Compact' to align with the actual changes in the changeset.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 91.67% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/session-id-persistence

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
plugins/mutils/hooks/session-id-persist.ts (1)

140-185: Consider adding type safety for tool_response.

The tool_response is cast to string (line 144), but the cc-hooks-ts type is generic. If tool_response is unexpectedly not a string (e.g., structured output), this could cause runtime issues.

The current code is safe in practice since parseSessionId handles non-matching strings gracefully by returning null, but adding a type check would be more defensive.

♻️ Optional: Add explicit type check
 		if (eventType === "PostToolUse" && context.input.tool_name === "Bash") {
 			// Type assertion for Bash tool input/output
 			const toolInput = context.input.tool_input as { command?: string };
-			const toolResponse = context.input.tool_response as string;
+			const toolResponse = context.input.tool_response;
+			if (typeof toolResponse !== "string") {
+				logger.debug("tool_response is not a string, skipping");
+				return context.success({});
+			}
 			const command = toolInput.command ?? "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/hooks/session-id-persist.ts` around lines 140 - 185, The code
currently casts context.input.tool_response to string (toolResponse) before
parsing; add a runtime type check to avoid runtime errors if tool_response is
non-string: check typeof context.input.tool_response === "string" (or if it's an
object, serialize with JSON.stringify) before assigning toolResponse, log a
warning via logger.warn and return context.success({}) if it's neither string
nor serializable, then use the validated/serialized toolResponse with
parseSessionId, saveSessionMapping, and the existing return payload; reference
the symbols context.input.tool_response, toolResponse, parseSessionId,
isGenerateTsCommand, and saveSessionMapping to locate and update the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@plugins/mutils/hooks/session-id-persist.ts`:
- Around line 140-185: The code currently casts context.input.tool_response to
string (toolResponse) before parsing; add a runtime type check to avoid runtime
errors if tool_response is non-string: check typeof context.input.tool_response
=== "string" (or if it's an object, serialize with JSON.stringify) before
assigning toolResponse, log a warning via logger.warn and return
context.success({}) if it's neither string nor serializable, then use the
validated/serialized toolResponse with parseSessionId, saveSessionMapping, and
the existing return payload; reference the symbols context.input.tool_response,
toolResponse, parseSessionId, isGenerateTsCommand, and saveSessionMapping to
locate and update the logic.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7a4a2ac and f1a5ef0.

📒 Files selected for processing (8)
  • .claude-plugin/marketplace.json
  • plugins/mutils/hooks/hooks.json
  • plugins/mutils/hooks/session-id-persist.ts
  • plugins/mutils/plugin.json
  • plugins/mutils/skills/ccs-handoff/ccs-handoff.ts
  • plugins/mutils/skills/session-id/SKILL.md
  • plugins/mutils/skills/session-id/generate.ts
  • plugins/mutils/skills/session-id/patterns.ts

@masseater
Copy link
Owner Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

The implementation follows the project's conventions:

  • Uses MutilsDB for database operations with proper resource management (using syntax)
  • Follows the hook patterns established in other mutils hooks
  • Properly handles error cases with try-catch and graceful degradation
  • Uses shared regex patterns from patterns.ts for consistency

CI checks passed:

  • mutils plugin: ✅
  • cc-plugin-lib: ✅
  • sdd-webapp: ✅
  • sdd plugin: ✅

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- Move command pattern check before git/.agents checks in PostToolUse
  to avoid unnecessary filesystem operations for non-matching commands
- Remove technical implementation details from SKILL.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
plugins/mutils/hooks/session-id-persist.ts (1)

139-150: Extract duplicated git/.agents guard into a shared helper.

The repository eligibility check is duplicated in two branches. A small helper (e.g., resolveEligibleGitRoot) would reduce divergence risk and make future condition changes safer.

Also applies to: 188-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/hooks/session-id-persist.ts` around lines 139 - 150, Extract
the duplicated git/.agents guard into a shared helper (e.g.,
resolveEligibleGitRoot) that encapsulates the logic using findGitRoot(cwd) and
existsSync(path.join(gitRoot, ".agents")); replace both occurrences (the block
around findGitRoot/agentsDir and the other block at lines ~188-199) by calling
resolveEligibleGitRoot(cwd) and returning context.success({}) when it returns
null or throwing/handling as the original blocks did; ensure the helper returns
the gitRoot string when eligible and null otherwise and update any local
variables (agentsDir, gitRoot) to use the helper's result so behavior remains
identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/mutils/hooks/session-id-persist.ts`:
- Around line 130-133: Normalize and type-check tool_response before
parsing/storing session-id: ensure the variable context.input.tool_response is
checked for null/undefined and is coerced to a string (e.g., String(...) or via
typeof check), then trimmed of whitespace/newlines before any parsing or
assignment; update usages around the variables tool_input, tool_response and
command (the code that reads tool_input.command and later reads tool_response at
the session-id persistence/restoration code paths) to use the normalized/trimmed
value and guard against non-string payloads (skip persistence or log/handle
invalid types) so trailing newlines or non-string outputs cannot produce or
store an invalid session-id.

---

Nitpick comments:
In `@plugins/mutils/hooks/session-id-persist.ts`:
- Around line 139-150: Extract the duplicated git/.agents guard into a shared
helper (e.g., resolveEligibleGitRoot) that encapsulates the logic using
findGitRoot(cwd) and existsSync(path.join(gitRoot, ".agents")); replace both
occurrences (the block around findGitRoot/agentsDir and the other block at lines
~188-199) by calling resolveEligibleGitRoot(cwd) and returning
context.success({}) when it returns null or throwing/handling as the original
blocks did; ensure the helper returns the gitRoot string when eligible and null
otherwise and update any local variables (agentsDir, gitRoot) to use the
helper's result so behavior remains identical.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1a5ef0 and f5f474a.

📒 Files selected for processing (2)
  • plugins/mutils/hooks/session-id-persist.ts
  • plugins/mutils/skills/session-id/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/mutils/skills/session-id/SKILL.md

Comment on lines +130 to +133
const toolInput = context.input.tool_input as { command?: string };
const toolResponse = context.input.tool_response as string;
const command = toolInput.command ?? "";

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize and type-check tool_response before parsing/storing session-id.

Line 131 force-casts tool_response and Lines 156/169 use it raw. If Bash output includes trailing newlines or non-string payloads, parsing/persistence can fail or store an invalid session-id value for restoration.

🔧 Proposed fix
-			const toolResponse = context.input.tool_response as string;
+			const toolResponseRaw = context.input.tool_response;
+			if (typeof toolResponseRaw !== "string") {
+				logger.warn("Skipping: Bash tool_response is not a string");
+				return context.success({});
+			}
+			const toolResponse = toolResponseRaw.trim();
+			if (toolResponse.length === 0) {
+				logger.warn("Skipping: Bash tool_response is empty");
+				return context.success({});
+			}

Also applies to: 155-171, 179-180

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/hooks/session-id-persist.ts` around lines 130 - 133, Normalize
and type-check tool_response before parsing/storing session-id: ensure the
variable context.input.tool_response is checked for null/undefined and is
coerced to a string (e.g., String(...) or via typeof check), then trimmed of
whitespace/newlines before any parsing or assignment; update usages around the
variables tool_input, tool_response and command (the code that reads
tool_input.command and later reads tool_response at the session-id
persistence/restoration code paths) to use the normalized/trimmed value and
guard against non-string payloads (skip persistence or log/handle invalid types)
so trailing newlines or non-string outputs cannot produce or store an invalid
session-id.

@masseater
Copy link
Owner Author

Code review

Found 2 issues:

  1. upsert() without id field causes auto-increment on every update (INSERT OR REPLACE deletes and re-inserts with new id)

// Check if record exists to preserve created_at
const existing = table.findOne("claude_session_id = ?", [claudeSessionId]);
const createdAt = existing?.created_at ?? now;
table.upsert({
claude_session_id: claudeSessionId,
user_session_id: userSessionId,
feature_name: featureName,
created_at: createdAt,
updated_at: now,
});

  1. AGENTS.md missing PostToolUse in hooks list (documentation drift)

| hooks | SessionStart, PreToolUse, Stop |

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- Preserve id in upsert to prevent auto-increment on update
- Add PostToolUse to AGENTS.md hooks list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/tanstack-query/skills/tanstack-query/references/error-handling.md`:
- Around line 94-95: Update the documentation examples to reflect v5 defaults
and make error access type-safe: change any statement that claims the default
error type is unknown to state it is Error (unless overridden), correct the
description of retry behavior to note v5 will retry 4xx if the queryFn throws
and that status-based skipping requires a custom retry function (refer to the
retry option), and modify the example using throwOnError / error.status so it
either demonstrates a custom error class (e.g., ApiError with a status property)
or shows type narrowing (e.g., axios.isAxiosError or instanceof checks) before
accessing error.status to avoid unsafe property access.

In `@plugins/tanstack-query/skills/tanstack-query/references/testing.md`:
- Around line 373-377: The import statement incorrectly includes a non-existent
named export "debug" from '@testing-library/react'; remove "debug" from the
import (leave "screen") in the import line that currently reads "import {
screen, debug } ..." and ensure any calls use screen.debug() (as already
present) and no other references to the removed symbol remain in the file.

In `@plugins/tanstack-query/skills/tanstack-query/references/transformations.md`:
- Around line 123-124: Update the sentence about select to clarify its actual
re-run semantics: explain that TanStack Query v5's select callback only
re-executes when the query's data changes or when the selector function
reference changes, and note that inline selector functions appear to run every
render only because their reference changes each render; recommend using stable
selectors (extracting the selector function or wrapping it in useCallback) to
avoid unnecessary re-runs. Reference the select callback and
useCallback/selector extraction as the recommended fixes.

In `@plugins/tanstack-query/skills/tanstack-query/SKILL.md`:
- Around line 185-189: The optimistic-update onMutate implementation can crash
when the ['todos'] cache is empty because setQueryData's updater receives
undefined; update the updater passed to queryClient.setQueryData in onMutate
(the function currently using old) to handle undefined safely (e.g., treat old
as an empty array) and preserve the previous value returned from
queryClient.getQueryData(['todos']) into previousTodos so rollback still works;
modify the updater used in queryClient.setQueryData to default old to an empty
array before spreading and adding newTodo.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5f474a and 4ffb948.

📒 Files selected for processing (10)
  • plugins/mutils/AGENTS.md
  • plugins/mutils/hooks/session-id-persist.ts
  • plugins/tanstack-query/plugin.json
  • plugins/tanstack-query/skills/tanstack-query/SKILL.md
  • plugins/tanstack-query/skills/tanstack-query/references/error-handling.md
  • plugins/tanstack-query/skills/tanstack-query/references/mutations.md
  • plugins/tanstack-query/skills/tanstack-query/references/query-keys.md
  • plugins/tanstack-query/skills/tanstack-query/references/render-optimization.md
  • plugins/tanstack-query/skills/tanstack-query/references/testing.md
  • plugins/tanstack-query/skills/tanstack-query/references/transformations.md
✅ Files skipped from review due to trivial changes (2)
  • plugins/tanstack-query/skills/tanstack-query/references/query-keys.md
  • plugins/tanstack-query/skills/tanstack-query/references/mutations.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins/mutils/hooks/session-id-persist.ts

Comment on lines +94 to +95
throwOnError: (error) => error.status >= 500, // Only throw server errors
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

TanStack Query v5 useQuery default error type TypeScript unknown

💡 Result:

In TanStack Query v5, useQuery’s default error type is Error (not unknown). If you’re seeing unknown, it’s usually because you (or a dependency) globally set the default error type to unknown via module augmentation. [1][2]

1) If you want the default error type to be Error again

Remove/adjust any augmentation like this:

import '@tanstack/react-query'

declare module '@tanstack/react-query' {
  interface Register {
    defaultError: unknown
  }
}

That exact pattern is documented as the way to force unknown (so call sites must narrow). [1]

2) If you do want unknown, narrow at the call site

const { error, isError } = useQuery({ queryKey: ['x'], queryFn })

if (isError && error instanceof Error) {
  console.log(error.message)
}

3) If you want a custom global error type (e.g. AxiosError)

import '@tanstack/react-query'
import type { AxiosError } from 'axios'

declare module '@tanstack/react-query' {
  interface Register {
    defaultError: AxiosError
  }
}

This is the recommended v5 approach to avoid having to pass TError generics everywhere. [1]

Sources: TanStack Query v5 TypeScript docs (default error type + global Register.defaultError) [1], v5 migration guide (error default changed to Error) [2].


🌐 Web query:

TanStack Query v5 default retry logic 4xx HTTP status code behavior

💡 Result:

In TanStack Query v5, the default retry behavior does not special‑case HTTP status codes (including 4xx). A query is retried purely based on whether your queryFn throws and whether the retry policy allows another attempt. By default, it retries 3 times (client-side; server-side rendering defaults to 0). [1], [2]

So for 4xx responses:

  • If your queryFn (or HTTP client like axios) throws for a 4xx, TanStack Query will retry it up to 3 times by default. [1]
  • TanStack Query intentionally doesn’t assume 4xx should never be retried, because some APIs/systems (e.g., eventual consistency) can legitimately return a transient 404. You’re expected to encode your own “don’t retry 4xx” rule if you want it. [3]

Typical “don’t retry 4xx” setup:

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: (failureCount, error: any) => {
        const status = error?.response?.status // axios-style
        if (status >= 400 && status < 500) return false
        return failureCount < 3
      },
    },
  },
})

🌐 Web query:

TanStack Query v5 throwOnError error.status TypeScript safe handling

💡 Result:

In TanStack Query v5, error is typed as Error by default (not unknown), so error.status is not type-safe unless your queryFn throws an error type that actually has status (or you narrow it first). [1][2]

1) Throw a typed error that includes status (recommended)

export class ApiError extends Error {
  constructor(
    public status: number,
    message: string,
    public body?: unknown,
  ) {
    super(message)
    this.name = 'ApiError'
  }
}

async function fetchJson<T>(url: string): Promise<T> {
  const res = await fetch(url)
  const body = await res.json().catch(() => undefined)
  if (!res.ok) throw new ApiError(res.status, `HTTP ${res.status}`, body)
  return body as T
}

// Usage
useQuery({
  queryKey: ['user'],
  queryFn: () => fetchJson<User>('/api/user'),
  throwOnError: (err) => err instanceof ApiError && err.status >= 500,
})

throwOnError can be boolean or a function (error, query) => boolean, so you can decide “throw to the nearest ErrorBoundary” only for certain statuses. [3]

2) If you use Axios: narrow before reading status

TanStack recommends type narrowing for specific error subclasses (like AxiosError). [2]

import axios from 'axios'

throwOnError: (err) =>
  axios.isAxiosError(err) && (err.response?.status ?? 0) === 401

3) (Optional) Make this globally type-safe

If you want compile-time forcing of narrowing everywhere, set defaultError: unknown via module augmentation; or set it to your ApiError if all your queryFns throw that type. [2]

import '@tanstack/react-query'

declare module '@tanstack/react-query' {
  interface Register {
    defaultError: unknown
  }
}

Key point: throwOnError doesn’t “add” a status; it only controls whether the caught error is thrown to an Error Boundary vs returned in query state—so you must model/narrow the error type yourself to safely access status. [3][1][2]

Sources: [1] Migrating to v5 (throwOnError rename + default Error) (tanstack.com), [2] TypeScript docs (error typing + narrowing + Register) (tanstack.com), [3] useQuery option type for throwOnError (tanstack.com)

Citations:


🏁 Script executed:

cat -n plugins/tanstack-query/skills/tanstack-query/references/error-handling.md | sed -n '90,100p;125,135p;175,190p'

Repository: masseater/claude-code-plugin

Length of output: 975


Fix inaccurate v5 behavior defaults in error handling documentation.

Line 128 incorrectly states errors are unknown by default; in v5, the default error type is Error (unless globally overridden via module augmentation).

Lines 180–183 misrepresent the default retry behavior. v5 does not skip 4xx errors by default—it retries them if your queryFn throws. Status-based retry logic requires an explicit custom retry function.

Line 94's error.status access is not type-safe with the default Error type. The example needs either a custom error class with a status property or explicit type narrowing to safely access status codes.

Suggested corrections

At line 128:

-By default, errors are `unknown`:
+By default in v5, `error` is typed as `Error`:

At lines 180–183:

 - Retries 3 times by default
 - Exponential backoff
-- No retry on 4xx errors (client errors)
+- Status-based retry logic requires a custom `retry` function; by default, 4xx responses are retried if thrown

At line 94, add context showing proper error handling (e.g., custom ApiError class or type narrowing like axios.isAxiosError()).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/tanstack-query/skills/tanstack-query/references/error-handling.md`
around lines 94 - 95, Update the documentation examples to reflect v5 defaults
and make error access type-safe: change any statement that claims the default
error type is unknown to state it is Error (unless overridden), correct the
description of retry behavior to note v5 will retry 4xx if the queryFn throws
and that status-based skipping requires a custom retry function (refer to the
retry option), and modify the example using throwOnError / error.status so it
either demonstrates a custom error class (e.g., ApiError with a status property)
or shows type narrowing (e.g., axios.isAxiosError or instanceof checks) before
accessing error.status to avoid unsafe property access.

Comment on lines +373 to +377
import { screen, debug } from '@testing-library/react'

renderWithQueryClient(<TodoList />)
screen.debug() // Print DOM
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

@testing-library/react debug function named export vs screen.debug

💡 Result:

  • There is no debug named export from @testing-library/react. In React Testing Library, debug is a method on the object returned by render() (the “render result”). [2]
  • screen.debug() comes from DOM Testing Library (re-exported by React Testing Library) and is a convenience method that logs prettyDOM() for document.body by default, or for a specific element / array of elements you pass in. [1]

Practical differences

  • render(...).debug() / {debug} = render(...)

    • Logs prettyDOM(baseElement) for that render call (where baseElement defaults to document.body, but can be customized via render options). [2]
    • Useful if you have multiple renders/containers and want the debug output tied to a specific render result.
  • screen.debug()

    • Logs what’s in document.body (or the element(s) you pass). [1]
    • Recommended by the RTL docs over render’s debug. [2]

Typical usage

import { render, screen } from '@testing-library/react'

render(<App />)
screen.debug() // recommended

Remove unused debug import from Line 373.

@testing-library/react does not export a named debug function. The debug method exists only on the render result object. The code correctly uses screen.debug() (the recommended approach), so the named import should be removed.

Suggested fix
-import { screen, debug } from '@testing-library/react'
+import { screen } from '@testing-library/react'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/tanstack-query/skills/tanstack-query/references/testing.md` around
lines 373 - 377, The import statement incorrectly includes a non-existent named
export "debug" from '@testing-library/react'; remove "debug" from the import
(leave "screen") in the import line that currently reads "import { screen, debug
} ..." and ensure any calls use screen.debug() (as already present) and no other
references to the removed symbol remain in the file.

Comment on lines +123 to +124
The select function runs on every render. Memoize if expensive:

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

TanStack Query v5 useQuery select function execution behavior memoization

💡 Result:

In TanStack Query v5, useQuery({ select }) behaves like a memoized per-observer selector:

  • When select executes: it re-runs only if (a) the query’s cached data changed or (b) the select function reference changed (e.g., you pass an inline arrow each render). To avoid re-running due to (b), wrap it in useCallback or use a stable function reference. [1]
  • It is observer-specific: select is intentionally applied per useQuery observer so different components can derive different shapes from the same cached query data; therefore it must be pure / side-effect free (don’t put setState, toasts, etc. in it). [2]
  • Structural sharing still helps: React Query’s structural sharing is applied to the result of select as well, so if the selected output is effectively unchanged, React Query can keep references stable and avoid unnecessary updates. [3]
  • Common pitfall: an inline select can make it look like “select runs every render”; that’s because its function identity changes every render unless memoized. [1][4]

Sources: Render Optimizations (memoization rules + examples) [1], maintainer explanation of per-observer/pure select [2], maintainer confirmation that structural sharing applies to select output [3], discussion noting inline select can run every render unless memoized [4].

References

  1. https://tanstack.com/query/latest/docs/framework/react/guides/render-optimizations
  2. v5 Roadmap 🗺 TanStack/query#4252
  3. select: does structural sharing apply to the result of select? (stable between renders if shape doesn't change, even if not memoized). TanStack/query#7280
  4. Re-rendering Issue with Select function When Using TanStack Query and Formik TanStack/query#8557

🌐 Web query:

TanStack Query select function runs on render vs data changes

💡 Result:

In TanStack Query (React Query), select is memoized per QueryObserver and will only re-run when:

  1. the fetched/cached data changes, or
  2. the select function reference changes (referential identity). [1][3]

So:

  • If you write select: (data) => ... inline, it’s a new function every render, so select will run on every render even if the underlying data didn’t change. [1][3]
  • If you pass a stable function (extract it or wrap in useCallback), select won’t re-run on re-renders unless data changes. [1]
  • select runs per component instance / per useQuery call (per observer). Multiple components using the same query will each run their own select. [3]

Fix (stable selector):

const selectTodoCount = (data: Todo[]) => data.length

useQuery({ queryKey: ['todos'], queryFn: fetchTodos, select: selectTodoCount })

or:

const selectTodoCount = useCallback((data: Todo[]) => data.length, [])

[1]

Sources: TanStack Query render optimizations guide (covers select + memoization rules) [1], TkDodo deep-dive on selectors (why inline selectors re-run; per-observer caching) [3], maintainer confirmation on GitHub discussions [2].

References: [1] (tanstack.com) [2] (github.com) [3] (tkdodo.eu)

Citations:


Clarify select execution semantics in Line 123.

The original statement "The select function runs on every render" is misleading. TanStack Query v5's select only re-runs when the query data changes or the selector function reference changes. Inline selectors appear to run every render only because they're a new function each render; stable selectors (extracted functions or wrapped in useCallback) won't re-run unless data actually changes. Update the doc to clarify this distinction:

Suggested doc tweak
-The select function runs on every render. Memoize if expensive:
+Inline select functions may run on each render due to function identity changes.
+Use a stable/memoized selector for expensive transforms:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/tanstack-query/skills/tanstack-query/references/transformations.md`
around lines 123 - 124, Update the sentence about select to clarify its actual
re-run semantics: explain that TanStack Query v5's select callback only
re-executes when the query's data changes or when the selector function
reference changes, and note that inline selector functions appear to run every
render only because their reference changes each render; recommend using stable
selectors (extracting the selector function or wrapping it in useCallback) to
avoid unnecessary re-runs. Reference the select callback and
useCallback/selector extraction as the recommended fixes.

Comment on lines +185 to +189
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
return { previousTodos }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "SKILL.md" -path "*tanstack-query*" | head -5

Repository: masseater/claude-code-plugin

Length of output: 127


🌐 Web query:

TanStack Query v5 queryClient.setQueryData updater function oldData undefined when no cache entry

💡 Result:

In TanStack Query v5 this is expected:

  • queryClient.setQueryData(queryKey, updater) calls your updater with the current cached data, so if there’s no cache entry yet, oldData will be undefined. Also, if the query does not exist, it will be created. [1]
  • If you don’t want to create a new cache entry when none exists, bail out by returning undefined when oldData is undefined (v5 explicitly documents this). [1]

Example:

queryClient.setQueryData(['todo', id], (old) => {
  if (!old) return undefined // don't create cache entry
  return { ...old, done: true }
})

If you want to check first, queryClient.getQueryData(queryKey) returns undefined when the query doesn’t exist. [1]

Sources: [1] TanStack Query v5 QueryClient reference (setQueryData / getQueryData).


🏁 Script executed:

cat -n ./plugins/tanstack-query/skills/tanstack-query/SKILL.md | sed -n '180,200p'

Repository: masseater/claude-code-plugin

Length of output: 820


Handle old being undefined in optimistic-update snippet.

Line 188 will crash if the ['todos'] query has no cached value yet. When setQueryData is called with no prior cache entry, the updater receives undefined, and spreading it throws a TypeError. Use a default parameter to safely handle this:

Suggested fix
-    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
+    queryClient.setQueryData(['todos'], (old = []) => [...old, newTodo])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/tanstack-query/skills/tanstack-query/SKILL.md` around lines 185 -
189, The optimistic-update onMutate implementation can crash when the ['todos']
cache is empty because setQueryData's updater receives undefined; update the
updater passed to queryClient.setQueryData in onMutate (the function currently
using old) to handle undefined safely (e.g., treat old as an empty array) and
preserve the previous value returned from queryClient.getQueryData(['todos'])
into previousTodos so rollback still works; modify the updater used in
queryClient.setQueryData to default old to an empty array before spreading and
adding newTodo.

Rename session-id skill to workspace-id to avoid confusion with
Claude Code's internal session_id. This clarifies the purpose:
workspace-id identifies a work context directory, not a session.

Changes:
- Rename skills/session-id/ → skills/workspace-id/
- Rename hooks/session-id-persist.ts → hooks/workspace-id-persist.ts
- Update all references: session-id → workspace-id
- Update directory path: .agents/sessions/ → .agents/workspaces/
- Update documentation and rules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
plugins/mutils/hooks/workspace-id-persist.ts (1)

131-133: ⚠️ Potential issue | 🟠 Major

Normalize and type-check Bash payloads before parsing/persisting workspace-id.

Line 131-133 force-casts both tool_input and tool_response. A non-string or newline-suffixed tool_response can fail parse/persist or store an invalid value.

🔧 Suggested fix
-			// Type assertion for Bash tool input/output
-			const toolInput = context.input.tool_input as { command?: string };
-			const toolResponse = context.input.tool_response as string;
-			const command = toolInput.command ?? "";
+			const toolInputRaw = context.input.tool_input;
+			const command =
+				typeof toolInputRaw === "object" &&
+				toolInputRaw !== null &&
+				"command" in toolInputRaw &&
+				typeof (toolInputRaw as { command?: unknown }).command === "string"
+					? ((toolInputRaw as { command: string }).command ?? "")
+					: "";
+
+			const toolResponseRaw = context.input.tool_response;
+			if (typeof toolResponseRaw !== "string") {
+				logger.warn("Skipping: Bash tool_response is not a string");
+				return context.success({});
+			}
+			const toolResponse = toolResponseRaw.trim();
+			if (toolResponse.length === 0) {
+				logger.warn("Skipping: Bash tool_response is empty");
+				return context.success({});
+			}

Also applies to: 157-171

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/hooks/workspace-id-persist.ts` around lines 131 - 133, The
code currently force-casts context.input.tool_input and
context.input.tool_response into toolInput/toolResponse and uses
toolInput.command (see variables toolInput, toolResponse, command), which can
crash or store invalid values; update the code to first assert types and
normalize strings: check typeof context.input.tool_response === "string" then
trim() to remove trailing newlines (and reject empty strings), ensure
context.input.tool_input is an object and that tool_input.command is a string
before assigning command (fallback to empty string only after those checks), and
apply the same guards/normalization to the parsing/persist logic later (the same
block referenced around the later parse/persist code) so only validated, trimmed
strings are parsed and persisted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/mutils/skills/ccs-handoff/ccs-handoff.ts`:
- Around line 457-493: The extractUserSessionId function currently picks the
most-recent workspace by mtime which can be incorrect; update
extractUserSessionId to first attempt to resolve the workspace id from the
persisted Claude session mapping (the mapping saved by
plugins/mutils/hooks/workspace-id-persist.ts), and only if that lookup returns
null/undefined fall back to the existing mtime-based selection and
parseWorkspaceId validation; keep the existing parseWorkspaceId usage and return
shape (fullId, featureName) unchanged and ensure any IO/lookup errors are
handled the same way as the current existence checks.
- Around line 450-455: The handoff output and instruction text still reference
"session-id" and the .agents/sessions/... path; update all occurrences to the
workspace model (e.g., "workspace-id" and .agents/workspaces/...) and adjust
types/identifiers accordingly—rename the UserSessionId concept to
UserWorkspaceId (or equivalent) and update any usages of fullId/featureName that
construct or display the path so the instructions point to
.agents/workspaces/<workspace-id> instead of .agents/sessions/<session-id>;
ensure the handoff messages generated in ccs-handoff.ts (the functions/variables
that build the handoff output around lines ~579-596) are changed to use
workspace naming consistently.

In `@plugins/mutils/skills/workspace-id/SKILL.md`:
- Around line 40-46: Two unlabeled fenced code blocks in SKILL.md (the directory
tree block starting with ".agents/workspaces/" and the "Workspace ID:
20260302-1430-doc-engine" block) must be labeled to satisfy markdownlint MD040;
update each triple-backtick fence to include a language token such as text
(i.e., change ``` to ```text) for the blocks that contain the directory listing
and the workspace/files listing so the linter recognizes them as code fences.

---

Duplicate comments:
In `@plugins/mutils/hooks/workspace-id-persist.ts`:
- Around line 131-133: The code currently force-casts context.input.tool_input
and context.input.tool_response into toolInput/toolResponse and uses
toolInput.command (see variables toolInput, toolResponse, command), which can
crash or store invalid values; update the code to first assert types and
normalize strings: check typeof context.input.tool_response === "string" then
trim() to remove trailing newlines (and reject empty strings), ensure
context.input.tool_input is an object and that tool_input.command is a string
before assigning command (fallback to empty string only after those checks), and
apply the same guards/normalization to the parsing/persist logic later (the same
block referenced around the later parse/persist code) so only validated, trimmed
strings are parsed and persisted.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4ffb948 and 3275857.

📒 Files selected for processing (7)
  • .claude/rules/ai-generated/project-context.md
  • plugins/mutils/hooks/hooks.json
  • plugins/mutils/hooks/workspace-id-persist.ts
  • plugins/mutils/skills/ccs-handoff/ccs-handoff.ts
  • plugins/mutils/skills/workspace-id/SKILL.md
  • plugins/mutils/skills/workspace-id/generate.ts
  • plugins/mutils/skills/workspace-id/patterns.ts

Comment on lines +450 to +455
// --- Session ID extraction from .agents/sessions/ ---

interface UserSessionId {
fullId: string;
featureName: string;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Complete the session→workspace rename in handoff output and instructions.

Line 580/583/588/595 and Line 592 still refer to session-id and .agents/sessions/..., which conflicts with the new workspace model and points users to the wrong directory.

🔧 Suggested fix
-// --- Session ID extraction from .agents/sessions/ ---
+// --- Workspace ID extraction from .agents/workspaces/ ---
@@
-	// User Session ID (for handoff)
-	lines.push("## User Session ID");
+	// Workspace ID (for handoff)
+	lines.push("## Workspace ID");
@@
-		lines.push(`**Session ID**: \`${userSessionId.fullId}\``);
+		lines.push(`**Workspace ID**: \`${userSessionId.fullId}\``);
@@
-		lines.push(
-			"To continue using this session-id, create the session directory:",
-		);
+		lines.push(
+			"To continue using this workspace-id, create the workspace directory:",
+		);
@@
-		lines.push(`mkdir -p .agents/sessions/${userSessionId.fullId}`);
+		lines.push(`mkdir -p .agents/workspaces/${userSessionId.fullId}`);
@@
-		lines.push("No user session-id found in .agents/sessions/");
+		lines.push("No workspace-id found in .agents/workspaces/");

Also applies to: 579-596

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/skills/ccs-handoff/ccs-handoff.ts` around lines 450 - 455, The
handoff output and instruction text still reference "session-id" and the
.agents/sessions/... path; update all occurrences to the workspace model (e.g.,
"workspace-id" and .agents/workspaces/...) and adjust types/identifiers
accordingly—rename the UserSessionId concept to UserWorkspaceId (or equivalent)
and update any usages of fullId/featureName that construct or display the path
so the instructions point to .agents/workspaces/<workspace-id> instead of
.agents/sessions/<session-id>; ensure the handoff messages generated in
ccs-handoff.ts (the functions/variables that build the handoff output around
lines ~579-596) are changed to use workspace naming consistently.

Comment on lines +457 to +493
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,
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid using “latest modified workspace directory” as the session mapping source.

Selecting entries[0] by mtime can associate the wrong workspace-id when multiple workspaces exist for the same project path. For handoff accuracy, resolve workspace-id by the current Claude session identifier first (the mapping persisted by plugins/mutils/hooks/workspace-id-persist.ts), and only use mtime as an explicit fallback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/skills/ccs-handoff/ccs-handoff.ts` around lines 457 - 493, The
extractUserSessionId function currently picks the most-recent workspace by mtime
which can be incorrect; update extractUserSessionId to first attempt to resolve
the workspace id from the persisted Claude session mapping (the mapping saved by
plugins/mutils/hooks/workspace-id-persist.ts), and only if that lookup returns
null/undefined fall back to the existing mtime-based selection and
parseWorkspaceId validation; keep the existing parseWorkspaceId usage and return
shape (fullId, featureName) unchanged and ensure any IO/lookup errors are
handled the same way as the current existence checks.

masseater and others added 2 commits March 3, 2026 12:41
- Remove PostToolUse hook - no longer needed
- generate.ts now saves directly to DB
- workspace-id-persist.ts only handles SessionStart for restore
- Simpler architecture: just get latest workspace-id on restore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
plugins/mutils/hooks/workspace-id-persist.ts (2)

69-114: Consider validating .agents/workspaces/ exists before advertising the path.

Right now you only check .agents exists. If .agents/workspaces is missing, the restored “Workspace directory: …” line may be misleading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/hooks/workspace-id-persist.ts` around lines 69 - 114, Validate
the actual workspace path before advertising it: after obtaining mapping from
getLatestWorkspaceMapping and before using mapping.workspace_id in the returned
context.json, build the workspaceDir path (using agentsDir or gitRoot +
".agents/workspaces/<mapping.workspace_id>"), check that it exists (existsSync
or equivalent), and if it doesn't exist log/debug a warning and return
context.success({}) (or adjust the message to not claim the directory exists).
Update the message sent in context.json to use the confirmed workspaceDir
variable (or omit the directory line when missing) so mapping.workspace_id is
only advertised when the physical .agents/workspaces/<id> directory is present.

36-49: Make “latest mapping” selection independent of id semantics; improve error logging.

Two concerns here:

  • ORDER BY id DESC makes “latest” depend on how id behaves under upserts/replacements; ordering by created_at DESC, id DESC is more robust.
  • logger.error(\... ${error}`)` loses stack/context; prefer structured logging.

Also, table.findAll("1=1 ORDER BY ... LIMIT 1", []) is a bit opaque unless findAll explicitly supports ORDER/LIMIT in that argument.

Proposed refactor
 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", []);
+		// Get the most recent entry (prefer created_at; id as a tie-breaker)
+		const all = table.findAll(
+			"1=1 ORDER BY created_at DESC, id DESC LIMIT 1",
+			[],
+		);
 		return all[0] ?? null;
 	} catch (error) {
-		logger.error(`Failed to get workspace mapping: ${error}`);
+		logger.error("Failed to get workspace mapping", { error });
 		return null;
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/hooks/workspace-id-persist.ts` around lines 36 - 49,
getLatestWorkspaceMapping currently orders only by id and logs errors as a
string; change the selection to order by created_at DESC, id DESC to reliably
pick the most recent mapping (ensure WORKSPACE_MAPPINGS_SCHEMA includes a
created_at timestamp), and if the DB API supports explicit order/limit params
use those instead of embedding SQL in findAll; also replace string interpolation
in logger.error with structured logging (e.g., pass the error object/metadata to
logger.error) so the stack and context are preserved when catching exceptions in
getLatestWorkspaceMapping and when calling table.findAll on the
"workspace_mappings" table.
plugins/mutils/skills/workspace-id/generate.ts (1)

21-26: Confirm minute-level time granularity and local-time semantics are intentional.

YYYYMMDD-HHMM-... uses local time and can collide if re-run within the same minute for the same feature-name (you’ll get the same workspaceId). If collisions/timezones matter, consider documenting this or switching to UTC / adding seconds.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/skills/workspace-id/generate.ts` around lines 21 - 26, The
current workspaceId generation (variables: now, pad, yyyymmdd, HHmm,
workspaceId) uses local time truncated to minute granularity which can produce
identical IDs if rerun within the same minute and varies by timezone; update the
generation to use a higher-resolution or UTC-based timestamp (e.g., use UTC
getters like getUTCFullYear/getUTCMonth/getUTCDate/getUTCHours/getUTCMinutes
and/or append seconds via pad(now.getSeconds()) or milliseconds via Date.now())
or explicitly document that local-minute granularity is intended—modify the
yyyymmdd/HHmm construction or replace it with an ISO/epoch-derived component to
avoid collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/mutils/skills/workspace-id/generate.ts`:
- Around line 31-60: The current upsert into the "workspace_mappings" table uses
table.upsert which triggers INSERT OR REPLACE and causes the row id to be
regenerated; fix by first querying the table for an existing row with the same
workspace_id (use the same table reference for "workspace_mappings"), if found
capture its id and then include that id in the object passed to table.upsert({
id, workspace_id: workspaceId, feature_name: featureName, created_at: timestamp
}) so the existing row is updated instead of replaced; also remove the explicit
UNIQUE index creation for workspace_id since the column declaration workspace_id
TEXT NOT NULL UNIQUE already creates an implicit index.

---

Nitpick comments:
In `@plugins/mutils/hooks/workspace-id-persist.ts`:
- Around line 69-114: Validate the actual workspace path before advertising it:
after obtaining mapping from getLatestWorkspaceMapping and before using
mapping.workspace_id in the returned context.json, build the workspaceDir path
(using agentsDir or gitRoot + ".agents/workspaces/<mapping.workspace_id>"),
check that it exists (existsSync or equivalent), and if it doesn't exist
log/debug a warning and return context.success({}) (or adjust the message to not
claim the directory exists). Update the message sent in context.json to use the
confirmed workspaceDir variable (or omit the directory line when missing) so
mapping.workspace_id is only advertised when the physical
.agents/workspaces/<id> directory is present.
- Around line 36-49: getLatestWorkspaceMapping currently orders only by id and
logs errors as a string; change the selection to order by created_at DESC, id
DESC to reliably pick the most recent mapping (ensure WORKSPACE_MAPPINGS_SCHEMA
includes a created_at timestamp), and if the DB API supports explicit
order/limit params use those instead of embedding SQL in findAll; also replace
string interpolation in logger.error with structured logging (e.g., pass the
error object/metadata to logger.error) so the stack and context are preserved
when catching exceptions in getLatestWorkspaceMapping and when calling
table.findAll on the "workspace_mappings" table.

In `@plugins/mutils/skills/workspace-id/generate.ts`:
- Around line 21-26: The current workspaceId generation (variables: now, pad,
yyyymmdd, HHmm, workspaceId) uses local time truncated to minute granularity
which can produce identical IDs if rerun within the same minute and varies by
timezone; update the generation to use a higher-resolution or UTC-based
timestamp (e.g., use UTC getters like
getUTCFullYear/getUTCMonth/getUTCDate/getUTCHours/getUTCMinutes and/or append
seconds via pad(now.getSeconds()) or milliseconds via Date.now()) or explicitly
document that local-minute granularity is intended—modify the yyyymmdd/HHmm
construction or replace it with an ISO/epoch-derived component to avoid
collisions.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3275857 and d5f0c2c.

📒 Files selected for processing (3)
  • plugins/mutils/hooks/hooks.json
  • plugins/mutils/hooks/workspace-id-persist.ts
  • plugins/mutils/skills/workspace-id/generate.ts

Comment on lines +31 to +60
if (gitRoot) {
const agentsDir = path.join(gitRoot, ".agents");
if (existsSync(agentsDir)) {
try {
using db = MutilsDB.open(gitRoot);
const table = db.table<{
workspace_id: string;
feature_name: string;
created_at: string;
}>("workspace_mappings", {
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)",
],
});

const timestamp = now.toISOString();
table.upsert({
workspace_id: workspaceId,
feature_name: featureName,
created_at: timestamp,
});
} catch {
// Fail silently - workspace-id generation should still work
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find MutilsDB/MutilsTable implementation =="
find . -type f -name "*.ts" -o -name "*.tsx" | xargs rg -l "MutilsDB|MutilsTable" | head -20

echo
echo "== Locate the main MutilsDB file =="
fd -e ts -e tsx -e js -e jsx | xargs rg -l "class MutilsDB|export.*MutilsDB" 2>/dev/null | head -10

Repository: masseater/claude-code-plugin

Length of output: 393


🏁 Script executed:

#!/bin/bash
echo "== Check the file being reviewed =="
head -70 plugins/mutils/skills/workspace-id/generate.ts | tail -50

Repository: masseater/claude-code-plugin

Length of output: 1538


🏁 Script executed:

#!/bin/bash
echo "== Find the MutilsDB module/implementation =="
find . -path "*/packages/*" -name "*.ts" | xargs rg -l "MutilsDB" 2>/dev/null | head -20

Repository: masseater/claude-code-plugin

Length of output: 190


🏁 Script executed:

#!/bin/bash
echo "== Read the db.ts file to understand MutilsDB and upsert implementation =="
wc -l packages/cc-plugin-lib/src/db.ts

Repository: masseater/claude-code-plugin

Length of output: 183


🏁 Script executed:

#!/bin/bash
echo "== Read db.ts =="
cat -n packages/cc-plugin-lib/src/db.ts

Repository: masseater/claude-code-plugin

Length of output: 3765


🏁 Script executed:

#!/bin/bash
echo "== Check if bun run check and typecheck work =="
cd . && bun run check plugins/mutils/skills/workspace-id/generate.ts 2>&1 | head -100

Repository: masseater/claude-code-plugin

Length of output: 161


🏁 Script executed:

#!/bin/bash
echo "== Check bun run typecheck =="
cd . && bun run typecheck 2>&1 | grep -A 5 "workspace-id/generate.ts" || echo "No typecheck errors for this file"

Repository: masseater/claude-code-plugin

Length of output: 135


🏁 Script executed:

#!/bin/bash
echo "== Search for other upsert calls in the codebase =="
rg -n "\.upsert\(" --type ts --type tsx

Repository: masseater/claude-code-plugin

Length of output: 168


🏁 Script executed:

#!/bin/bash
echo "== Check if there are any tests or issues related to workspace-id churning =="
rg -i "churn|workspace.*id|id.*auto" plugins/mutils/ --type ts --type tsx | head -20

Repository: masseater/claude-code-plugin

Length of output: 194


🏁 Script executed:

#!/bin/bash
echo "== Search for upsert calls with different approach =="
rg -n "\.upsert\(" plugins/ packages/

Repository: masseater/claude-code-plugin

Length of output: 261


🏁 Script executed:

#!/bin/bash
echo "== Check the skill file guidelines - look for resource references =="
cat plugins/mutils/skills/workspace-id/generate.ts | grep -n "@\|reference\|resource" || echo "No resource references found"

Repository: masseater/claude-code-plugin

Length of output: 210


🏁 Script executed:

#!/bin/bash
echo "== Check the entire generate.ts file for context =="
cat -n plugins/mutils/skills/workspace-id/generate.ts

Repository: masseater/claude-code-plugin

Length of output: 2391


Preserve id on upsert conflict to prevent id regeneration.

The upsert() method in packages/cc-plugin-lib/src/db.ts uses INSERT OR REPLACE, which deletes and reinserts the row on conflict, generating a new id each time (line 55 of db.ts). This causes unexpected id-ordering semantics when the same workspace_id is upserted multiple times.

Additionally, the explicit UNIQUE index on workspace_id (line 48) is redundant—SQLite automatically creates an implicit index for TEXT NOT NULL UNIQUE constraints (line 43).

To fix: Query for the existing row by workspace_id, retrieve its id, and include the id in the upsert call to preserve it across updates.

Proposed fix (preserve id on conflict)
-			const table = db.table<{
-				workspace_id: string;
-				feature_name: string;
-				created_at: string;
-			}>("workspace_mappings", {
+			type WorkspaceMappingRow = {
+				id?: number;
+				workspace_id: string;
+				feature_name: string;
+				created_at: string;
+			};
+
+			const table = db.table<WorkspaceMappingRow>("workspace_mappings", {
 				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)",
 				],
 			});

 			const timestamp = now.toISOString();
-			table.upsert({
-				workspace_id: workspaceId,
-				feature_name: featureName,
-				created_at: timestamp,
-			});
+			const existing = table.findAll("workspace_id = ?", [workspaceId])[0];
+			if (existing?.id) {
+				table.upsert({
+					id: existing.id,
+					workspace_id: workspaceId,
+					feature_name: featureName,
+					created_at: timestamp,
+				});
+			} else {
+				table.upsert({
+					workspace_id: workspaceId,
+					feature_name: featureName,
+					created_at: timestamp,
+				});
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/mutils/skills/workspace-id/generate.ts` around lines 31 - 60, The
current upsert into the "workspace_mappings" table uses table.upsert which
triggers INSERT OR REPLACE and causes the row id to be regenerated; fix by first
querying the table for an existing row with the same workspace_id (use the same
table reference for "workspace_mappings"), if found capture its id and then
include that id in the object passed to table.upsert({ id, workspace_id:
workspaceId, feature_name: featureName, created_at: timestamp }) so the existing
row is updated instead of replaced; also remove the explicit UNIQUE index
creation for workspace_id since the column declaration workspace_id TEXT NOT
NULL UNIQUE already creates an implicit index.

@masseater
Copy link
Owner Author

Superseded by #20 - renamed session-id to workspace-id and simplified architecture

@masseater masseater closed this Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant