Skip to content

feat(workspaces): add AI item links#555

Merged
urjitc merged 1 commit into
ThinkEx-OSS:mainfrom
v-rdyy:feat/item-links
Jul 3, 2026
Merged

feat(workspaces): add AI item links#555
urjitc merged 1 commit into
ThinkEx-OSS:mainfrom
v-rdyy:feat/item-links

Conversation

@v-rdyy

@v-rdyy v-rdyy commented Jun 30, 2026

Copy link
Copy Markdown

Summary

  • Adds AI item links through the existing workspace_create_items, workspace_edit_item, and workspace_read_items tools.
  • Links are stored inside kernel_items.metadata_json, so this does not add a new database table.
  • Reading an item now returns its links inline.
  • Link creation remains a side effect of the model's existing item-tool usage: no background job, no embeddings/vector search, and no confidence threshold. Link judgement is left to the model, with intentionally open semantics per the design discussion.

Behavioral Change

  • workspace_edit_item now allows links-only edits by making edits optional.
  • Either edits or links must be present.
  • Existing callers that provide edits are unaffected.
  • This was necessary so the AI can update an item's related workspace items without also changing document content.

Out of Scope

  • Separate links table
  • Background job or periodic workspace scan
  • Embeddings or vector similarity
  • Confidence scoring
  • Frontend/UI work, including graph view or backlinks display

Test Plan

  • pnpm check
  • pnpm test (7 tests)
  • pnpm build

All passing locally.

Summary by CodeRabbit

  • New Features

    • Added support for workspace item relationships, including creating, viewing, and serializing related items.
    • Workspace create and edit actions can now include optional relations.
    • Read results now include related items when available.
  • Bug Fixes

    • Deleting items now also removes their associated relationships.
    • Relationship entries are validated and limited to keep results consistent and manageable.
  • Chores

    • Updated workspace schemas and supporting data structures to reflect the new relation fields and output shape.

@capy-ai

capy-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@urjitc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6bf48edc-56d4-4459-bddd-ed44e88f3eda

📥 Commits

Reviewing files that changed from the base of the PR and between 3900226 and 48573cf.

📒 Files selected for processing (17)
  • src/features/workspaces/ai/ai-tool-presentation.ts
  • src/features/workspaces/components/ai-chat/ai-chat-display-state.ts
  • src/features/workspaces/contracts.ts
  • src/features/workspaces/kernel/workspace-kernel-access.ts
  • src/features/workspaces/kernel/workspace-kernel-item-commands.ts
  • src/features/workspaces/kernel/workspace-kernel-relations.ts
  • src/features/workspaces/kernel/workspace-kernel-schema.ts
  • src/features/workspaces/kernel/workspace-kernel-types.ts
  • src/features/workspaces/kernel/workspace-kernel.ts
  • src/features/workspaces/operations/create-items.ts
  • src/features/workspaces/operations/edit-item.ts
  • src/features/workspaces/operations/link-items.ts
  • src/features/workspaces/operations/read-items.ts
  • src/features/workspaces/operations/relations.ts
  • src/features/workspaces/operations/workspace-operation-context.ts
  • src/features/workspaces/operations/workspace-tool-definitions.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
📝 Walkthrough

Walkthrough

This PR introduces workspace item relations: a new WorkspaceRelationKind contract, a kernel_relations storage table, a WorkspaceKernelRelations persistence class, kernel client/facade methods for creating and listing relations, resolution/serialization helpers, relation-aware create/edit/read operations, and corresponding AI tool schema updates.

Changes

Workspace item relations feature

Layer / File(s) Summary
Relation kind contract
src/features/workspaces/contracts.ts
Adds workspaceRelationKindValues, workspaceRelationKindSchema, and WorkspaceRelationKind type.
Kernel relation storage and schema
src/features/workspaces/kernel/workspace-kernel-schema.ts, src/features/workspaces/kernel/workspace-kernel-relations.ts
Adds kernel_relations table with indexes, and implements WorkspaceKernelRelations with create/delete/list operations and row mapping.
Kernel client contract and facade methods
src/features/workspaces/kernel/workspace-kernel-types.ts, workspace-kernel-access.ts, workspace-kernel-item-commands.ts, workspace-kernel.ts
Adds relation types, extends WorkspaceKernelClient, wires WorkspaceKernelRelations into item commands (including deletion cleanup), and adds createRelations/listItemRelations to WorkspaceKernel.
Relation resolution and serialization helpers
src/features/workspaces/operations/relations.ts
Adds resolveWorkspaceRelations, serializeWorkspaceRelations, resolveWorkspaceRelationTarget, and related failure code types.
Relation-aware item creation
src/features/workspaces/operations/create-items.ts
Resolves and creates relations per item during creation, with new failure codes.
Relation-aware item editing
src/features/workspaces/operations/edit-item.ts
Resolves and creates relations during document edits, with new failure codes.
Relation-enriched item reads
src/features/workspaces/operations/workspace-operation-context.ts, read-items.ts
Adds pageItems to operation context and enriches read items with serialized relations.
AI tool schemas and wiring
src/features/workspaces/operations/workspace-tool-schemas.ts, workspace-tool-definitions.ts
Adds relation input/output Zod schemas, examples, and forwards relations through the edit tool.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Tool as workspace_edit_item tool
  participant EditOp as editWorkspaceItemOperation
  participant RelResolver as resolveWorkspaceRelations
  participant Kernel as WorkspaceKernel
  participant RelStore as WorkspaceKernelRelations

  Tool->>EditOp: execute(path, edits, relations)
  EditOp->>RelResolver: resolveWorkspaceRelations(fromItemId, relations, tree)
  RelResolver-->>EditOp: ready relations or failure
  alt resolution failed
    EditOp-->>Tool: failure result
  else resolution ready
    EditOp->>EditOp: apply markdown edits
    alt applied > 0 and relations present
      EditOp->>Kernel: createRelations(relations)
      Kernel->>Kernel: assert items active
      Kernel->>RelStore: createRelations(relations)
      RelStore-->>Kernel: void
    end
    EditOp-->>Tool: success result
  end
Loading
sequenceDiagram
  participant ReadOp as readWorkspaceItemsOperation
  participant Kernel as WorkspaceKernel
  participant RelStore as WorkspaceKernelRelations
  participant Serializer as serializeWorkspaceRelations

  ReadOp->>ReadOp: build pathsByItemId from pageItems
  ReadOp->>Kernel: listItemRelations(itemId)
  Kernel->>RelStore: listItemRelations(itemId, limit)
  RelStore-->>Kernel: WorkspaceKernelRelation[]
  Kernel-->>ReadOp: relations
  ReadOp->>Serializer: serializeWorkspaceRelations(item, pathsByItemId, relations)
  Serializer-->>ReadOp: WorkspaceRelationOutput[]
  ReadOp-->>ReadOp: attach relations to item result
Loading

Suggested labels: capy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly describes the main change: adding AI item links to workspaces.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
src/features/workspaces/kernel/workspace-kernel-item-commands.ts (1)

394-399: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider rejecting self-links and improving the error message.

assertLinkTargetsExist doesn't exclude the item's own id, so an item can be linked to itself (in updateItemLinks, the target item is already active by the time this runs). Also, failures surface as a generic "Workspace item not found." with no indication that it originated from link validation, which makes debugging harder.

♻️ Optional improvement
-	private assertLinkTargetsExist(linkItemIds: readonly string[]) {
-		for (const linkItemId of linkItemIds) {
-			this.store.assertActiveItem(linkItemId);
-		}
-	}
+	private assertLinkTargetsExist(linkItemIds: readonly string[], excludeItemId?: string) {
+		for (const linkItemId of linkItemIds) {
+			if (linkItemId === excludeItemId) {
+				throw new Error(`Workspace item cannot link to itself: ${linkItemId}`);
+			}
+
+			try {
+				this.store.assertActiveItem(linkItemId);
+			} catch {
+				throw new Error(`Link target item not found: ${linkItemId}`);
+			}
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/kernel/workspace-kernel-item-commands.ts` around
lines 394 - 399, Update assertLinkTargetsExist in workspace-kernel-item-commands
so it rejects self-links by checking each linkItemId against the current item’s
id before calling store.assertActiveItem. Also change the validation path to
surface a link-specific error message instead of the generic “Workspace item not
found.”, so failures from updateItemLinks clearly indicate link validation and
the offending target.
src/features/workspaces/ai/workspace-kernel-ai-common.ts (1)

226-249: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Path index rebuilt per item link lookup; hoist for batch reads.

getWorkspaceKernelAiItemLinks rebuilds the full item tree/path index (buildWorkspaceKernelItemPathIndex, which itself calls buildWorkspaceKernelTree) and an itemsById map from scratch on every call. Since workspace_read_items can read multiple paths in a single request and this function is invoked once per item read (per the readWorkspaceKernelAiItem/readWorkspaceKernelAiFileItem snippets), the same index gets recomputed redundantly for every item in the batch.

♻️ Proposed refactor: accept a precomputed index
-export function getWorkspaceKernelAiItemLinks(input: {
-	item: WorkspaceItemSummary;
-	pageItems: WorkspaceItemSummary[];
-}): WorkspaceItemLink[] {
-	const pathsByItemId = buildWorkspaceKernelItemPathIndex(input.pageItems);
-	const itemsById = new Map(input.pageItems.map((item) => [item.id, item]));
+export function getWorkspaceKernelAiItemLinks(input: {
+	item: WorkspaceItemSummary;
+	pathsByItemId: ReadonlyMap<string, string>;
+	itemsById: ReadonlyMap<string, WorkspaceItemSummary>;
+}): WorkspaceItemLink[] {
+	const { pathsByItemId, itemsById } = input;
 	const links: WorkspaceItemLink[] = [];

Callers (e.g. read/edit flows) would build pathsByItemId/itemsById once per request from pageItems and pass them to every per-item call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/ai/workspace-kernel-ai-common.ts` around lines 226 -
249, `getWorkspaceKernelAiItemLinks` is recomputing the same path/index data for
every item lookup, which is wasteful during batch reads. Refactor this function
to accept precomputed `pathsByItemId` and `itemsById` (built once from
`pageItems` by callers such as the `readWorkspaceKernelAiItem` /
`readWorkspaceKernelAiFileItem` flow) instead of calling
`buildWorkspaceKernelItemPathIndex` internally each time. Update the call sites
to hoist the shared index creation per request and pass it through to
`getWorkspaceKernelAiItemLinks`, while preserving the existing link-building
behavior.
src/features/workspaces/ai/workspace-kernel-ai-item-links.test.ts (3)

269-281: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated item-type union literal.

The inline union "document" | "file" | "flashcard" | "folder" | "quiz" duplicates the workspace item type enum likely defined elsewhere (e.g. via workspaceItemTypeSchema, referenced in the kernel item-commands snippet). If the canonical type union changes, this helper silently drifts out of sync.

♻️ Suggested fix
-async function expectItemLinks(
-	path: string,
-	links: Array<{ path: string; type: "document" | "file" | "flashcard" | "folder" | "quiz" }>,
-) {
+async function expectItemLinks(
+	path: string,
+	links: Array<{ path: string; type: WorkspaceItemType }>,
+) {

Import WorkspaceItemType (or z.infer<typeof workspaceItemTypeSchema>) from the shared model module instead of re-declaring the union.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/ai/workspace-kernel-ai-item-links.test.ts` around
lines 269 - 281, The helper expectItemLinks is re-declaring the workspace item
type union inline, which can drift from the canonical type definition. Replace
the local Array<{ path: string; type: ... }> annotation with the shared
WorkspaceItemType type (or z.infer<typeof workspaceItemTypeSchema>) imported
from the shared model module so the test stays aligned with the source of truth.

124-218: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for the "neither edits nor links provided" validation.

The PR objective states edits is now optional and "Either edits or links must be provided." This suite covers content-only edits, links-only edits, and link-resolution failures, but doesn't exercise the case where both edits and links are omitted on editWorkspaceKernelAiItem. Worth adding to lock in that validation behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/ai/workspace-kernel-ai-item-links.test.ts` around
lines 124 - 218, Add a test in workspace-kernel-ai-item-links.test.ts that calls
editWorkspaceKernelAiItem without either edits or links and asserts the
validation error for the “either edits or links must be provided” case. Reuse
the existing editWorkspaceKernelAiItem and createWorkspaceKernelAiItems setup
patterns, and place the new coverage near the current edit validation tests so
the behavior is locked in alongside the existing link and edit failure cases.

283-344: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Load the test DB from the real D1 migrations instead of duplicating schema here. wrangler.test.jsonc already points migrations_dir at drizzle, and the checked-in migrations include foreign keys, unique indexes, and check constraints that this hand-rolled DDL omits. Use readD1Migrations/applyD1Migrations (or a shared schema fixture) so the test schema stays aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/ai/workspace-kernel-ai-item-links.test.ts` around
lines 283 - 344, The test helper seedWorkspaceAccess is duplicating the database
schema with hand-written DDL in workspaceAccessSchema and
applyWorkspaceAccessSchema instead of using the real D1 migrations. Replace that
setup with the existing migration-loading path (for example readD1Migrations and
applyD1Migrations, or a shared schema fixture) so the test DB is built from the
checked-in drizzle migrations and stays aligned with the production schema,
including foreign keys, unique indexes, and check constraints.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/workspaces/ai/workspace-kernel-ai-common.ts`:
- Around line 195-312: `resolveWorkspaceKernelAiLinkPaths` and
`resolveWorkspaceKernelAiLinkPath` currently allow resolving the item’s own
path, which can persist a self-referential link during the
`workspace-kernel-ai-edit` flow. Update the link-resolution flow to detect when
a resolved `itemId` matches the item being edited and fail that entry instead of
returning `ready`, using the existing resolution/failure pattern in
`resolveWorkspaceKernelAiLinkPaths` so self-links are excluded before
persistence.

In `@src/features/workspaces/ai/workspace-kernel-ai-edit.ts`:
- Around line 119-142: The link update branch in workspace-kernel-ai-edit’s edit
flow should not run when applyMarkdownEdits reports failures. Update the logic
around applyMarkdownEdits and context.kernel.updateItemLinks so links are only
mutated when result.failures is empty (or otherwise handled explicitly), and
keep the return shape in sync with the successful/failed edit outcome.

---

Nitpick comments:
In `@src/features/workspaces/ai/workspace-kernel-ai-common.ts`:
- Around line 226-249: `getWorkspaceKernelAiItemLinks` is recomputing the same
path/index data for every item lookup, which is wasteful during batch reads.
Refactor this function to accept precomputed `pathsByItemId` and `itemsById`
(built once from `pageItems` by callers such as the `readWorkspaceKernelAiItem`
/ `readWorkspaceKernelAiFileItem` flow) instead of calling
`buildWorkspaceKernelItemPathIndex` internally each time. Update the call sites
to hoist the shared index creation per request and pass it through to
`getWorkspaceKernelAiItemLinks`, while preserving the existing link-building
behavior.

In `@src/features/workspaces/ai/workspace-kernel-ai-item-links.test.ts`:
- Around line 269-281: The helper expectItemLinks is re-declaring the workspace
item type union inline, which can drift from the canonical type definition.
Replace the local Array<{ path: string; type: ... }> annotation with the shared
WorkspaceItemType type (or z.infer<typeof workspaceItemTypeSchema>) imported
from the shared model module so the test stays aligned with the source of truth.
- Around line 124-218: Add a test in workspace-kernel-ai-item-links.test.ts that
calls editWorkspaceKernelAiItem without either edits or links and asserts the
validation error for the “either edits or links must be provided” case. Reuse
the existing editWorkspaceKernelAiItem and createWorkspaceKernelAiItems setup
patterns, and place the new coverage near the current edit validation tests so
the behavior is locked in alongside the existing link and edit failure cases.
- Around line 283-344: The test helper seedWorkspaceAccess is duplicating the
database schema with hand-written DDL in workspaceAccessSchema and
applyWorkspaceAccessSchema instead of using the real D1 migrations. Replace that
setup with the existing migration-loading path (for example readD1Migrations and
applyD1Migrations, or a shared schema fixture) so the test DB is built from the
checked-in drizzle migrations and stays aligned with the production schema,
including foreign keys, unique indexes, and check constraints.

In `@src/features/workspaces/kernel/workspace-kernel-item-commands.ts`:
- Around line 394-399: Update assertLinkTargetsExist in
workspace-kernel-item-commands so it rejects self-links by checking each
linkItemId against the current item’s id before calling store.assertActiveItem.
Also change the validation path to surface a link-specific error message instead
of the generic “Workspace item not found.”, so failures from updateItemLinks
clearly indicate link validation and the offending target.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e6d6f173-4dea-4d1a-9de8-ca68ba1a3a12

📥 Commits

Reviewing files that changed from the base of the PR and between f71343d and f7d026e.

📒 Files selected for processing (17)
  • src/features/workspaces/ai/workspace-kernel-ai-common.ts
  • src/features/workspaces/ai/workspace-kernel-ai-create.ts
  • src/features/workspaces/ai/workspace-kernel-ai-edit.ts
  • src/features/workspaces/ai/workspace-kernel-ai-item-links.test.ts
  • src/features/workspaces/ai/workspace-kernel-ai-read.ts
  • src/features/workspaces/ai/workspace-tools.ts
  • src/features/workspaces/kernel/workspace-kernel-access.ts
  • src/features/workspaces/kernel/workspace-kernel-item-commands.ts
  • src/features/workspaces/kernel/workspace-kernel-types.ts
  • src/features/workspaces/kernel/workspace-kernel.ts
  • src/features/workspaces/model/workspace-item-links.ts
  • src/features/workspaces/model/workspace-page.ts
  • src/features/workspaces/realtime/messages.ts
  • src/features/workspaces/server/permissions.ts
  • src/test-worker.ts
  • vitest.config.ts
  • wrangler.test.jsonc

Comment on lines +195 to +312

export function resolveWorkspaceKernelAiLinkPaths(input: {
createdItemsByPath?: ReadonlyMap<string, WorkspaceKernelAiCreatedLinkTarget>;
paths: readonly string[];
tree: WorkspaceKernelTree;
}): WorkspaceKernelAiLinkResolution {
const linkItemIds: string[] = [];

for (const [index, path] of input.paths.entries()) {
const resolution = resolveWorkspaceKernelAiLinkPath({
createdItemsByPath: input.createdItemsByPath,
path,
tree: input.tree,
});

if (resolution.status === "failed") {
return {
...resolution,
index,
};
}

linkItemIds.push(resolution.itemId);
}

return {
linkItemIds: uniqueWorkspaceKernelAiLinkItemIds(linkItemIds),
status: "ready",
};
}

export function getWorkspaceKernelAiItemLinks(input: {
item: WorkspaceItemSummary;
pageItems: WorkspaceItemSummary[];
}): WorkspaceItemLink[] {
const pathsByItemId = buildWorkspaceKernelItemPathIndex(input.pageItems);
const itemsById = new Map(input.pageItems.map((item) => [item.id, item]));
const links: WorkspaceItemLink[] = [];

for (const itemId of getWorkspaceItemLinkItemIds(input.item.metadataJson)) {
const item = itemsById.get(itemId);
const path = pathsByItemId.get(itemId);

if (!item || !path) {
continue;
}

links.push({
path,
type: item.type,
});
}

return links;
}

function resolveWorkspaceKernelAiLinkPath(input: {
createdItemsByPath?: ReadonlyMap<string, WorkspaceKernelAiCreatedLinkTarget>;
path: string;
tree: WorkspaceKernelTree;
}):
| {
itemId: string;
status: "ready";
}
| {
code: WorkspaceKernelAiLinkResolutionFailureCode;
path: string;
status: "failed";
} {
let normalizedPath: string;

try {
normalizedPath = normalizeWorkspacePath(input.path);
} catch (error) {
if (error instanceof WorkspaceKernelPathError && error.code === "path_not_absolute") {
return {
code: "link_path_not_absolute",
path: input.path,
status: "failed",
};
}

throw error;
}

if (normalizedPath === "/") {
return {
code: "link_path_is_root",
path: normalizedPath,
status: "failed",
};
}

const createdItem = input.createdItemsByPath?.get(normalizedPath);

if (createdItem) {
return {
itemId: createdItem.id,
status: "ready",
};
}

const item = resolveWorkspaceKernelItemPath(normalizedPath, input.tree);

if (!item) {
return {
code: "link_path_not_found",
path: normalizedPath,
status: "failed",
};
}

return {
itemId: item.id,
status: "ready",
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

No guard against an item linking to itself.

resolveWorkspaceKernelAiLinkPaths/resolveWorkspaceKernelAiLinkPath resolve any valid path, including the path of the item currently being edited. Per the edit flow (workspace-kernel-ai-edit.ts), input.links is resolved without excluding resolution.item.id, and the kernel's assertLinkTargetsExist only checks existence, not self-reference. A user/model request that includes the item's own path in links will silently persist a self-referential link.

🔧 Proposed fix to exclude self-references
 export function resolveWorkspaceKernelAiLinkPaths(input: {
 	createdItemsByPath?: ReadonlyMap<string, WorkspaceKernelAiCreatedLinkTarget>;
+	excludeItemId?: string;
 	paths: readonly string[];
 	tree: WorkspaceKernelTree;
 }): WorkspaceKernelAiLinkResolution {
 	const linkItemIds: string[] = [];

 	for (const [index, path] of input.paths.entries()) {
 		const resolution = resolveWorkspaceKernelAiLinkPath({
 			createdItemsByPath: input.createdItemsByPath,
 			path,
 			tree: input.tree,
 		});

 		if (resolution.status === "failed") {
 			return {
 				...resolution,
 				index,
 			};
 		}

+		if (resolution.itemId === input.excludeItemId) {
+			continue;
+		}
+
 		linkItemIds.push(resolution.itemId);
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function resolveWorkspaceKernelAiLinkPaths(input: {
createdItemsByPath?: ReadonlyMap<string, WorkspaceKernelAiCreatedLinkTarget>;
paths: readonly string[];
tree: WorkspaceKernelTree;
}): WorkspaceKernelAiLinkResolution {
const linkItemIds: string[] = [];
for (const [index, path] of input.paths.entries()) {
const resolution = resolveWorkspaceKernelAiLinkPath({
createdItemsByPath: input.createdItemsByPath,
path,
tree: input.tree,
});
if (resolution.status === "failed") {
return {
...resolution,
index,
};
}
linkItemIds.push(resolution.itemId);
}
return {
linkItemIds: uniqueWorkspaceKernelAiLinkItemIds(linkItemIds),
status: "ready",
};
}
export function getWorkspaceKernelAiItemLinks(input: {
item: WorkspaceItemSummary;
pageItems: WorkspaceItemSummary[];
}): WorkspaceItemLink[] {
const pathsByItemId = buildWorkspaceKernelItemPathIndex(input.pageItems);
const itemsById = new Map(input.pageItems.map((item) => [item.id, item]));
const links: WorkspaceItemLink[] = [];
for (const itemId of getWorkspaceItemLinkItemIds(input.item.metadataJson)) {
const item = itemsById.get(itemId);
const path = pathsByItemId.get(itemId);
if (!item || !path) {
continue;
}
links.push({
path,
type: item.type,
});
}
return links;
}
function resolveWorkspaceKernelAiLinkPath(input: {
createdItemsByPath?: ReadonlyMap<string, WorkspaceKernelAiCreatedLinkTarget>;
path: string;
tree: WorkspaceKernelTree;
}):
| {
itemId: string;
status: "ready";
}
| {
code: WorkspaceKernelAiLinkResolutionFailureCode;
path: string;
status: "failed";
} {
let normalizedPath: string;
try {
normalizedPath = normalizeWorkspacePath(input.path);
} catch (error) {
if (error instanceof WorkspaceKernelPathError && error.code === "path_not_absolute") {
return {
code: "link_path_not_absolute",
path: input.path,
status: "failed",
};
}
throw error;
}
if (normalizedPath === "/") {
return {
code: "link_path_is_root",
path: normalizedPath,
status: "failed",
};
}
const createdItem = input.createdItemsByPath?.get(normalizedPath);
if (createdItem) {
return {
itemId: createdItem.id,
status: "ready",
};
}
const item = resolveWorkspaceKernelItemPath(normalizedPath, input.tree);
if (!item) {
return {
code: "link_path_not_found",
path: normalizedPath,
status: "failed",
};
}
return {
itemId: item.id,
status: "ready",
};
}
export function resolveWorkspaceKernelAiLinkPaths(input: {
createdItemsByPath?: ReadonlyMap<string, WorkspaceKernelAiCreatedLinkTarget>;
excludeItemId?: string;
paths: readonly string[];
tree: WorkspaceKernelTree;
}): WorkspaceKernelAiLinkResolution {
const linkItemIds: string[] = [];
for (const [index, path] of input.paths.entries()) {
const resolution = resolveWorkspaceKernelAiLinkPath({
createdItemsByPath: input.createdItemsByPath,
path,
tree: input.tree,
});
if (resolution.status === "failed") {
return {
...resolution,
index,
};
}
if (resolution.itemId === input.excludeItemId) {
continue;
}
linkItemIds.push(resolution.itemId);
}
return {
linkItemIds: uniqueWorkspaceKernelAiLinkItemIds(linkItemIds),
status: "ready",
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/ai/workspace-kernel-ai-common.ts` around lines 195 -
312, `resolveWorkspaceKernelAiLinkPaths` and `resolveWorkspaceKernelAiLinkPath`
currently allow resolving the item’s own path, which can persist a
self-referential link during the `workspace-kernel-ai-edit` flow. Update the
link-resolution flow to detect when a resolved `itemId` matches the item being
edited and fail that entry instead of returning `ready`, using the existing
resolution/failure pattern in `resolveWorkspaceKernelAiLinkPaths` so self-links
are excluded before persistence.

Comment thread src/features/workspaces/ai/workspace-kernel-ai-edit.ts Outdated
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds AI-managed item links to the workspace kernel, storing link target IDs inside kernel_items.metadata_json under a "links" key. The three AI workspace tools (workspace_create_items, workspace_edit_item, workspace_read_items) are all updated to carry links through, and a new updateItemLinks kernel command handles atomic persistence with target existence assertions.

  • Create: items can declare links at creation time, including forward-references to items created earlier in the same batch request.
  • Edit: edits is now optional; either edits or links must be present. A links-only edit path skips the document session entirely and goes straight to updateItemLinks.
  • Read: every read result now includes a resolved links array (path + type), populated from the workspace page snapshot.
  • Infrastructure: a dedicated wrangler.test.jsonc and test-worker.ts shim enable integration tests that run against real Durable Object logic without remote bindings.

Confidence Score: 3/5

Safe to merge once the combined-edits-and-links atomicity question in workspace-kernel-ai-edit.ts is resolved; the rest of the change is well-structured and well-tested.

In the combined edits+links code path, updateItemLinks fires unconditionally after applyMarkdownEdits — even when every document edit fails. The model receives applied: 0 with a full failed array alongside a mutated links field, creating a split state where content is unchanged but links are permanently replaced. This is the only path in the PR where the new code interacts with existing document-session behavior, and it produces an outcome that is unlikely to be intentional. The rest of the change — the links-only edit path, create-time links, read-time link resolution, kernel command, and test harness — is clean and straightforward.

src/features/workspaces/ai/workspace-kernel-ai-edit.ts — specifically the code that calls updateItemLinks after applyMarkdownEdits without checking whether any edits succeeded.

Important Files Changed

Filename Overview
src/features/workspaces/ai/workspace-kernel-ai-edit.ts Makes edits optional and adds a links parameter. Two issues: (1) updateItemLinks fires unconditionally even when all document edits fail; (2) edit link-resolution failures drop the invalid path present in create failures.
src/features/workspaces/ai/workspace-kernel-ai-common.ts Adds resolveWorkspaceKernelAiLinkPaths and getWorkspaceKernelAiItemLinks. Path resolution, root detection, and deduplication are all correct. The createdItemsByPath pass-through for within-batch links is handled cleanly.
src/features/workspaces/ai/workspace-kernel-ai-create.ts Wires optional links through item creation and uses createdItemsByPath for within-batch link resolution. Failure propagation matches existing create failure conventions.
src/features/workspaces/kernel/workspace-kernel-item-commands.ts Adds updateItemLinks command with assertLinkTargetsExist guard before the SQL UPDATE. The assertion order is correct: targets are validated before any mutation.
src/features/workspaces/model/workspace-item-links.ts Clean model file: stores link IDs as "links" in metadata_json, with robust deduplication and type-safe helpers.
src/features/workspaces/ai/workspace-kernel-ai-item-links.test.ts Five integration tests covering create/edit link semantics, invalid paths (not absolute, root, not found), and metadata coexistence. Good coverage of the new paths.
src/features/workspaces/ai/workspace-tools.ts Schema changes correctly add links to create/edit inputs and read output, with max-20 guard and superRefine validation requiring at least edits or links.

Reviews (1): Last reviewed commit: "feat(workspaces): add AI item links" | Re-trigger Greptile

Comment on lines 119 to 145
const result = await documentSession.applyMarkdownEdits({
edits: input.edits,
edits,
});
const command =
links === undefined
? null
: await context.kernel.updateItemLinks({
itemId: resolution.item.id,
linkItemIds: links.linkItemIds,
actorUserId: input.userId,
clientMutationId: null,
});

return {
applied: result.applied,
failed: result.failures,
...(command
? {
links: getWorkspaceKernelAiItemLinks({
item: command.result,
pageItems: context.pageItems,
}),
}
: {}),
path: resolution.path,
warnings: result.warnings,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Links updated even when all document edits fail

When both edits and links are supplied, updateItemLinks is called unconditionally after applyMarkdownEdits — even if every edit failed (result.applied === 0, result.failures.length === edits.length). The model receives a response with applied: 0, a full failed array, but also the mutated links field, creating a split state: content unchanged, links permanently replaced. On a retry the model will set the same links a second time (harmless but wasteful), but it will never know the content edit and link update should have been atomic. Consider guarding the updateItemLinks call with if (result.applied > 0 || result.failures.length === 0), or document explicitly that links are always applied regardless of edit outcomes.

Comment on lines +78 to +84
if (links?.status === "failed") {
return {
path: resolution.path,
warnings: [],
...failedWorkspaceAiEditResult(links.code, Math.max(edits.length, 1), links.index),
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Edit link failures omit the invalid path

Create link failures include {code, index, path} so the caller knows which path was rejected. Edit link failures go through failedWorkspaceAiEditResult, which only returns {code, index} — the links.path that triggered the error is silently dropped. This makes it harder for the model to self-correct (it knows there was a bad link but not which one). Either thread path through failedWorkspaceAiEditResult for link-specific failures, or include path directly in the returned failure object for this branch.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

9 issues found across 17 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/ai/workspace-kernel-ai-common.ts">

<violation number="1" location="src/features/workspaces/ai/workspace-kernel-ai-common.ts:230">
P2: Indexes rebuilt on every call to `getWorkspaceKernelAiItemLinks`, leading to O(m·n) work when reading multiple items</violation>
</file>

<file name="src/features/workspaces/model/workspace-page.ts">

<violation number="1" location="src/features/workspaces/model/workspace-page.ts:27">
P1: Metadata update events may allow stale full-item payloads to overwrite newer page state because `upsertWorkspaceItemsInPage` applies the event payload unconditionally while only the page revision is merged via `Math.max`. This is exacerbated by the new links-only metadata edits, which can be concurrent with content/name/color changes.</violation>
</file>

<file name="src/features/workspaces/model/workspace-item-links.ts">

<violation number="1" location="src/features/workspaces/model/workspace-item-links.ts:3">
P2: Using a generic `"links"` key in shared metadata risks silently overwriting unrelated metadata stored under the same key; use a namespaced key to avoid collisions.</violation>
</file>

<file name="src/features/workspaces/ai/workspace-kernel-ai-read.ts">

<violation number="1" location="src/features/workspaces/ai/workspace-kernel-ai-read.ts:146">
P2: File item reads compute links twice: the outer `readWorkspaceKernelAiItem` resolves links but does not pass them to the file branch, so `readWorkspaceKernelAiFileItem` recomputes the same links from the same item and pageItems. This adds unnecessary overhead to every file read, especially in batch reads.</violation>
</file>

<file name="src/features/workspaces/kernel/workspace-kernel-item-commands.ts">

<violation number="1" location="src/features/workspaces/kernel/workspace-kernel-item-commands.ts:35">
P1: Link target validation can be bypassed when links are supplied via `metadataJson` instead of `linkItemIds` during item creation.</violation>

<violation number="2" location="src/features/workspaces/kernel/workspace-kernel-item-commands.ts:35">
P2: Deleting a workspace item leaves dangling link IDs in other items' metadata because `deleteItems` does not clean up or revalidate inbound `metadata_json` links. Links are validated at write-time, but there is no symmetric cleanup or read-time filtering, so items can reference deleted targets.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

case "workspace.item.moved":
case "workspace.item.color.updated":
case "workspace.item.content.updated":
case "workspace.item.metadata.updated":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Metadata update events may allow stale full-item payloads to overwrite newer page state because upsertWorkspaceItemsInPage applies the event payload unconditionally while only the page revision is merged via Math.max. This is exacerbated by the new links-only metadata edits, which can be concurrent with content/name/color changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/model/workspace-page.ts, line 27:

<comment>Metadata update events may allow stale full-item payloads to overwrite newer page state because `upsertWorkspaceItemsInPage` applies the event payload unconditionally while only the page revision is merged via `Math.max`. This is exacerbated by the new links-only metadata edits, which can be concurrent with content/name/color changes.</comment>

<file context>
@@ -24,6 +24,7 @@ export function applyWorkspaceEventToPage(
 		case "workspace.item.moved":
 		case "workspace.item.color.updated":
 		case "workspace.item.content.updated":
+		case "workspace.item.metadata.updated":
 			return upsertWorkspaceItemInPage(page, event.payload.item, event.revision);
 		case "workspace.items.moved":
</file context>

Comment thread src/features/workspaces/ai/workspace-kernel-ai-edit.ts Outdated
UpdateWorkspaceKernelItemLinksArgs,
WriteWorkspaceKernelItemArgs,
} from "#/features/workspaces/kernel/workspace-kernel-types";
import { withWorkspaceItemLinksMetadata } from "#/features/workspaces/model/workspace-item-links";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Link target validation can be bypassed when links are supplied via metadataJson instead of linkItemIds during item creation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/kernel/workspace-kernel-item-commands.ts, line 35:

<comment>Link target validation can be bypassed when links are supplied via `metadataJson` instead of `linkItemIds` during item creation.</comment>

<file context>
@@ -28,8 +29,10 @@ import type {
+	UpdateWorkspaceKernelItemLinksArgs,
 	WriteWorkspaceKernelItemArgs,
 } from "#/features/workspaces/kernel/workspace-kernel-types";
+import { withWorkspaceItemLinksMetadata } from "#/features/workspaces/model/workspace-item-links";
 import {
 	resolveWorkspaceItemColorForCreate,
</file context>

Comment thread src/test-worker.ts Outdated
item: WorkspaceItemSummary;
pageItems: WorkspaceItemSummary[];
}): WorkspaceItemLink[] {
const pathsByItemId = buildWorkspaceKernelItemPathIndex(input.pageItems);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Indexes rebuilt on every call to getWorkspaceKernelAiItemLinks, leading to O(m·n) work when reading multiple items

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/workspace-kernel-ai-common.ts, line 230:

<comment>Indexes rebuilt on every call to `getWorkspaceKernelAiItemLinks`, leading to O(m·n) work when reading multiple items</comment>

<file context>
@@ -165,3 +192,137 @@ export function resolveWorkspaceKernelAiExistingItemPath<TRootCode extends strin
+	item: WorkspaceItemSummary;
+	pageItems: WorkspaceItemSummary[];
+}): WorkspaceItemLink[] {
+	const pathsByItemId = buildWorkspaceKernelItemPathIndex(input.pageItems);
+	const itemsById = new Map(input.pageItems.map((item) => [item.id, item]));
+	const links: WorkspaceItemLink[] = [];
</file context>

@@ -0,0 +1,44 @@
import type { JsonValue, WorkspaceItemSummary } from "#/features/workspaces/contracts";

export const workspaceItemLinksMetadataKey = "links";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Using a generic "links" key in shared metadata risks silently overwriting unrelated metadata stored under the same key; use a namespaced key to avoid collisions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/model/workspace-item-links.ts, line 3:

<comment>Using a generic `"links"` key in shared metadata risks silently overwriting unrelated metadata stored under the same key; use a namespaced key to avoid collisions.</comment>

<file context>
@@ -0,0 +1,44 @@
+import type { JsonValue, WorkspaceItemSummary } from "#/features/workspaces/contracts";
+
+export const workspaceItemLinksMetadataKey = "links";
+
+export interface WorkspaceItemLink {
</file context>

path: string;
}): Promise<WorkspaceKernelAiReadItem> {
const { item } = input;
const links = getWorkspaceKernelAiItemLinks({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: File item reads compute links twice: the outer readWorkspaceKernelAiItem resolves links but does not pass them to the file branch, so readWorkspaceKernelAiFileItem recomputes the same links from the same item and pageItems. This adds unnecessary overhead to every file read, especially in batch reads.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/workspace-kernel-ai-read.ts, line 146:

<comment>File item reads compute links twice: the outer `readWorkspaceKernelAiItem` resolves links but does not pass them to the file branch, so `readWorkspaceKernelAiFileItem` recomputes the same links from the same item and pageItems. This adds unnecessary overhead to every file read, especially in batch reads.</comment>

<file context>
@@ -135,9 +139,14 @@ async function readWorkspaceKernelAiItem(input: {
 	path: string;
 }): Promise<WorkspaceKernelAiReadItem> {
 	const { item } = input;
+	const links = getWorkspaceKernelAiItemLinks({
+		item,
+		pageItems: input.pageItems,
</file context>

Comment thread src/features/workspaces/ai/workspace-kernel-ai-edit.ts Outdated
Comment thread src/features/workspaces/kernel/workspace-kernel-item-commands.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/ai/workspace-kernel-ai-common.ts">

<violation number="1" location="src/features/workspaces/ai/workspace-kernel-ai-common.ts:230">
P2: Indexes rebuilt on every call to `getWorkspaceKernelAiItemLinks`, leading to O(m·n) work when reading multiple items</violation>
</file>

<file name="src/features/workspaces/model/workspace-page.ts">

<violation number="1" location="src/features/workspaces/model/workspace-page.ts:27">
P1: Metadata update events may allow stale full-item payloads to overwrite newer page state because `upsertWorkspaceItemsInPage` applies the event payload unconditionally while only the page revision is merged via `Math.max`. This is exacerbated by the new links-only metadata edits, which can be concurrent with content/name/color changes.</violation>
</file>

<file name="src/features/workspaces/model/workspace-item-links.ts">

<violation number="1" location="src/features/workspaces/model/workspace-item-links.ts:3">
P2: Using a generic `"links"` key in shared metadata risks silently overwriting unrelated metadata stored under the same key; use a namespaced key to avoid collisions.</violation>
</file>

<file name="src/features/workspaces/ai/workspace-kernel-ai-read.ts">

<violation number="1" location="src/features/workspaces/ai/workspace-kernel-ai-read.ts:146">
P2: File item reads compute links twice: the outer `readWorkspaceKernelAiItem` resolves links but does not pass them to the file branch, so `readWorkspaceKernelAiFileItem` recomputes the same links from the same item and pageItems. This adds unnecessary overhead to every file read, especially in batch reads.</violation>
</file>

<file name="src/features/workspaces/kernel/workspace-kernel-item-commands.ts">

<violation number="1" location="src/features/workspaces/kernel/workspace-kernel-item-commands.ts:35">
P1: Link target validation can be bypassed when links are supplied via `metadataJson` instead of `linkItemIds` during item creation.</violation>
</file>

<file name="src/features/workspaces/ai/workspace-kernel-ai-create.ts">

<violation number="1" location="src/features/workspaces/ai/workspace-kernel-ai-create.ts:143">
P2: Self-link filter in createWorkspaceKernelAiItems is a no-op: the resolved link IDs cannot include the freshly generated item ID, so the filter never removes anything and gives a false sense of protection.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}

const itemId = crypto.randomUUID();
const linkItemIds = links?.linkItemIds.filter((linkItemId) => linkItemId !== itemId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Self-link filter in createWorkspaceKernelAiItems is a no-op: the resolved link IDs cannot include the freshly generated item ID, so the filter never removes anything and gives a false sense of protection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/ai/workspace-kernel-ai-create.ts, line 143:

<comment>Self-link filter in createWorkspaceKernelAiItems is a no-op: the resolved link IDs cannot include the freshly generated item ID, so the filter never removes anything and gives a false sense of protection.</comment>

<file context>
@@ -139,16 +139,19 @@ export async function createWorkspaceKernelAiItems(
 		}
 
+		const itemId = crypto.randomUUID();
+		const linkItemIds = links?.linkItemIds.filter((linkItemId) => linkItemId !== itemId);
 		let command: Awaited<ReturnType<WorkspaceKernelClient["createItem"]>>;
 
</file context>

@urjitc
urjitc force-pushed the feat/item-links branch from 17cfe79 to 3900226 Compare July 3, 2026 03:03

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Added 1 comment

Comment thread src/features/workspaces/operations/workspace-tool-schemas.ts Outdated
@urjitc
urjitc force-pushed the feat/item-links branch from 3900226 to c6ebcbc Compare July 3, 2026 03:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/features/workspaces/kernel/workspace-kernel-relations.ts (1)

77-85: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Hard failure on unrecognized/legacy kind values.

workspaceRelationKindSchema.parse(row.kind) throws if a stored row's kind no longer matches workspaceRelationKindValues (e.g. a future rename or removal of an enum value). Since this runs inside listItemRelations, one stale row would break reads for the entire item rather than degrading gracefully.

🛡️ Proposed defensive fallback
 function mapKernelRelationRow(row: KernelRelationRow): WorkspaceKernelRelation {
+	const parsedKind = workspaceRelationKindSchema.safeParse(row.kind);
+
 	return {
 		id: row.id,
 		fromItemId: row.from_item_id,
 		toItemId: row.to_item_id,
-		kind: workspaceRelationKindSchema.parse(row.kind),
+		kind: parsedKind.success ? parsedKind.data : (row.kind as WorkspaceRelationKind),
 		note: row.note || null,
 	};
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/kernel/workspace-kernel-relations.ts` around lines 77
- 85, `mapKernelRelationRow` is hard-failing on legacy or unrecognized `kind`
values because `workspaceRelationKindSchema.parse(row.kind)` throws inside
`listItemRelations`. Update the mapping to handle invalid `row.kind` defensively
by validating with `workspaceRelationKindSchema` and falling back to a safe
value or returning a nullable/filtered relation instead of throwing, so one
stale row does not break reads for the whole item.
src/features/workspaces/operations/read-items.ts (1)

118-148: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential per-item read + relations fetch adds avoidable latency.

readWorkspaceItem and kernel.listItemRelations are independent kernel calls but are awaited sequentially per path. For multi-path reads this compounds linearly with input.paths.length.

⚡ Suggested fix
-			const item = await readWorkspaceItem({
-				item: resolution.item,
-				kernel: workspaceContext.kernel,
-				pages: input.pages,
-				path: resolution.path,
-			});
-			const relations = serializeWorkspaceRelations({
-				item: resolution.item,
-				pathsByItemId,
-				relations: await workspaceContext.kernel.listItemRelations({
-					itemId: resolution.item.id,
-				}),
-			});
+			const [item, kernelRelations] = await Promise.all([
+				readWorkspaceItem({
+					item: resolution.item,
+					kernel: workspaceContext.kernel,
+					pages: input.pages,
+					path: resolution.path,
+				}),
+				workspaceContext.kernel.listItemRelations({ itemId: resolution.item.id }),
+			]);
+			const relations = serializeWorkspaceRelations({
+				item: resolution.item,
+				pathsByItemId,
+				relations: kernelRelations,
+			});

Longer-term, consider a bulk listItemRelations kernel call keyed by multiple item ids to also eliminate the cross-path N+1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/operations/read-items.ts` around lines 118 - 148, The
per-item fetch in read-items.ts is doing two independent kernel calls
sequentially, which adds avoidable latency. In the read loop, update the
readWorkspaceItem and kernel.listItemRelations flow so both requests are started
together and awaited together for each resolution, while preserving the existing
WorkspacePageSelectionError handling and result.items/result.failed behavior.
Use the existing readWorkspaceItem, workspaceContext.kernel.listItemRelations,
and serializeWorkspaceRelations symbols to locate the logic; if needed, also
consider a future bulk relation fetch to reduce the N+1 pattern across multiple
paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/workspaces/operations/create-items.ts`:
- Around line 171-174: The unguarded workspaceContext.kernel.createRelations
call in createWorkspaceItemsOperation can throw after items have already been
persisted, causing the whole operation to fail and lose the accumulated
items/failed results. Wrap the createRelations step in error handling, record
the failure in the existing failed array for the current item, and continue or
return a partial result instead of letting the exception escape. Keep the fix
localized around createWorkspaceItemsOperation and its relations.relations check
so the caller still receives the items and failed collections.

In `@src/features/workspaces/operations/edit-item.ts`:
- Around line 100-102: The createRelations call in editWorkspaceItemOperation is
not guarded, so a failure after the markdown edits are already applied can
escape uncaught and hide the successful edit result. Update the result.applied /
relations.relations.length branch to handle errors from
workspaceContext.kernel.createRelations explicitly, and make sure the function
reports that the edit succeeded even if relation creation fails by catching the
exception and preserving the applied state. Use editWorkspaceItemOperation and
createRelations as the key symbols when locating the fix.

In `@src/features/workspaces/operations/workspace-tool-definitions.ts`:
- Around line 162-177: The workspace_edit_item tool wiring already forwards
relations correctly, but the current workspaceEditItemInputSchema still blocks
relations-only calls because edits is required upstream. Update the input schema
and any related validation in workspace-tool-schemas.ts to make edits optional
per the PR goal, and keep defineWorkspaceTool.execute in
workspace-tool-definitions.ts compatible with edits being undefined while still
passing relations through to editWorkspaceItemOperation.

In `@src/features/workspaces/operations/workspace-tool-schemas.ts`:
- Around line 120-134: The workspaceEditItemInputSchema still requires edits,
which blocks links-only updates and contradicts the intended optional edit flow.
Update the schema in workspaceEditItemInputSchema so edits can be omitted when
relations are provided, while still enforcing that at least one of edits or
relations is present. Then adjust the downstream editWorkspaceItemOperation and
edit-item.ts handling to safely accept an undefined or empty edits value and
skip applyMarkdownEdits when no text edits are supplied.

---

Nitpick comments:
In `@src/features/workspaces/kernel/workspace-kernel-relations.ts`:
- Around line 77-85: `mapKernelRelationRow` is hard-failing on legacy or
unrecognized `kind` values because `workspaceRelationKindSchema.parse(row.kind)`
throws inside `listItemRelations`. Update the mapping to handle invalid
`row.kind` defensively by validating with `workspaceRelationKindSchema` and
falling back to a safe value or returning a nullable/filtered relation instead
of throwing, so one stale row does not break reads for the whole item.

In `@src/features/workspaces/operations/read-items.ts`:
- Around line 118-148: The per-item fetch in read-items.ts is doing two
independent kernel calls sequentially, which adds avoidable latency. In the read
loop, update the readWorkspaceItem and kernel.listItemRelations flow so both
requests are started together and awaited together for each resolution, while
preserving the existing WorkspacePageSelectionError handling and
result.items/result.failed behavior. Use the existing readWorkspaceItem,
workspaceContext.kernel.listItemRelations, and serializeWorkspaceRelations
symbols to locate the logic; if needed, also consider a future bulk relation
fetch to reduce the N+1 pattern across multiple paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 969a2732-0e11-4120-9f0b-044a8569b836

📥 Commits

Reviewing files that changed from the base of the PR and between 17cfe79 and 3900226.

📒 Files selected for processing (14)
  • src/features/workspaces/contracts.ts
  • src/features/workspaces/kernel/workspace-kernel-access.ts
  • src/features/workspaces/kernel/workspace-kernel-item-commands.ts
  • src/features/workspaces/kernel/workspace-kernel-relations.ts
  • src/features/workspaces/kernel/workspace-kernel-schema.ts
  • src/features/workspaces/kernel/workspace-kernel-types.ts
  • src/features/workspaces/kernel/workspace-kernel.ts
  • src/features/workspaces/operations/create-items.ts
  • src/features/workspaces/operations/edit-item.ts
  • src/features/workspaces/operations/read-items.ts
  • src/features/workspaces/operations/relations.ts
  • src/features/workspaces/operations/workspace-operation-context.ts
  • src/features/workspaces/operations/workspace-tool-definitions.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

🧹 Nitpick comments (2)
src/features/workspaces/kernel/workspace-kernel-relations.ts (1)

77-85: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Hard failure on unrecognized/legacy kind values.

workspaceRelationKindSchema.parse(row.kind) throws if a stored row's kind no longer matches workspaceRelationKindValues (e.g. a future rename or removal of an enum value). Since this runs inside listItemRelations, one stale row would break reads for the entire item rather than degrading gracefully.

🛡️ Proposed defensive fallback
 function mapKernelRelationRow(row: KernelRelationRow): WorkspaceKernelRelation {
+	const parsedKind = workspaceRelationKindSchema.safeParse(row.kind);
+
 	return {
 		id: row.id,
 		fromItemId: row.from_item_id,
 		toItemId: row.to_item_id,
-		kind: workspaceRelationKindSchema.parse(row.kind),
+		kind: parsedKind.success ? parsedKind.data : (row.kind as WorkspaceRelationKind),
 		note: row.note || null,
 	};
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/kernel/workspace-kernel-relations.ts` around lines 77
- 85, `mapKernelRelationRow` is hard-failing on legacy or unrecognized `kind`
values because `workspaceRelationKindSchema.parse(row.kind)` throws inside
`listItemRelations`. Update the mapping to handle invalid `row.kind` defensively
by validating with `workspaceRelationKindSchema` and falling back to a safe
value or returning a nullable/filtered relation instead of throwing, so one
stale row does not break reads for the whole item.
src/features/workspaces/operations/read-items.ts (1)

118-148: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential per-item read + relations fetch adds avoidable latency.

readWorkspaceItem and kernel.listItemRelations are independent kernel calls but are awaited sequentially per path. For multi-path reads this compounds linearly with input.paths.length.

⚡ Suggested fix
-			const item = await readWorkspaceItem({
-				item: resolution.item,
-				kernel: workspaceContext.kernel,
-				pages: input.pages,
-				path: resolution.path,
-			});
-			const relations = serializeWorkspaceRelations({
-				item: resolution.item,
-				pathsByItemId,
-				relations: await workspaceContext.kernel.listItemRelations({
-					itemId: resolution.item.id,
-				}),
-			});
+			const [item, kernelRelations] = await Promise.all([
+				readWorkspaceItem({
+					item: resolution.item,
+					kernel: workspaceContext.kernel,
+					pages: input.pages,
+					path: resolution.path,
+				}),
+				workspaceContext.kernel.listItemRelations({ itemId: resolution.item.id }),
+			]);
+			const relations = serializeWorkspaceRelations({
+				item: resolution.item,
+				pathsByItemId,
+				relations: kernelRelations,
+			});

Longer-term, consider a bulk listItemRelations kernel call keyed by multiple item ids to also eliminate the cross-path N+1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/operations/read-items.ts` around lines 118 - 148, The
per-item fetch in read-items.ts is doing two independent kernel calls
sequentially, which adds avoidable latency. In the read loop, update the
readWorkspaceItem and kernel.listItemRelations flow so both requests are started
together and awaited together for each resolution, while preserving the existing
WorkspacePageSelectionError handling and result.items/result.failed behavior.
Use the existing readWorkspaceItem, workspaceContext.kernel.listItemRelations,
and serializeWorkspaceRelations symbols to locate the logic; if needed, also
consider a future bulk relation fetch to reduce the N+1 pattern across multiple
paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/workspaces/operations/create-items.ts`:
- Around line 171-174: The unguarded workspaceContext.kernel.createRelations
call in createWorkspaceItemsOperation can throw after items have already been
persisted, causing the whole operation to fail and lose the accumulated
items/failed results. Wrap the createRelations step in error handling, record
the failure in the existing failed array for the current item, and continue or
return a partial result instead of letting the exception escape. Keep the fix
localized around createWorkspaceItemsOperation and its relations.relations check
so the caller still receives the items and failed collections.

In `@src/features/workspaces/operations/edit-item.ts`:
- Around line 100-102: The createRelations call in editWorkspaceItemOperation is
not guarded, so a failure after the markdown edits are already applied can
escape uncaught and hide the successful edit result. Update the result.applied /
relations.relations.length branch to handle errors from
workspaceContext.kernel.createRelations explicitly, and make sure the function
reports that the edit succeeded even if relation creation fails by catching the
exception and preserving the applied state. Use editWorkspaceItemOperation and
createRelations as the key symbols when locating the fix.

In `@src/features/workspaces/operations/workspace-tool-definitions.ts`:
- Around line 162-177: The workspace_edit_item tool wiring already forwards
relations correctly, but the current workspaceEditItemInputSchema still blocks
relations-only calls because edits is required upstream. Update the input schema
and any related validation in workspace-tool-schemas.ts to make edits optional
per the PR goal, and keep defineWorkspaceTool.execute in
workspace-tool-definitions.ts compatible with edits being undefined while still
passing relations through to editWorkspaceItemOperation.

In `@src/features/workspaces/operations/workspace-tool-schemas.ts`:
- Around line 120-134: The workspaceEditItemInputSchema still requires edits,
which blocks links-only updates and contradicts the intended optional edit flow.
Update the schema in workspaceEditItemInputSchema so edits can be omitted when
relations are provided, while still enforcing that at least one of edits or
relations is present. Then adjust the downstream editWorkspaceItemOperation and
edit-item.ts handling to safely accept an undefined or empty edits value and
skip applyMarkdownEdits when no text edits are supplied.

---

Nitpick comments:
In `@src/features/workspaces/kernel/workspace-kernel-relations.ts`:
- Around line 77-85: `mapKernelRelationRow` is hard-failing on legacy or
unrecognized `kind` values because `workspaceRelationKindSchema.parse(row.kind)`
throws inside `listItemRelations`. Update the mapping to handle invalid
`row.kind` defensively by validating with `workspaceRelationKindSchema` and
falling back to a safe value or returning a nullable/filtered relation instead
of throwing, so one stale row does not break reads for the whole item.

In `@src/features/workspaces/operations/read-items.ts`:
- Around line 118-148: The per-item fetch in read-items.ts is doing two
independent kernel calls sequentially, which adds avoidable latency. In the read
loop, update the readWorkspaceItem and kernel.listItemRelations flow so both
requests are started together and awaited together for each resolution, while
preserving the existing WorkspacePageSelectionError handling and
result.items/result.failed behavior. Use the existing readWorkspaceItem,
workspaceContext.kernel.listItemRelations, and serializeWorkspaceRelations
symbols to locate the logic; if needed, also consider a future bulk relation
fetch to reduce the N+1 pattern across multiple paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 969a2732-0e11-4120-9f0b-044a8569b836

📥 Commits

Reviewing files that changed from the base of the PR and between 17cfe79 and 3900226.

📒 Files selected for processing (14)
  • src/features/workspaces/contracts.ts
  • src/features/workspaces/kernel/workspace-kernel-access.ts
  • src/features/workspaces/kernel/workspace-kernel-item-commands.ts
  • src/features/workspaces/kernel/workspace-kernel-relations.ts
  • src/features/workspaces/kernel/workspace-kernel-schema.ts
  • src/features/workspaces/kernel/workspace-kernel-types.ts
  • src/features/workspaces/kernel/workspace-kernel.ts
  • src/features/workspaces/operations/create-items.ts
  • src/features/workspaces/operations/edit-item.ts
  • src/features/workspaces/operations/read-items.ts
  • src/features/workspaces/operations/relations.ts
  • src/features/workspaces/operations/workspace-operation-context.ts
  • src/features/workspaces/operations/workspace-tool-definitions.ts
  • src/features/workspaces/operations/workspace-tool-schemas.ts
🛑 Comments failed to post (4)
src/features/workspaces/operations/create-items.ts (1)

171-174: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Unguarded createRelations call after item is already persisted.

If kernel.createRelations throws here, the item was already created (durable side effect), but the exception propagates out of createWorkspaceItemsOperation uncaught, discarding the items/failed arrays accumulated so far for this and all prior loop iterations. The caller gets a hard failure with no indication that some items were actually created.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/operations/create-items.ts` around lines 171 - 174,
The unguarded workspaceContext.kernel.createRelations call in
createWorkspaceItemsOperation can throw after items have already been persisted,
causing the whole operation to fail and lose the accumulated items/failed
results. Wrap the createRelations step in error handling, record the failure in
the existing failed array for the current item, and continue or return a partial
result instead of letting the exception escape. Keep the fix localized around
createWorkspaceItemsOperation and its relations.relations check so the caller
still receives the items and failed collections.
src/features/workspaces/operations/edit-item.ts (1)

100-102: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Unguarded createRelations call after edits are already applied.

If kernel.createRelations throws here, the markdown edits were already applied (durable side effect), but the exception propagates out of editWorkspaceItemOperation uncaught, and the caller never learns the edits succeeded.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/operations/edit-item.ts` around lines 100 - 102, The
createRelations call in editWorkspaceItemOperation is not guarded, so a failure
after the markdown edits are already applied can escape uncaught and hide the
successful edit result. Update the result.applied / relations.relations.length
branch to handle errors from workspaceContext.kernel.createRelations explicitly,
and make sure the function reports that the edit succeeded even if relation
creation fails by catching the exception and preserving the applied state. Use
editWorkspaceItemOperation and createRelations as the key symbols when locating
the fix.
src/features/workspaces/operations/workspace-tool-definitions.ts (1)

162-177: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Forwarding is consistent with current schema, but inherits the edits-required gap.

execute correctly forwards relations through to editWorkspaceItemOperation, matching the current (required-edits) schema. However, since workspaceEditItemInputSchema.edits still requires .min(1) (see workspace-tool-schemas.ts), a relations-only call from the model will never reach this execute function — it will fail Zod validation upstream. Once edits is made optional per the PR objective, this destructuring/forwarding will already work correctly since edits can be undefined here without further changes needed to this file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/operations/workspace-tool-definitions.ts` around
lines 162 - 177, The workspace_edit_item tool wiring already forwards relations
correctly, but the current workspaceEditItemInputSchema still blocks
relations-only calls because edits is required upstream. Update the input schema
and any related validation in workspace-tool-schemas.ts to make edits optional
per the PR goal, and keep defineWorkspaceTool.execute in
workspace-tool-definitions.ts compatible with edits being undefined while still
passing relations through to editWorkspaceItemOperation.
src/features/workspaces/operations/workspace-tool-schemas.ts (1)

120-134: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

edits is still required, contradicting the stated "links-only edit" support.

The PR objectives state that workspace_edit_item should make edits optional so that either edits or relations can be provided, enabling links-only updates. Here edits still uses .min(1) without .optional(), so a relations-only call will fail validation before it ever reaches editWorkspaceItemOperation. This defeats the primary stated goal of the PR: enabling the AI to add/update item links without touching document content.

🐛 Proposed fix to make edits optional and require at least one of edits/relations
 export const workspaceEditItemInputSchema = z.object({
 	path: z.string().min(1).describe("Absolute path of one actual ThinkEx workspace item to edit."),
 	relations: z
 		.array(workspaceRelationInputSchema)
 		.max(20)
 		.optional()
 		.describe("Optional relationships from the edited item to existing workspace items."),
 	edits: z
 		.array(documentMarkdownEditSchema)
-		.min(1)
 		.max(40)
+		.optional()
 		.describe(
 			`Ordered text edits to apply to the item projection. ${workspaceDocumentMarkdownMathInstruction}`,
 		),
-});
+}).refine((data) => (data.edits?.length ?? 0) > 0 || (data.relations?.length ?? 0) > 0, {
+	message: "Either edits or relations must be provided.",
+});

Note this also requires downstream editWorkspaceItemOperation/edit-item.ts (not in this file set) to handle an undefined/empty edits array, since it currently calls input.edits.length unconditionally and always invokes applyMarkdownEdits.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const workspaceEditItemInputSchema = z.object({
	path: z.string().min(1).describe("Absolute path of one actual ThinkEx workspace item to edit."),
	relations: z
		.array(workspaceRelationInputSchema)
		.max(20)
		.optional()
		.describe("Optional relationships from the edited item to existing workspace items."),
	edits: z
		.array(documentMarkdownEditSchema)
		.max(40)
		.optional()
		.describe(
			`Ordered text edits to apply to the item projection. ${workspaceDocumentMarkdownMathInstruction}`,
		),
}).refine((data) => (data.edits?.length ?? 0) > 0 || (data.relations?.length ?? 0) > 0, {
	message: "Either edits or relations must be provided.",
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/workspaces/operations/workspace-tool-schemas.ts` around lines
120 - 134, The workspaceEditItemInputSchema still requires edits, which blocks
links-only updates and contradicts the intended optional edit flow. Update the
schema in workspaceEditItemInputSchema so edits can be omitted when relations
are provided, while still enforcing that at least one of edits or relations is
present. Then adjust the downstream editWorkspaceItemOperation and edit-item.ts
handling to safely accept an undefined or empty edits value and skip
applyMarkdownEdits when no text edits are supplied.

@urjitc
urjitc force-pushed the feat/item-links branch 2 times, most recently from c0ae6a8 to 6868c9b Compare July 3, 2026 03:52
@urjitc
urjitc force-pushed the feat/item-links branch from 6868c9b to 48573cf Compare July 3, 2026 03:57
@urjitc
urjitc merged commit 0824931 into ThinkEx-OSS:main Jul 3, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants