From 0ca8ed870384315db847f3a7c3ebf7f0ab85a5f6 Mon Sep 17 00:00:00 2001 From: DeepSeek Assistant Date: Thu, 5 Feb 2026 10:18:44 -0600 Subject: [PATCH 1/5] workspace/list command, RAG project context fallback, sentinel auto-injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New workspace/list command: scans .git/continuum-worktrees/ on disk, merges WorkspaceStrategy in-memory state, returns git status per workspace. Supports --personaId filter. Generated via CommandGenerator. - ProjectContextSource RAG fix: falls back to main repo when no persona-specific workspace exists (solves chicken-and-egg — personas now see codebase context before first code/* tool use). Distinguishes "Shared Repository" vs "Your Workspace" with bootstrapping hint. - SentinelAutoConfig: auto-detects build/test/lint commands from code/shell/execute and injects sentinel classification rules. Wired into PersonaToolExecutor as fire-and-forget after shell execution starts. - Git cleanup: removed 39 stale undefined/* branches, 2 stale session worktrees, fast-forwarded 3 valid ai/ worktrees to HEAD. --- src/debug/jtag/browser/generated.ts | 8 +- .../verify/server/CodeVerifyServerCommand.ts | 47 ++-- .../git/push/server/GitPushServerCommand.ts | 5 +- .../git/shared/resolveWorkspacePath.ts | 39 +++ .../status/server/GitStatusServerCommand.ts | 5 +- .../server/GitWorkspaceCleanServerCommand.ts | 5 +- .../server/GitWorkspaceInitServerCommand.ts | 4 +- .../jtag/commands/workspace/list/.npmignore | 20 ++ .../jtag/commands/workspace/list/README.md | 165 +++++++++++ .../browser/WorkspaceListBrowserCommand.ts | 21 ++ .../jtag/commands/workspace/list/package.json | 35 +++ .../list/server/WorkspaceListServerCommand.ts | 209 ++++++++++++++ .../list/shared/WorkspaceListTypes.ts | 135 +++++++++ .../WorkspaceListIntegration.test.ts | 196 +++++++++++++ .../test/unit/WorkspaceListCommand.test.ts | 259 ++++++++++++++++++ .../adapters/groq/shared/GroqAdapter.ts | 8 +- .../shared/AICapabilityRegistry.ts | 4 +- .../adapters/BaseOpenAICompatibleAdapter.ts | 4 + .../shared/ArtifactsDaemon.ts | 2 +- .../server/SessionDaemonServer.ts | 3 +- .../user-daemon/server/UserDaemonServer.ts | 5 +- src/debug/jtag/generated-command-schemas.json | 18 +- .../jtag/generator/specs/workspace-list.json | 48 ++++ src/debug/jtag/package-lock.json | 4 +- src/debug/jtag/package.json | 2 +- src/debug/jtag/server/generated.ts | 8 +- .../shared/generated-command-constants.ts | 1 + src/debug/jtag/shared/version.ts | 2 +- .../system/code/server/SentinelAutoConfig.ts | 150 ++++++++++ .../jtag/system/code/server/Workspace.ts | 1 + .../system/code/server/WorkspaceStrategy.ts | 32 +-- .../rag/sources/ProjectContextSource.ts | 135 ++++++--- .../directory/server/UserDirectoryManager.ts | 7 +- .../jtag/system/user/server/PersonaUser.ts | 4 +- .../modules/PersonaResponseGenerator.ts | 23 +- .../server/modules/PersonaToolExecutor.ts | 19 ++ .../user/server/modules/ToolFormatAdapter.ts | 11 +- .../integration/persona-user-storage.test.ts | 13 +- 38 files changed, 1526 insertions(+), 131 deletions(-) create mode 100644 src/debug/jtag/commands/workspace/git/shared/resolveWorkspacePath.ts create mode 100644 src/debug/jtag/commands/workspace/list/.npmignore create mode 100644 src/debug/jtag/commands/workspace/list/README.md create mode 100644 src/debug/jtag/commands/workspace/list/browser/WorkspaceListBrowserCommand.ts create mode 100644 src/debug/jtag/commands/workspace/list/package.json create mode 100644 src/debug/jtag/commands/workspace/list/server/WorkspaceListServerCommand.ts create mode 100644 src/debug/jtag/commands/workspace/list/shared/WorkspaceListTypes.ts create mode 100644 src/debug/jtag/commands/workspace/list/test/integration/WorkspaceListIntegration.test.ts create mode 100644 src/debug/jtag/commands/workspace/list/test/unit/WorkspaceListCommand.test.ts create mode 100644 src/debug/jtag/generator/specs/workspace-list.json create mode 100644 src/debug/jtag/system/code/server/SentinelAutoConfig.ts diff --git a/src/debug/jtag/browser/generated.ts b/src/debug/jtag/browser/generated.ts index a526bda02..4b383c439 100644 --- a/src/debug/jtag/browser/generated.ts +++ b/src/debug/jtag/browser/generated.ts @@ -1,7 +1,7 @@ /** * Browser Structure Registry - Auto-generated * - * Contains 11 daemons and 186 commands and 2 adapters and 28 widgets. + * Contains 11 daemons and 187 commands and 2 adapters and 28 widgets. * Generated by scripts/generate-structure.ts - DO NOT EDIT MANUALLY */ @@ -200,6 +200,7 @@ import { GitPushBrowserCommand } from './../commands/workspace/git/push/browser/ import { GitStatusBrowserCommand } from './../commands/workspace/git/status/browser/GitStatusBrowserCommand'; import { GitWorkspaceCleanBrowserCommand } from './../commands/workspace/git/workspace/clean/browser/GitWorkspaceCleanBrowserCommand'; import { GitWorkspaceInitBrowserCommand } from './../commands/workspace/git/workspace/init/browser/GitWorkspaceInitBrowserCommand'; +import { WorkspaceListBrowserCommand } from './../commands/workspace/list/browser/WorkspaceListBrowserCommand'; import { RecipeLoadBrowserCommand } from './../commands/workspace/recipe/load/browser/RecipeLoadBrowserCommand'; import { TaskCompleteBrowserCommand } from './../commands/workspace/task/complete/browser/TaskCompleteBrowserCommand'; import { TaskCreateBrowserCommand } from './../commands/workspace/task/create/browser/TaskCreateBrowserCommand'; @@ -1213,6 +1214,11 @@ export const BROWSER_COMMANDS: CommandEntry[] = [ className: 'GitWorkspaceInitBrowserCommand', commandClass: GitWorkspaceInitBrowserCommand }, +{ + name: 'workspace/list', + className: 'WorkspaceListBrowserCommand', + commandClass: WorkspaceListBrowserCommand + }, { name: 'workspace/recipe/load', className: 'RecipeLoadBrowserCommand', diff --git a/src/debug/jtag/commands/code/verify/server/CodeVerifyServerCommand.ts b/src/debug/jtag/commands/code/verify/server/CodeVerifyServerCommand.ts index f69fe8691..03c356c88 100644 --- a/src/debug/jtag/commands/code/verify/server/CodeVerifyServerCommand.ts +++ b/src/debug/jtag/commands/code/verify/server/CodeVerifyServerCommand.ts @@ -82,43 +82,36 @@ export class CodeVerifyServerCommand extends CommandBase challengeIdStart + uuidLen + 1) { - const actualPersonaId = personaId.slice(-(uuidLen)); - const challengeId = personaId.slice(challengeIdStart, personaId.length - uuidLen - 1); - const challengeDir = path.join(jtagRoot, '.continuum', 'personas', actualPersonaId, 'challenges', challengeId); - if (fs.existsSync(challengeDir)) { - return challengeDir; + const personasRoot = path.join(jtagRoot, '.continuum', 'personas'); + + if (fs.existsSync(personasRoot)) { + try { + const entries = fs.readdirSync(personasRoot, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const wsDir = path.join(personasRoot, entry.name, 'workspace'); + if (fs.existsSync(wsDir)) { + // For sandbox mode, the handle IS the personaId. Check if this directory + // was registered for this handle by looking for any content. + return wsDir; + } } - } + } catch { /* fallthrough */ } } - // Last resort: use the standard workspace path even if it doesn't exist yet - return workspaceDir; + // Absolute last resort — construct path and hope for the best + return path.join(personasRoot, params.userId!, 'workspace'); } /** diff --git a/src/debug/jtag/commands/workspace/git/push/server/GitPushServerCommand.ts b/src/debug/jtag/commands/workspace/git/push/server/GitPushServerCommand.ts index 1ba5f5c95..848f313c4 100644 --- a/src/debug/jtag/commands/workspace/git/push/server/GitPushServerCommand.ts +++ b/src/debug/jtag/commands/workspace/git/push/server/GitPushServerCommand.ts @@ -8,6 +8,7 @@ import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared import type { JTAGContext } from '@system/core/types/JTAGTypes'; import type { GitPushParams, GitPushResult } from '../shared/GitPushTypes'; import { createGitPushResultFromParams } from '../shared/GitPushTypes'; +import { resolveWorkspacePathFromUserId } from '../../shared/resolveWorkspacePath'; import * as path from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; @@ -24,9 +25,7 @@ export class GitPushServerCommand extends CommandBase { try { const userId = params.userId || 'unknown'; - const workspacePath = params.workspacePath || path.resolve( - process.cwd(), '.continuum/sessions/user/shared', userId, 'workspace' - ); + const workspacePath = params.workspacePath || await resolveWorkspacePathFromUserId(userId); if (!fs.existsSync(workspacePath)) { throw new Error(`Workspace not found at ${workspacePath}`); diff --git a/src/debug/jtag/commands/workspace/git/shared/resolveWorkspacePath.ts b/src/debug/jtag/commands/workspace/git/shared/resolveWorkspacePath.ts new file mode 100644 index 000000000..d3a51ff2b --- /dev/null +++ b/src/debug/jtag/commands/workspace/git/shared/resolveWorkspacePath.ts @@ -0,0 +1,39 @@ +/** + * resolveWorkspacePath - Resolve workspace path from userId (UUID) + * + * Shared utility for git workspace commands that need to find a persona's + * workspace directory. Looks up the user entity to get the human-readable + * uniqueId, then constructs the path using that (not the UUID). + * + * Path convention: .continuum/sessions/user/shared/{uniqueId}/workspace + */ + +import { DataDaemon } from '@daemons/data-daemon/shared/DataDaemon'; +import { COLLECTIONS } from '@system/data/config/DatabaseConfig'; +import type { UserEntity } from '@system/data/entities/UserEntity'; +import * as path from 'path'; + +/** + * Resolve workspace path from a userId (UUID). + * Looks up the user entity to get uniqueId for human-readable directory naming. + * Falls back to userId if entity lookup fails. + */ +export async function resolveWorkspacePathFromUserId(userId: string): Promise { + let dirName = userId; // fallback to UUID if lookup fails + + try { + const entity = await DataDaemon.read(COLLECTIONS.USERS, userId); + if (entity?.uniqueId) { + dirName = entity.uniqueId; + } + } catch { + // Entity lookup failed — use UUID as fallback + } + + return path.resolve( + process.cwd(), + '.continuum/sessions/user/shared', + dirName, + 'workspace', + ); +} diff --git a/src/debug/jtag/commands/workspace/git/status/server/GitStatusServerCommand.ts b/src/debug/jtag/commands/workspace/git/status/server/GitStatusServerCommand.ts index f42623271..b14defaf6 100644 --- a/src/debug/jtag/commands/workspace/git/status/server/GitStatusServerCommand.ts +++ b/src/debug/jtag/commands/workspace/git/status/server/GitStatusServerCommand.ts @@ -8,6 +8,7 @@ import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared import type { JTAGContext } from '@system/core/types/JTAGTypes'; import type { GitStatusParams, GitStatusResult } from '../shared/GitStatusTypes'; import { createGitStatusResultFromParams } from '../shared/GitStatusTypes'; +import { resolveWorkspacePathFromUserId } from '../../shared/resolveWorkspacePath'; import * as path from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; @@ -24,9 +25,7 @@ export class GitStatusServerCommand extends CommandBase { try { const userId = params.userId || 'unknown'; - const workspacePath = params.workspacePath || path.resolve( - process.cwd(), '.continuum/sessions/user/shared', userId, 'workspace' - ); + const workspacePath = params.workspacePath || await resolveWorkspacePathFromUserId(userId); if (!fs.existsSync(workspacePath)) { throw new Error(`Workspace not found at ${workspacePath}`); diff --git a/src/debug/jtag/commands/workspace/git/workspace/clean/server/GitWorkspaceCleanServerCommand.ts b/src/debug/jtag/commands/workspace/git/workspace/clean/server/GitWorkspaceCleanServerCommand.ts index 9e78d9548..469d8bae5 100644 --- a/src/debug/jtag/commands/workspace/git/workspace/clean/server/GitWorkspaceCleanServerCommand.ts +++ b/src/debug/jtag/commands/workspace/git/workspace/clean/server/GitWorkspaceCleanServerCommand.ts @@ -8,6 +8,7 @@ import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared import type { JTAGContext } from '@system/core/types/JTAGTypes'; import type { GitWorkspaceCleanParams, GitWorkspaceCleanResult } from '../shared/GitWorkspaceCleanTypes'; import { createGitWorkspaceCleanResultFromParams } from '../shared/GitWorkspaceCleanTypes'; +import { resolveWorkspacePathFromUserId } from '../../../shared/resolveWorkspacePath'; import * as path from 'path'; import * as fs from 'fs'; import { promisify } from 'util'; @@ -24,9 +25,7 @@ export class GitWorkspaceCleanServerCommand extends CommandBase { try { const userId = params.userId || 'unknown'; - const workspacePath = params.workspacePath || path.resolve( - process.cwd(), '.continuum/sessions/user/shared', userId, 'workspace' - ); + const workspacePath = params.workspacePath || await resolveWorkspacePathFromUserId(userId); if (!fs.existsSync(workspacePath)) { return createGitWorkspaceCleanResultFromParams(params, { diff --git a/src/debug/jtag/commands/workspace/git/workspace/init/server/GitWorkspaceInitServerCommand.ts b/src/debug/jtag/commands/workspace/git/workspace/init/server/GitWorkspaceInitServerCommand.ts index 6e26122d2..b8a58fe72 100644 --- a/src/debug/jtag/commands/workspace/git/workspace/init/server/GitWorkspaceInitServerCommand.ts +++ b/src/debug/jtag/commands/workspace/git/workspace/init/server/GitWorkspaceInitServerCommand.ts @@ -56,11 +56,11 @@ export class GitWorkspaceInitServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('workspace/list', context, subpath, commander); + } + + async execute(params: WorkspaceListParams): Promise { + console.log('🌐 BROWSER: Delegating Workspace List to server'); + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/workspace/list/package.json b/src/debug/jtag/commands/workspace/list/package.json new file mode 100644 index 000000000..d7f1f340b --- /dev/null +++ b/src/debug/jtag/commands/workspace/list/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/workspace/list", + "version": "1.0.0", + "description": "List all persona workspaces across the team — worktree paths, git branches, modified files, shell activity. Scans both in-memory active workspaces and persisted git worktrees on disk.", + "main": "server/WorkspaceListServerCommand.ts", + "types": "shared/WorkspaceListTypes.ts", + "scripts": { + "test": "npm run test:unit && npm run test:integration", + "test:unit": "npx vitest run test/unit/*.test.ts", + "test:integration": "npx tsx test/integration/WorkspaceListIntegration.test.ts", + "lint": "npx eslint **/*.ts", + "typecheck": "npx tsc --noEmit" + }, + "peerDependencies": { + "@jtag/core": "*" + }, + "files": [ + "shared/**/*.ts", + "browser/**/*.ts", + "server/**/*.ts", + "test/**/*.ts", + "README.md" + ], + "keywords": [ + "jtag", + "command", + "workspace/list" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/workspace/list/server/WorkspaceListServerCommand.ts b/src/debug/jtag/commands/workspace/list/server/WorkspaceListServerCommand.ts new file mode 100644 index 000000000..488a8bd9c --- /dev/null +++ b/src/debug/jtag/commands/workspace/list/server/WorkspaceListServerCommand.ts @@ -0,0 +1,209 @@ +/** + * Workspace List Command - Server Implementation + * + * Discovers ALL persona workspaces by scanning: + * 1. On-disk git worktrees at .git/continuum-worktrees/{personaId}/{slug}/ + * 2. In-memory active workspaces from WorkspaceStrategy + * + * For each discovered workspace, optionally queries git status (branch, + * modified files, staged files, commits ahead, HEAD info). + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { WorkspaceListParams, WorkspaceListResult, WorkspaceInfo, WorkspaceGitStatus } from '../shared/WorkspaceListTypes'; +import { createWorkspaceListResultFromParams } from '../shared/WorkspaceListTypes'; +import { WorkspaceStrategy } from '../../../../system/code/server/WorkspaceStrategy'; +import * as fs from 'fs'; +import * as path from 'path'; +import { execSync } from 'child_process'; + +export class WorkspaceListServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('workspace/list', context, subpath, commander); + } + + async execute(params: WorkspaceListParams): Promise { + const includeGitStatus = params.includeGitStatus !== false; // default true + const filterPersona = params.personaId || undefined; + + // Phase 1: Scan on-disk worktrees + const diskWorkspaces = this.scanDiskWorktrees(filterPersona); + + // Phase 2: Merge in-memory active state + const activeHandles = new Set(); + for (const [_handle, meta] of WorkspaceStrategy.allProjectWorkspaces) { + activeHandles.add(meta.worktreeDir); + } + + // Mark workspaces as active if they're in the in-memory map + for (const ws of diskWorkspaces) { + ws.active = activeHandles.has(ws.worktreeDir); + } + + // Phase 3: Add any in-memory workspaces not found on disk (sandbox mode, etc.) + for (const [_handle, meta] of WorkspaceStrategy.allProjectWorkspaces) { + const alreadyListed = diskWorkspaces.some(w => w.worktreeDir === meta.worktreeDir); + if (!alreadyListed) { + // Extract personaId from worktreeDir path convention: + // .git/continuum-worktrees/{personaId}/{slug} + const parts = meta.worktreeDir.split(path.sep); + const worktreeIdx = parts.indexOf('continuum-worktrees'); + const personaId = worktreeIdx >= 0 ? parts[worktreeIdx + 1] : 'unknown'; + const slug = worktreeIdx >= 0 ? parts[worktreeIdx + 2] : 'unknown'; + + if (filterPersona && personaId !== filterPersona) continue; + + diskWorkspaces.push({ + personaId, + taskSlug: slug, + worktreeDir: meta.worktreeDir, + branch: meta.branch, + active: true, + mode: 'project', + }); + } + } + + // Phase 4: Optionally get git status for each workspace + if (includeGitStatus) { + const statusPromises = diskWorkspaces.map(ws => this.getGitStatus(ws)); + await Promise.all(statusPromises); + } + + // Sort: active first, then by persona name + diskWorkspaces.sort((a, b) => { + if (a.active !== b.active) return a.active ? -1 : 1; + return a.personaId.localeCompare(b.personaId); + }); + + const activeCount = diskWorkspaces.filter(w => w.active).length; + + return createWorkspaceListResultFromParams(params, { + success: true, + workspaces: diskWorkspaces, + totalCount: diskWorkspaces.length, + activeCount, + }); + } + + /** + * Scan .git/continuum-worktrees/ for on-disk worktrees. + * Directory convention: .git/continuum-worktrees/{personaUniqueId}/{taskSlug}/ + */ + private scanDiskWorktrees(filterPersona?: string): WorkspaceInfo[] { + const worktreeRoot = path.resolve(process.cwd(), '..', '..', '..', '.git', 'continuum-worktrees'); + + // Also check the main repo .git/continuum-worktrees (resolve from git root) + let gitRoot: string; + try { + gitRoot = execSync('git rev-parse --show-toplevel', { + cwd: process.cwd(), + stdio: 'pipe', + timeout: 3000, + }).toString().trim(); + } catch { + gitRoot = process.cwd(); + } + + const worktreeDirs = [ + path.join(gitRoot, '.git', 'continuum-worktrees'), + ]; + + const workspaces: WorkspaceInfo[] = []; + + for (const baseDir of worktreeDirs) { + if (!fs.existsSync(baseDir)) continue; + + let personaDirs: string[]; + try { + personaDirs = fs.readdirSync(baseDir).filter(entry => { + const entryPath = path.join(baseDir, entry); + return fs.statSync(entryPath).isDirectory(); + }); + } catch { + continue; + } + + for (const personaDir of personaDirs) { + if (filterPersona && personaDir !== filterPersona) continue; + + const personaPath = path.join(baseDir, personaDir); + let slugDirs: string[]; + try { + slugDirs = fs.readdirSync(personaPath).filter(entry => { + const entryPath = path.join(personaPath, entry); + return fs.statSync(entryPath).isDirectory(); + }); + } catch { + continue; + } + + for (const slug of slugDirs) { + const worktreeDir = path.join(personaPath, slug); + + // Validate it's actually a git worktree (has a .git file/dir) + const gitRef = path.join(worktreeDir, '.git'); + if (!fs.existsSync(gitRef)) continue; + + // Read branch from the worktree + let branch = ''; + try { + branch = execSync('git branch --show-current', { + cwd: worktreeDir, + stdio: 'pipe', + timeout: 3000, + }).toString().trim(); + } catch { + branch = 'detached'; + } + + workspaces.push({ + personaId: personaDir, + taskSlug: slug, + worktreeDir, + branch, + active: false, // Will be set in phase 2 + mode: 'project', + }); + } + } + } + + return workspaces; + } + + /** + * Populate git status on a workspace info object (mutates in place). + */ + private async getGitStatus(ws: WorkspaceInfo): Promise { + const dir = ws.worktreeDir; + if (!fs.existsSync(dir)) return; + + const gitExec = (cmd: string): string => { + try { + return execSync(cmd, { cwd: dir, stdio: 'pipe', timeout: 5000 }).toString().trim(); + } catch { + return ''; + } + }; + + const modified = gitExec('git diff --name-only').split('\n').filter(Boolean); + const staged = gitExec('git diff --cached --name-only').split('\n').filter(Boolean); + const untracked = gitExec('git ls-files --others --exclude-standard').split('\n').filter(Boolean); + const aheadStr = gitExec('git rev-list --count @{u}..HEAD 2>/dev/null || echo "0"'); + const commitsAhead = parseInt(aheadStr) || 0; + const headCommit = gitExec('git log -1 --format=%h'); + const headMessage = gitExec('git log -1 --format=%s'); + + ws.git = { + modified, + staged, + untracked, + commitsAhead, + headCommit, + headMessage, + }; + } +} diff --git a/src/debug/jtag/commands/workspace/list/shared/WorkspaceListTypes.ts b/src/debug/jtag/commands/workspace/list/shared/WorkspaceListTypes.ts new file mode 100644 index 000000000..09d39e0a3 --- /dev/null +++ b/src/debug/jtag/commands/workspace/list/shared/WorkspaceListTypes.ts @@ -0,0 +1,135 @@ +/** + * Workspace List Command - Shared Types + * + * List all persona workspaces across the team — worktree paths, git branches, + * modified files, shell activity. Scans both in-memory active workspaces and + * persisted git worktrees on disk. + */ + +import type { CommandParams, CommandResult, CommandInput, JTAGContext } from '@system/core/types/JTAGTypes'; +import { createPayload, transformPayload } from '@system/core/types/JTAGTypes'; +import { Commands } from '@system/core/shared/Commands'; +import type { JTAGError } from '@system/core/types/ErrorTypes'; +import type { UUID } from '@system/core/types/CrossPlatformUUID'; + +/** + * Workspace List Command Parameters + */ +export interface WorkspaceListParams extends CommandParams { + /** Filter to a specific persona's workspaces (by uniqueId). If omitted, returns all. */ + personaId?: string; + /** Include git status (branch, modified files, staged) for each workspace. Defaults to true. */ + includeGitStatus?: boolean; +} + +/** + * Per-workspace information returned by workspace/list. + */ +export interface WorkspaceInfo { + /** Persona uniqueId that owns this workspace (e.g., 'deepseek', 'together') */ + personaId: string; + /** Task slug within the persona's worktree directory */ + taskSlug: string; + /** Absolute path to the worktree directory */ + worktreeDir: string; + /** Git branch checked out in this worktree */ + branch: string; + /** Whether this workspace is currently active in the server's in-memory state */ + active: boolean; + /** Workspace mode (detected from structure) */ + mode: 'project' | 'worktree' | 'sandbox' | 'unknown'; + /** Git status (populated when includeGitStatus is true) */ + git?: WorkspaceGitStatus; +} + +/** + * Git status for a single workspace. + */ +export interface WorkspaceGitStatus { + /** List of modified (unstaged) files */ + modified: string[]; + /** List of staged files */ + staged: string[]; + /** List of untracked files */ + untracked: string[]; + /** Number of commits ahead of the tracking branch */ + commitsAhead: number; + /** HEAD commit short hash */ + headCommit: string; + /** HEAD commit message (first line) */ + headMessage: string; +} + +/** + * Factory function for creating WorkspaceListParams + */ +export const createWorkspaceListParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + personaId?: string; + includeGitStatus?: boolean; + } +): WorkspaceListParams => createPayload(context, sessionId, { + personaId: data.personaId ?? '', + includeGitStatus: data.includeGitStatus ?? true, + ...data +}); + +/** + * Workspace List Command Result + */ +export interface WorkspaceListResult extends CommandResult { + success: boolean; + /** Array of workspace info objects for each discovered workspace */ + workspaces: WorkspaceInfo[]; + /** Total number of workspaces found */ + totalCount: number; + /** Number of workspaces currently active in memory (server session) */ + activeCount: number; + error?: JTAGError; +} + +/** + * Factory function for creating WorkspaceListResult with defaults + */ +export const createWorkspaceListResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + workspaces?: WorkspaceInfo[]; + totalCount?: number; + activeCount?: number; + error?: JTAGError; + } +): WorkspaceListResult => createPayload(context, sessionId, { + workspaces: data.workspaces ?? [], + totalCount: data.totalCount ?? 0, + activeCount: data.activeCount ?? 0, + ...data +}); + +/** + * Smart Workspace List-specific inheritance from params + * Auto-inherits context and sessionId from params + * Must provide all required result fields + */ +export const createWorkspaceListResultFromParams = ( + params: WorkspaceListParams, + differences: Omit +): WorkspaceListResult => transformPayload(params, differences); + +/** + * WorkspaceList — Type-safe command executor + * + * Usage: + * import { WorkspaceList } from '...shared/WorkspaceListTypes'; + * const result = await WorkspaceList.execute({ ... }); + */ +export const WorkspaceList = { + execute(params: CommandInput): Promise { + return Commands.execute('workspace/list', params as Partial); + }, + commandName: 'workspace/list' as const, +} as const; diff --git a/src/debug/jtag/commands/workspace/list/test/integration/WorkspaceListIntegration.test.ts b/src/debug/jtag/commands/workspace/list/test/integration/WorkspaceListIntegration.test.ts new file mode 100644 index 000000000..05552e099 --- /dev/null +++ b/src/debug/jtag/commands/workspace/list/test/integration/WorkspaceListIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * WorkspaceList Command Integration Tests + * + * Tests Workspace List command against the LIVE RUNNING SYSTEM. + * This is NOT a mock test - it tests real commands, real events, real widgets. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Workspace List/test/integration/WorkspaceListIntegration.test.ts + * + * PREREQUISITES: + * - Server must be running: npm start (wait 90+ seconds) + * - Browser client connected via http://localhost:9003 + */ + +import { jtag } from '@server/server-index'; + +console.log('🧪 WorkspaceList Command Integration Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`✅ ${message}`); +} + +/** + * Test 1: Connect to live system + */ +async function testSystemConnection(): Promise>> { + console.log('\n🔌 Test 1: Connecting to live JTAG system'); + + const client = await jtag.connect(); + + assert(client !== null, 'Connected to live system'); + console.log(' ✅ Connected successfully'); + + return client; +} + +/** + * Test 2: Execute Workspace List command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\n⚡ Test 2: Executing Workspace List command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Workspace List']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' 📊 Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Workspace List returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Workspace List succeeded'); + // assert(result.yourField !== undefined, 'Result has yourField'); +} + +/** + * Test 3: Validate required parameters + */ +async function testRequiredParameters(_client: Awaited>): Promise { + console.log('\n🚨 Test 3: Testing required parameter validation'); + + // TODO: Uncomment and test missing required parameters + // try { + // await _client.commands['Workspace List']({ + // // Missing required param + // }); + // assert(false, 'Should have thrown validation error'); + // } catch (error) { + // assert((error as Error).message.includes('required'), 'Error mentions required parameter'); + // console.log(' ✅ ValidationError thrown correctly'); + // } + + console.log(' ⚠️ TODO: Add required parameter validation test'); +} + +/** + * Test 4: Test optional parameters + */ +async function testOptionalParameters(_client: Awaited>): Promise { + console.log('\n🔧 Test 4: Testing optional parameters'); + + // TODO: Uncomment to test with and without optional parameters + // const withOptional = await client.commands['Workspace List']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Workspace List']({ + // requiredParam: 'test' + // }); + // + // assert(withOptional.success === true, 'Works with optional params'); + // assert(withoutOptional.success === true, 'Works without optional params'); + + console.log(' ⚠️ TODO: Add optional parameter tests'); +} + +/** + * Test 5: Performance test + */ +async function testPerformance(_client: Awaited>): Promise { + console.log('\n⚡ Test 5: Performance under load'); + + // TODO: Uncomment to test command performance + // const iterations = 10; + // const times: number[] = []; + // + // for (let i = 0; i < iterations; i++) { + // const start = Date.now(); + // await _client.commands['Workspace List']({ /* params */ }); + // times.push(Date.now() - start); + // } + // + // const avg = times.reduce((a, b) => a + b, 0) / iterations; + // const max = Math.max(...times); + // + // console.log(` Average: ${avg.toFixed(2)}ms`); + // console.log(` Max: ${max}ms`); + // + // assert(avg < 500, `Average ${avg.toFixed(2)}ms under 500ms`); + // assert(max < 1000, `Max ${max}ms under 1000ms`); + + console.log(' ⚠️ TODO: Add performance test'); +} + +/** + * Test 6: Widget/Event integration (if applicable) + */ +async function testWidgetIntegration(_client: Awaited>): Promise { + console.log('\n🎨 Test 6: Widget/Event integration'); + + // TODO: Uncomment if your command emits events or updates widgets + // Example: + // const before = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // await client.commands['Workspace List']({ /* params */ }); + // await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for event propagation + // const after = await client.commands['debug/widget-state']({ widgetSelector: 'your-widget' }); + // + // assert(after.state.someValue !== before.state.someValue, 'Widget state updated'); + + console.log(' ⚠️ TODO: Add widget/event integration test (if applicable)'); +} + +/** + * Run all integration tests + */ +async function runAllWorkspaceListIntegrationTests(): Promise { + console.log('🚀 Starting WorkspaceList Integration Tests\n'); + console.log('📋 Testing against LIVE system (not mocks)\n'); + + try { + const client = await testSystemConnection(); + await testCommandExecution(client); + await testRequiredParameters(client); + await testOptionalParameters(client); + await testPerformance(client); + await testWidgetIntegration(client); + + console.log('\n🎉 ALL WorkspaceList INTEGRATION TESTS PASSED!'); + console.log('📋 Validated:'); + console.log(' ✅ Live system connection'); + console.log(' ✅ Command execution on real system'); + console.log(' ✅ Parameter validation'); + console.log(' ✅ Optional parameter handling'); + console.log(' ✅ Performance benchmarks'); + console.log(' ✅ Widget/Event integration'); + console.log('\n💡 NOTE: This test uses the REAL running system'); + console.log(' - Real database operations'); + console.log(' - Real event propagation'); + console.log(' - Real widget updates'); + console.log(' - Real cross-daemon communication'); + + } catch (error) { + console.error('\n❌ WorkspaceList integration tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + console.error('\n💡 Make sure:'); + console.error(' 1. Server is running: npm start'); + console.error(' 2. Wait 90+ seconds for deployment'); + console.error(' 3. Browser is connected to http://localhost:9003'); + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllWorkspaceListIntegrationTests(); +} else { + module.exports = { runAllWorkspaceListIntegrationTests }; +} diff --git a/src/debug/jtag/commands/workspace/list/test/unit/WorkspaceListCommand.test.ts b/src/debug/jtag/commands/workspace/list/test/unit/WorkspaceListCommand.test.ts new file mode 100644 index 000000000..f70cc24ec --- /dev/null +++ b/src/debug/jtag/commands/workspace/list/test/unit/WorkspaceListCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * WorkspaceList Command Unit Tests + * + * Tests Workspace List command logic in isolation using mock dependencies. + * This is a REFERENCE EXAMPLE showing best practices for command testing. + * + * Generated by: ./jtag generate + * Run with: npx tsx commands/Workspace List/test/unit/WorkspaceListCommand.test.ts + * + * NOTE: This is a self-contained test (no external test utilities needed). + * Use this as a template for your own command tests. + */ + +// import { ValidationError } from '@system/core/types/ErrorTypes'; // Uncomment when adding validation tests +import { generateUUID } from '@system/core/types/CrossPlatformUUID'; +import type { WorkspaceListParams, WorkspaceListResult } from '../../shared/WorkspaceListTypes'; + +console.log('🧪 WorkspaceList Command Unit Tests'); + +function assert(condition: boolean, message: string): void { + if (!condition) { + throw new Error(`❌ Assertion failed: ${message}`); + } + console.log(`✅ ${message}`); +} + +/** + * Mock command that implements Workspace List logic for testing + */ +async function mockWorkspaceListCommand(params: WorkspaceListParams): Promise { + // TODO: Validate required parameters (BEST PRACTICE) + // Example: + // if (!params.requiredParam || params.requiredParam.trim() === '') { + // throw new ValidationError( + // 'requiredParam', + // `Missing required parameter 'requiredParam'. ` + + // `Use the help tool with 'Workspace List' or see the Workspace List README for usage information.` + // ); + // } + + // TODO: Handle optional parameters with sensible defaults + // const optionalParam = params.optionalParam ?? defaultValue; + + // TODO: Implement your command logic here + return { + success: true, + // TODO: Add your result fields with actual computed values + context: params.context, + sessionId: params.sessionId + } as WorkspaceListResult; +} + +/** + * Test 1: Command structure validation + */ +function testWorkspaceListCommandStructure(): void { + console.log('\n📋 Test 1: WorkspaceList command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Workspace List command + const validParams: WorkspaceListParams = { + // TODO: Add your required parameters here + context, + sessionId + }; + + // Validate param structure + assert(validParams.context !== undefined, 'Params have context'); + assert(validParams.sessionId !== undefined, 'Params have sessionId'); + // TODO: Add assertions for your specific parameters + // assert(typeof validParams.requiredParam === 'string', 'requiredParam is string'); +} + +/** + * Test 2: Mock command execution + */ +async function testMockWorkspaceListExecution(): Promise { + console.log('\n⚡ Test 2: Mock Workspace List command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: WorkspaceListParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockWorkspaceListCommand(params); + + // Validate result structure + assert(result.success === true, 'Mock result shows success'); + // TODO: Add assertions for your result fields + // assert(typeof result.yourField === 'string', 'yourField is string'); +} + +/** + * Test 3: Required parameter validation (CRITICAL) + * + * This test ensures your command throws ValidationError + * when required parameters are missing (BEST PRACTICE) + */ +async function testWorkspaceListRequiredParams(): Promise { + console.log('\n🚨 Test 3: Required parameter validation'); + + // TODO: Uncomment when implementing validation + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test cases that should throw ValidationError + // Example: + // const testCases = [ + // { params: {} as WorkspaceListParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as WorkspaceListParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockWorkspaceListCommand({ ...testCase.params, context, sessionId }); + // throw new Error(`Should have thrown ValidationError for: ${testCase.desc}`); + // } catch (error) { + // if (error instanceof ValidationError) { + // assert(error.field === 'requiredParam', `ValidationError field is 'requiredParam' for: ${testCase.desc}`); + // assert(error.message.includes('required parameter'), `Error message mentions 'required parameter' for: ${testCase.desc}`); + // assert(error.message.includes('help tool'), `Error message is tool-agnostic for: ${testCase.desc}`); + // } else { + // throw error; // Re-throw if not ValidationError + // } + // } + // } + + console.log('✅ All required parameter validations work correctly'); +} + +/** + * Test 4: Optional parameter handling + */ +async function testWorkspaceListOptionalParams(): Promise { + console.log('\n🔧 Test 4: Optional parameter handling'); + + // TODO: Uncomment when implementing optional param tests + // const context = { environment: 'server' as const }; + // const sessionId = generateUUID(); + + // TODO: Test WITHOUT optional param (should use default) + // const paramsWithoutOptional: WorkspaceListParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockWorkspaceListCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: WorkspaceListParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockWorkspaceListCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('✅ Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testWorkspaceListPerformance(): Promise { + console.log('\n⚡ Test 5: WorkspaceList performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockWorkspaceListCommand({ + // TODO: Add your parameters + context, + sessionId + } as WorkspaceListParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `WorkspaceList completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testWorkspaceListResultStructure(): Promise { + console.log('\n🔍 Test 6: WorkspaceList result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockWorkspaceListCommand({ + // TODO: Add your parameters + context, + sessionId + } as WorkspaceListParams); + + assert(basicResult.success === true, 'Result has success field'); + // TODO: Add assertions for your result fields + // assert(typeof basicResult.yourField === 'string', 'Result has yourField (string)'); + assert(basicResult.context === context, 'Result includes context'); + assert(basicResult.sessionId === sessionId, 'Result includes sessionId'); + + console.log('✅ All result structure validations pass'); +} + +/** + * Run all unit tests + */ +async function runAllWorkspaceListUnitTests(): Promise { + console.log('🚀 Starting WorkspaceList Command Unit Tests\n'); + + try { + testWorkspaceListCommandStructure(); + await testMockWorkspaceListExecution(); + await testWorkspaceListRequiredParams(); + await testWorkspaceListOptionalParams(); + await testWorkspaceListPerformance(); + await testWorkspaceListResultStructure(); + + console.log('\n🎉 ALL WorkspaceList UNIT TESTS PASSED!'); + console.log('📋 Validated:'); + console.log(' ✅ Command structure and parameter validation'); + console.log(' ✅ Mock command execution patterns'); + console.log(' ✅ Required parameter validation (throws ValidationError)'); + console.log(' ✅ Optional parameter handling (sensible defaults)'); + console.log(' ✅ Performance requirements (< 100ms)'); + console.log(' ✅ Result structure validation'); + console.log('\n📝 This is a REFERENCE EXAMPLE - use as a template for your commands!'); + console.log('💡 TIP: Copy this test structure and modify for your command logic'); + + } catch (error) { + console.error('\n❌ WorkspaceList unit tests failed:', (error as Error).message); + if ((error as Error).stack) { + console.error((error as Error).stack); + } + process.exit(1); + } +} + +// Run if called directly +if (require.main === module) { + void runAllWorkspaceListUnitTests(); +} else { + module.exports = { runAllWorkspaceListUnitTests }; +} diff --git a/src/debug/jtag/daemons/ai-provider-daemon/adapters/groq/shared/GroqAdapter.ts b/src/debug/jtag/daemons/ai-provider-daemon/adapters/groq/shared/GroqAdapter.ts index 4460bd660..8c23d8f43 100644 --- a/src/debug/jtag/daemons/ai-provider-daemon/adapters/groq/shared/GroqAdapter.ts +++ b/src/debug/jtag/daemons/ai-provider-daemon/adapters/groq/shared/GroqAdapter.ts @@ -45,7 +45,7 @@ export class GroqAdapter extends BaseOpenAICompatibleAdapter { capabilities: ['text-generation', 'chat'], contextWindow: 131072, supportsStreaming: true, - supportsFunctions: false + supportsFunctions: true }, { id: 'llama-3.1-8b-instant', @@ -54,7 +54,7 @@ export class GroqAdapter extends BaseOpenAICompatibleAdapter { capabilities: ['text-generation', 'chat'], contextWindow: 131072, supportsStreaming: true, - supportsFunctions: false + supportsFunctions: true }, // Mixtral family (Mistral AI) { @@ -64,7 +64,7 @@ export class GroqAdapter extends BaseOpenAICompatibleAdapter { capabilities: ['text-generation', 'chat'], contextWindow: 32768, supportsStreaming: true, - supportsFunctions: false + supportsFunctions: true }, // Gemma family (Google) { @@ -74,7 +74,7 @@ export class GroqAdapter extends BaseOpenAICompatibleAdapter { capabilities: ['text-generation', 'chat'], contextWindow: 8192, supportsStreaming: true, - supportsFunctions: false + supportsFunctions: true } ] }); diff --git a/src/debug/jtag/daemons/ai-provider-daemon/shared/AICapabilityRegistry.ts b/src/debug/jtag/daemons/ai-provider-daemon/shared/AICapabilityRegistry.ts index 57adc6c8a..1bf1a96ac 100644 --- a/src/debug/jtag/daemons/ai-provider-daemon/shared/AICapabilityRegistry.ts +++ b/src/debug/jtag/daemons/ai-provider-daemon/shared/AICapabilityRegistry.ts @@ -491,7 +491,7 @@ export class AICapabilityRegistry { this.registerProvider({ providerId: 'together', providerName: 'Together AI', - defaultCapabilities: ['text-input', 'text-output', 'streaming'], + defaultCapabilities: ['text-input', 'text-output', 'streaming', 'function-calling'], models: [ { modelId: 'meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo', @@ -545,7 +545,7 @@ export class AICapabilityRegistry { this.registerProvider({ providerId: 'groq', providerName: 'Groq', - defaultCapabilities: ['text-input', 'text-output', 'streaming'], + defaultCapabilities: ['text-input', 'text-output', 'streaming', 'function-calling'], models: [ { modelId: 'llama-3.2-90b-vision-preview', diff --git a/src/debug/jtag/daemons/ai-provider-daemon/shared/adapters/BaseOpenAICompatibleAdapter.ts b/src/debug/jtag/daemons/ai-provider-daemon/shared/adapters/BaseOpenAICompatibleAdapter.ts index ef2f9ae9d..88b9be890 100644 --- a/src/debug/jtag/daemons/ai-provider-daemon/shared/adapters/BaseOpenAICompatibleAdapter.ts +++ b/src/debug/jtag/daemons/ai-provider-daemon/shared/adapters/BaseOpenAICompatibleAdapter.ts @@ -158,6 +158,10 @@ export abstract class BaseOpenAICompatibleAdapter extends BaseAIProviderAdapter this.providerName = config.providerName; this.supportedCapabilities = config.supportedCapabilities; + // Sync base-layer timeout with adapter config timeout + // (base class defaults to 30s; adapters specify their own via config) + this.baseTimeout = config.timeout; + // Inject logger into PricingManager singleton (first adapter wins) PricingManager.getInstance().setLogger((msg: string) => this.log(null, 'warn', msg)); } diff --git a/src/debug/jtag/daemons/artifacts-daemon/shared/ArtifactsDaemon.ts b/src/debug/jtag/daemons/artifacts-daemon/shared/ArtifactsDaemon.ts index 144085d49..548087d3d 100644 --- a/src/debug/jtag/daemons/artifacts-daemon/shared/ArtifactsDaemon.ts +++ b/src/debug/jtag/daemons/artifacts-daemon/shared/ArtifactsDaemon.ts @@ -24,7 +24,7 @@ export const STORAGE_PATHS = { LOGS: '.continuum/logs', CONFIG: (homeDir: string) => `${homeDir}/.continuum`, SESSION: (sessionId: string) => `.continuum/jtag/sessions/user/${sessionId}`, - PERSONA: (personaId: string) => `${process.env.HOME}/.continuum/personas/${personaId}` + PERSONA: (personaUniqueId: string) => `${process.env.HOME}/.continuum/personas/${personaUniqueId}` } as const; // Artifacts operation types diff --git a/src/debug/jtag/daemons/session-daemon/server/SessionDaemonServer.ts b/src/debug/jtag/daemons/session-daemon/server/SessionDaemonServer.ts index 018076a0c..9794f9834 100644 --- a/src/debug/jtag/daemons/session-daemon/server/SessionDaemonServer.ts +++ b/src/debug/jtag/daemons/session-daemon/server/SessionDaemonServer.ts @@ -23,6 +23,7 @@ import { UserEntity } from '../../../system/data/entities/UserEntity'; import { UserStateEntity } from '../../../system/data/entities/UserStateEntity'; import { UserIdentityResolver } from '../../../system/user/shared/UserIdentityResolver'; import { Logger } from '../../../system/core/logging/Logger'; +import { SystemPaths } from '../../../system/core/config/SystemPaths'; import { type SessionMetadata, type CreateSessionParams, @@ -447,7 +448,7 @@ export class SessionDaemonServer extends SessionDaemon { // Create appropriate User subclass based on type let user: BaseUser; if (userEntity.type === 'persona') { - const personaDatabasePath = `.continuum/personas/${userId}/state.sqlite`; + const personaDatabasePath = SystemPaths.personas.state(userEntity.uniqueId); const storage = new SQLiteStateBackend(personaDatabasePath); user = new PersonaUser(userEntity, userState, storage); } else if (userEntity.type === 'agent') { diff --git a/src/debug/jtag/daemons/user-daemon/server/UserDaemonServer.ts b/src/debug/jtag/daemons/user-daemon/server/UserDaemonServer.ts index 7a8e36b2c..c5f5b4cd2 100644 --- a/src/debug/jtag/daemons/user-daemon/server/UserDaemonServer.ts +++ b/src/debug/jtag/daemons/user-daemon/server/UserDaemonServer.ts @@ -22,6 +22,7 @@ import { JTAGClient } from '../../../system/core/client/shared/JTAGClient'; import { JTAGClientServer } from '../../../system/core/client/server/JTAGClientServer'; import { AIDecisionLogger } from '../../../system/ai/server/AIDecisionLogger'; import { Logger, type ComponentLogger } from '../../../system/core/logging/Logger'; +import { SystemPaths } from '../../../system/core/config/SystemPaths'; export class UserDaemonServer extends UserDaemon { private static instance: UserDaemonServer | null = null; @@ -294,8 +295,8 @@ export class UserDaemonServer extends UserDaemon { throw new Error(`UserStateEntity not found for persona ${userEntity.displayName} (${userEntity.id}) - user must be created via user/create command`); } - // Initialize SQLite storage backend - const dbPath = `.continuum/personas/${userEntity.id}/state.sqlite`; + // Initialize SQLite storage backend — path via SystemPaths (single source of truth) + const dbPath = SystemPaths.personas.state(userEntity.uniqueId); const storage = new SQLiteStateBackend(dbPath); // Create JTAGClientServer for this persona via static connect method diff --git a/src/debug/jtag/generated-command-schemas.json b/src/debug/jtag/generated-command-schemas.json index bc3654777..fac7eb0b8 100644 --- a/src/debug/jtag/generated-command-schemas.json +++ b/src/debug/jtag/generated-command-schemas.json @@ -1,5 +1,5 @@ { - "generated": "2026-02-04T23:37:20.476Z", + "generated": "2026-02-05T16:14:25.713Z", "version": "1.0.0", "commands": [ { @@ -187,6 +187,22 @@ } } }, + { + "name": "workspace/list", + "description": "Workspace List Command - Shared Types\n *\n * List all persona workspaces across the team — worktree paths, git branches,\n * modified files, shell activity. Scans both in-memory active workspaces and\n * persisted git worktrees on disk.", + "params": { + "personaId": { + "type": "string", + "required": false, + "description": "personaId parameter" + }, + "includeGitStatus": { + "type": "boolean", + "required": false, + "description": "includeGitStatus parameter" + } + } + }, { "name": "workspace/git/workspace/init", "description": "Git Workspace Init Command - Shared Types\n *\n * Initialize git workspace for persona collaboration with isolated worktree", diff --git a/src/debug/jtag/generator/specs/workspace-list.json b/src/debug/jtag/generator/specs/workspace-list.json new file mode 100644 index 000000000..766456ab0 --- /dev/null +++ b/src/debug/jtag/generator/specs/workspace-list.json @@ -0,0 +1,48 @@ +{ + "name": "workspace/list", + "description": "List all persona workspaces across the team — worktree paths, git branches, modified files, shell activity. Scans both in-memory active workspaces and persisted git worktrees on disk.", + "params": [ + { + "name": "personaId", + "type": "string", + "optional": true, + "description": "Filter to a specific persona's workspaces. If omitted, returns all." + }, + { + "name": "includeGitStatus", + "type": "boolean", + "optional": true, + "description": "Include git status (branch, modified files, staged) for each workspace. Defaults to true." + } + ], + "results": [ + { + "name": "workspaces", + "type": "WorkspaceInfo[]", + "description": "Array of workspace info objects for each discovered workspace" + }, + { + "name": "totalCount", + "type": "number", + "description": "Total number of workspaces found" + }, + { + "name": "activeCount", + "type": "number", + "description": "Number of workspaces currently active in memory (server session)" + } + ], + "examples": [ + { + "description": "List all persona workspaces", + "command": "./jtag workspace/list", + "expectedResult": "{ workspaces: [...], totalCount: 3, activeCount: 1 }" + }, + { + "description": "List workspaces for a specific persona", + "command": "./jtag workspace/list --personaId=\"deepseek\"", + "expectedResult": "{ workspaces: [{ personaId: 'deepseek', branch: 'ai/deepseek-assistant/default', ... }] }" + } + ], + "accessLevel": "ai-safe" +} diff --git a/src/debug/jtag/package-lock.json b/src/debug/jtag/package-lock.json index 2a3b53d41..b022bbe1c 100644 --- a/src/debug/jtag/package-lock.json +++ b/src/debug/jtag/package-lock.json @@ -1,12 +1,12 @@ { "name": "@continuum/jtag", - "version": "1.0.7610", + "version": "1.0.7617", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@continuum/jtag", - "version": "1.0.7610", + "version": "1.0.7617", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/debug/jtag/package.json b/src/debug/jtag/package.json index b300e0aba..25208162a 100644 --- a/src/debug/jtag/package.json +++ b/src/debug/jtag/package.json @@ -1,6 +1,6 @@ { "name": "@continuum/jtag", - "version": "1.0.7610", + "version": "1.0.7617", "description": "Global CLI debugging system for any Node.js project. Install once globally, use anywhere: npm install -g @continuum/jtag", "config": { "active_example": "widget-ui", diff --git a/src/debug/jtag/server/generated.ts b/src/debug/jtag/server/generated.ts index f44847a27..84c52872f 100644 --- a/src/debug/jtag/server/generated.ts +++ b/src/debug/jtag/server/generated.ts @@ -1,7 +1,7 @@ /** * Server Structure Registry - Auto-generated * - * Contains 18 daemons and 216 commands and 3 adapters. + * Contains 18 daemons and 217 commands and 3 adapters. * Generated by scripts/generate-structure.ts - DO NOT EDIT MANUALLY */ @@ -237,6 +237,7 @@ import { GitPushServerCommand } from './../commands/workspace/git/push/server/Gi import { GitStatusServerCommand } from './../commands/workspace/git/status/server/GitStatusServerCommand'; import { GitWorkspaceCleanServerCommand } from './../commands/workspace/git/workspace/clean/server/GitWorkspaceCleanServerCommand'; import { GitWorkspaceInitServerCommand } from './../commands/workspace/git/workspace/init/server/GitWorkspaceInitServerCommand'; +import { WorkspaceListServerCommand } from './../commands/workspace/list/server/WorkspaceListServerCommand'; import { RecipeLoadServerCommand } from './../commands/workspace/recipe/load/server/RecipeLoadServerCommand'; import { TaskCompleteServerCommand } from './../commands/workspace/task/complete/server/TaskCompleteServerCommand'; import { TaskCreateServerCommand } from './../commands/workspace/task/create/server/TaskCreateServerCommand'; @@ -1405,6 +1406,11 @@ export const SERVER_COMMANDS: CommandEntry[] = [ className: 'GitWorkspaceInitServerCommand', commandClass: GitWorkspaceInitServerCommand }, +{ + name: 'workspace/list', + className: 'WorkspaceListServerCommand', + commandClass: WorkspaceListServerCommand + }, { name: 'workspace/recipe/load', className: 'RecipeLoadServerCommand', diff --git a/src/debug/jtag/shared/generated-command-constants.ts b/src/debug/jtag/shared/generated-command-constants.ts index fd407d0aa..11f742a0c 100644 --- a/src/debug/jtag/shared/generated-command-constants.ts +++ b/src/debug/jtag/shared/generated-command-constants.ts @@ -239,6 +239,7 @@ export const COMMANDS = { WORKSPACE_GIT_STATUS: 'workspace/git/status', WORKSPACE_GIT_WORKSPACE_CLEAN: 'workspace/git/workspace/clean', WORKSPACE_GIT_WORKSPACE_INIT: 'workspace/git/workspace/init', + WORKSPACE_LIST: 'workspace/list', WORKSPACE_RECIPE_LOAD: 'workspace/recipe/load', WORKSPACE_TASK_COMPLETE: 'workspace/task/complete', WORKSPACE_TASK_CREATE: 'workspace/task/create', diff --git a/src/debug/jtag/shared/version.ts b/src/debug/jtag/shared/version.ts index 2bfa0bcb5..8ff47f374 100644 --- a/src/debug/jtag/shared/version.ts +++ b/src/debug/jtag/shared/version.ts @@ -3,5 +3,5 @@ * DO NOT EDIT MANUALLY */ -export const VERSION = '1.0.7610'; +export const VERSION = '1.0.7617'; export const PACKAGE_NAME = '@continuum/jtag'; diff --git a/src/debug/jtag/system/code/server/SentinelAutoConfig.ts b/src/debug/jtag/system/code/server/SentinelAutoConfig.ts new file mode 100644 index 000000000..91631d850 --- /dev/null +++ b/src/debug/jtag/system/code/server/SentinelAutoConfig.ts @@ -0,0 +1,150 @@ +/** + * SentinelAutoConfig - Auto-generate sentinel rules for shell commands + * + * When a persona executes a shell command (code/shell/execute), this class + * detects if it's a build, test, or lint command and applies appropriate + * sentinel classification rules automatically. + * + * Sentinel rules classify output lines as Error/Warning/Info/Success/Verbose, + * enabling personas to get filtered, meaningful output instead of raw stdout/stderr. + * + * This bridges the gap between "sentinel exists" and "personas actually use it" — + * rules are auto-injected so personas get classified output without having to + * manually call code/shell/sentinel. + */ + +import { CodeDaemon } from '../../../daemons/code-daemon/shared/CodeDaemon'; +import type { SentinelRule } from '../../../shared/generated/code/SentinelRule'; +import { Logger } from '../../core/logging/Logger'; + +const log = Logger.create('SentinelAutoConfig', 'code'); + +/** + * Command category detected from the shell command string. + */ +type CommandCategory = 'build' | 'test' | 'lint' | 'unknown'; + +export class SentinelAutoConfig { + + /** + * Detect command category and apply sentinel rules if applicable. + * Fire-and-forget — caller should .catch() errors, this is non-blocking. + */ + static async applyIfApplicable( + personaId: string, + executionId: string, + cmd: string, + ): Promise { + const category = SentinelAutoConfig.detectCategory(cmd); + if (category === 'unknown') return; + + const rules = SentinelAutoConfig.rulesForCategory(category); + if (rules.length === 0) return; + + try { + const result = await CodeDaemon.shellSentinel(personaId, executionId, rules); + log.info(`Auto-sentinel applied: ${category} (${result.ruleCount} rules) for execution ${executionId.slice(0, 8)}`); + } catch (error: any) { + log.warn(`Auto-sentinel failed for ${executionId.slice(0, 8)}: ${error.message}`); + } + } + + /** + * Detect what kind of command this is from the command string. + * Pattern-matches against common build/test/lint tool invocations. + */ + static detectCategory(cmd: string): CommandCategory { + const normalized = cmd.trim().toLowerCase(); + + // Build commands + if ( + /\b(npm run build|npx tsc|tsc\b|cargo build|make\b|cmake|xcodebuild|gradle\s+build|mvn\s+(compile|package)|dotnet\s+build|go\s+build)/.test(normalized) + ) { + return 'build'; + } + + // Test commands + if ( + /\b(npm\s+test|npx\s+(vitest|jest|mocha)|vitest\b|jest\b|cargo\s+test|pytest\b|go\s+test|dotnet\s+test|gradle\s+test|mvn\s+test)/.test(normalized) + ) { + return 'test'; + } + + // Lint commands + if ( + /\b(eslint|prettier|clippy|pylint|flake8|golangci-lint|rubocop)/.test(normalized) + ) { + return 'lint'; + } + + return 'unknown'; + } + + /** + * Generate sentinel rules for a command category. + * Rules are ordered: more specific patterns first, catch-all last. + */ + static rulesForCategory(category: CommandCategory): SentinelRule[] { + switch (category) { + case 'build': + return SentinelAutoConfig.buildRules(); + case 'test': + return SentinelAutoConfig.testRules(); + case 'lint': + return SentinelAutoConfig.lintRules(); + default: + return []; + } + } + + // ──────────────────────────────────────────────────────────── + // Rule sets per category + // ──────────────────────────────────────────────────────────── + + private static buildRules(): SentinelRule[] { + return [ + // Errors — compilation failures, missing modules, type errors + { pattern: '(?i)(error|ERROR|Error TS\\d+|failed to compile|FAILED|cannot find module|fatal)', classification: 'Error', action: 'Emit' }, + // Warnings — non-fatal issues + { pattern: '(?i)(warning|WARN|Warning TS\\d+|deprecated)', classification: 'Warning', action: 'Emit' }, + // Success markers + { pattern: '(?i)(successfully compiled|built successfully|compiled successfully|build succeeded|✓)', classification: 'Success', action: 'Emit' }, + // Informational — progress, file counts + { pattern: '(?i)(compiling|building|bundling|emitting|\\d+ modules?)', classification: 'Info', action: 'Emit' }, + // Suppress noisy lines — blank lines, stack traces from node_modules + { pattern: '^\\s*$', classification: 'Verbose', action: 'Suppress' }, + { pattern: 'node_modules/', classification: 'Verbose', action: 'Suppress' }, + ]; + } + + private static testRules(): SentinelRule[] { + return [ + // Test failures + { pattern: '(?i)(FAIL|✗|✘|AssertionError|assertion failed|panicked|FAILED)', classification: 'Error', action: 'Emit' }, + // Test passes + { pattern: '(?i)(PASS|✓|✔|ok\\b|passed)', classification: 'Success', action: 'Emit' }, + // Test summary lines + { pattern: '(?i)(Tests?:|test result|Suites?:|Tests\\s+\\d+|Duration)', classification: 'Info', action: 'Emit' }, + // Warnings + { pattern: '(?i)(WARN|warning|deprecated|skipped)', classification: 'Warning', action: 'Emit' }, + // Suppress noisy lines + { pattern: '^\\s*$', classification: 'Verbose', action: 'Suppress' }, + { pattern: '^\\s+at\\s', classification: 'Verbose', action: 'Suppress' }, + ]; + } + + private static lintRules(): SentinelRule[] { + return [ + // Lint errors + { pattern: '(?i)(error|\\d+ errors?)', classification: 'Error', action: 'Emit' }, + // Lint warnings + { pattern: '(?i)(warning|\\d+ warnings?)', classification: 'Warning', action: 'Emit' }, + // File references (useful context) + { pattern: ':\\d+:\\d+', classification: 'Info', action: 'Emit' }, + // Summary + { pattern: '(?i)(problems?|fixable|\\d+ files? checked)', classification: 'Info', action: 'Emit' }, + // Suppress blank/noisy + { pattern: '^\\s*$', classification: 'Verbose', action: 'Suppress' }, + ]; + } +} diff --git a/src/debug/jtag/system/code/server/Workspace.ts b/src/debug/jtag/system/code/server/Workspace.ts index 69e57461e..d41b5d4aa 100644 --- a/src/debug/jtag/system/code/server/Workspace.ts +++ b/src/debug/jtag/system/code/server/Workspace.ts @@ -141,6 +141,7 @@ export class Workspace { userId: this.handle, typeCheck, testFiles, + cwd: this.dir, // Explicit path — don't rely on UUID-based path derivation }); } diff --git a/src/debug/jtag/system/code/server/WorkspaceStrategy.ts b/src/debug/jtag/system/code/server/WorkspaceStrategy.ts index 64f4ed4e4..c59580b48 100644 --- a/src/debug/jtag/system/code/server/WorkspaceStrategy.ts +++ b/src/debug/jtag/system/code/server/WorkspaceStrategy.ts @@ -13,6 +13,7 @@ import { Commands } from '../../core/shared/Commands'; import { CodeDaemon } from '../../../daemons/code-daemon/shared/CodeDaemon'; import { Logger } from '../../core/logging/Logger'; +import { SystemPaths } from '../../core/config/SystemPaths'; import { stringToUUID } from '../../core/types/CrossPlatformUUID'; import { execSync } from 'child_process'; import * as fs from 'fs'; @@ -27,9 +28,12 @@ const log = Logger.create('WorkspaceStrategy', 'code'); export type WorkspaceMode = 'sandbox' | 'worktree' | 'project'; export interface WorkspaceConfig { - /** Persona ID creating the workspace */ + /** Persona UUID (for handle generation + Rust backend registration) */ readonly personaId: string; + /** Human-readable persona identifier (e.g., 'together', 'deepseek') — used for ALL filesystem paths */ + readonly personaUniqueId: string; + /** Which workspace strategy to use */ readonly mode: WorkspaceMode; @@ -44,9 +48,6 @@ export interface WorkspaceConfig { /** Persona display name for git identity (project mode) */ readonly personaName?: string; - - /** Persona unique ID for git email identity (project mode) */ - readonly personaUniqueId?: string; } export interface WorkspaceResult { @@ -111,21 +112,17 @@ export class WorkspaceStrategy { /** * Create an isolated sandbox workspace (current default behavior). - * Directory: .continuum/personas/{personaId}/workspace/ + * Directory: {SystemPaths.personas.dir(uniqueId)}/workspace/ * Registered with Rust backend as writable + read-only codebase access. */ private static async createSandbox(config: WorkspaceConfig): Promise { const handle = config.personaId; + const workspaceDir = path.join(SystemPaths.personas.dir(config.personaUniqueId), 'workspace'); if (initializedWorkspaces.has(handle)) { - const jtagRoot = process.cwd(); - const workspaceDir = path.join(jtagRoot, '.continuum', 'personas', config.personaId, 'workspace'); return { handle, workspaceDir, mode: 'sandbox' }; } - const jtagRoot = process.cwd(); - const workspaceDir = path.join(jtagRoot, '.continuum', 'personas', config.personaId, 'workspace'); - // Create workspace directory if it doesn't exist if (!fs.existsSync(workspaceDir)) { fs.mkdirSync(workspaceDir, { recursive: true }); @@ -133,9 +130,10 @@ export class WorkspaceStrategy { } // Register with Rust backend — writable workspace + read-only codebase access + const jtagRoot = process.cwd(); await CodeDaemon.createWorkspace(handle, workspaceDir, [jtagRoot]); initializedWorkspaces.add(handle); - log.info(`Sandbox workspace initialized for persona ${config.personaId}`); + log.info(`Sandbox workspace initialized for persona ${config.personaUniqueId}`); return { handle, workspaceDir, mode: 'sandbox' }; } @@ -150,10 +148,10 @@ export class WorkspaceStrategy { const handle = `worktree-${config.personaId}-${slug}`; if (initializedWorkspaces.has(handle)) { - // Already initialized — resolve path from convention + // Already initialized — resolve path from convention (human-readable uniqueId) const jtagRoot = process.cwd(); const workspaceDir = path.join( - jtagRoot, '.continuum', 'sessions', 'user', 'shared', config.personaId, 'workspace', + jtagRoot, '.continuum', 'sessions', 'user', 'shared', config.personaUniqueId, 'workspace', ); return { handle, workspaceDir, mode: 'worktree' }; } @@ -234,14 +232,14 @@ export class WorkspaceStrategy { } // Branch name: ai/{personaName}/{slug} - const safeName = (config.personaName ?? config.personaId.slice(0, 8)) + const safeName = (config.personaName ?? config.personaUniqueId) .toLowerCase() .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-'); const branchName = `ai/${safeName}/${slug}`; - // Worktree directory: inside the repo's .git to keep it clean - const worktreeDir = path.join(resolvedRepoPath, '.git', 'continuum-worktrees', config.personaId, slug); + // Worktree directory: inside the repo's .git — uses uniqueId for human-readable paths + const worktreeDir = path.join(resolvedRepoPath, '.git', 'continuum-worktrees', config.personaUniqueId, slug); log.info(`Creating project workspace: repo=${resolvedRepoPath} branch=${branchName}`); @@ -293,7 +291,7 @@ export class WorkspaceStrategy { // Set local git identity in the worktree (not global) const userName = config.personaName ?? 'AI Persona'; - const userEmail = `${config.personaUniqueId ?? config.personaId}@continuum.local`; + const userEmail = `${config.personaUniqueId}@continuum.local`; const wtOpts = { cwd: worktreeDir, stdio: 'pipe' as const }; execSync(`git config user.name "${userName}"`, wtOpts); execSync(`git config user.email "${userEmail}"`, wtOpts); diff --git a/src/debug/jtag/system/rag/sources/ProjectContextSource.ts b/src/debug/jtag/system/rag/sources/ProjectContextSource.ts index e83f40af4..66c971bd4 100644 --- a/src/debug/jtag/system/rag/sources/ProjectContextSource.ts +++ b/src/debug/jtag/system/rag/sources/ProjectContextSource.ts @@ -1,23 +1,23 @@ /** * ProjectContextSource - Injects project workspace context into persona RAG * - * When a persona has an active project workspace (git worktree on any repo), - * this source surfaces: + * Provides two modes of project awareness: + * + * 1. Personal workspace — when a persona has their own git worktree (via code/* tools), + * this shows THEIR branch, THEIR changes, and team activity on the same repo. + * + * 2. Shared repository fallback — when no personal workspace exists yet, this shows + * the main repository context (branch, status, recent commits, file tree). This + * ensures personas ALWAYS have codebase awareness, even before invoking code/* tools. + * + * Context includes: * - Project type and build/test commands * - File tree (top 2 levels) * - Git branch + status (modified files, ahead/behind) * - Recent commits on this branch * - Team activity (other ai/* branches on this repo, their status) - * - Build status (last build result if tracked) - * - * This gives personas situational awareness of: - * - What they're working on (their files, their branch) - * - What the team is working on (other branches, recent commits) - * - Who might need help (merge conflicts, build failures) * * Priority 70 - Between semantic-memory (60) and conversation-history (80). - * Project context is important for coding activities but shouldn't displace - * conversation history or identity. */ import type { RAGSource, RAGSourceContext, RAGSection } from '../shared/RAGSource'; @@ -33,57 +33,80 @@ export class ProjectContextSource implements RAGSource { readonly priority = 70; readonly defaultBudgetPercent = 12; + /** Cached main repo git check (stable for process lifetime) */ + private static _isMainRepoGit: boolean | null = null; + isApplicable(context: RAGSourceContext): boolean { - // Only include if persona has an active project workspace - return !!WorkspaceStrategy.getProjectForPersona(context.personaId); + // If persona has their own project workspace, always applicable + if (WorkspaceStrategy.getProjectForPersona(context.personaId)) { + return true; + } + // Fall back: provide main repo context so personas always see the codebase + return ProjectContextSource.isMainRepoGit(); } async load(context: RAGSourceContext, allocatedBudget: number): Promise { const startTime = performance.now(); const wsMeta = WorkspaceStrategy.getProjectForPersona(context.personaId); - if (!wsMeta) { - return this.emptySection(startTime); - } + + // Resolve workspace directory — personal worktree or main repo + const workDir = wsMeta?.worktreeDir ?? process.cwd(); + const repoPath = wsMeta?.repoPath ?? process.cwd(); + const isPersonalWorkspace = !!wsMeta; try { - const gitOpts = { cwd: wsMeta.worktreeDir, stdio: 'pipe' as const, timeout: 5000 }; + // Resolve branch — from workspace metadata or live git query + let branch = wsMeta?.branch ?? ''; + if (!branch) { + try { + branch = execSync('git branch --show-current', { + cwd: workDir, stdio: 'pipe', timeout: 3000, + }).toString().trim(); + } catch { + branch = 'unknown'; + } + } - // Run git queries concurrently via Promise.all on sync operations - // These are fast (~5-10ms each) since they're local git operations + // Run git queries concurrently (all fast, local git operations) const [projectType, gitStatus, gitLog, teamBranches, fileTree] = await Promise.all([ - ProjectDetector.detect(wsMeta.worktreeDir), - this.getGitStatus(wsMeta.worktreeDir), - this.getGitLog(wsMeta.worktreeDir, 5), - this.getTeamBranches(wsMeta.repoPath), - this.getFileTree(wsMeta.worktreeDir, 2), + ProjectDetector.detect(workDir), + this.getGitStatus(workDir), + this.getGitLog(workDir, 5), + this.getTeamBranches(repoPath), + this.getFileTree(workDir, 2), ]); - // Check for team members who might need help (merge conflicts) - const teamStatus = await this.getTeamStatus(wsMeta.repoPath, wsMeta.branch); + // Team status only makes sense when persona has their own workspace + // (can't detect merge conflicts in someone else's worktree from main repo) + const teamStatus = isPersonalWorkspace + ? await this.getTeamStatus(repoPath, branch) + : []; const formatted = this.formatProjectContext({ projectType, - branch: wsMeta.branch, + branch, gitStatus, gitLog, teamBranches, teamStatus, fileTree, - repoPath: wsMeta.repoPath, + repoPath, + isPersonalWorkspace, }); // Respect budget const tokenCount = this.estimateTokens(formatted); const budgetTokens = Math.floor(allocatedBudget); const finalPrompt = tokenCount > budgetTokens - ? this.formatMinimal(wsMeta.branch, projectType, gitStatus) + ? this.formatMinimal(branch, projectType, gitStatus, isPersonalWorkspace) : formatted; const finalTokens = this.estimateTokens(finalPrompt); const loadTimeMs = performance.now() - startTime; - log.debug(`Loaded project context (${finalTokens} tokens, ${loadTimeMs.toFixed(1)}ms) for ${context.personaId.slice(0, 8)}`); + const mode = isPersonalWorkspace ? 'personal workspace' : 'shared repo'; + log.debug(`Loaded project context [${mode}] (${finalTokens} tokens, ${loadTimeMs.toFixed(1)}ms) for ${context.personaId.slice(0, 8)}`); return { sourceName: this.name, @@ -91,10 +114,11 @@ export class ProjectContextSource implements RAGSource { loadTimeMs, systemPromptSection: finalPrompt, metadata: { - branch: wsMeta.branch, - repoPath: wsMeta.repoPath, + branch, + repoPath, projectType: projectType.type, teamBranchCount: teamBranches.length, + isPersonalWorkspace, }, }; } catch (error: any) { @@ -103,6 +127,26 @@ export class ProjectContextSource implements RAGSource { } } + /** + * Check if process.cwd() is inside a git repository. + * Cached for process lifetime (repo doesn't move at runtime). + */ + private static isMainRepoGit(): boolean { + if (ProjectContextSource._isMainRepoGit === null) { + try { + execSync('git rev-parse --is-inside-work-tree', { + cwd: process.cwd(), + stdio: 'pipe', + timeout: 3000, + }); + ProjectContextSource._isMainRepoGit = true; + } catch { + ProjectContextSource._isMainRepoGit = false; + } + } + return ProjectContextSource._isMainRepoGit; + } + // ──────────────────────────────────────────────────────────── // Git data extraction (fast, synchronous operations) // ──────────────────────────────────────────────────────────── @@ -199,6 +243,7 @@ export class ProjectContextSource implements RAGSource { teamStatus: TeamMemberStatus[]; fileTree: string; repoPath: string; + isPersonalWorkspace: boolean; }): string { const sections: string[] = []; @@ -207,11 +252,28 @@ export class ProjectContextSource implements RAGSource { if (data.projectType.buildCommand) commands.push(`Build: ${data.projectType.buildCommand}`); if (data.projectType.testCommand) commands.push(`Test: ${data.projectType.testCommand}`); if (data.projectType.serveCommand) commands.push(`Serve: ${data.projectType.serveCommand}`); - sections.push(`## Project Context\nType: ${data.projectType.description}${commands.length ? ' | ' + commands.join(' | ') : ''}`); - // Your branch status + const workspaceLabel = data.isPersonalWorkspace + ? '## Your Workspace' + : '## Shared Repository'; + sections.push(`${workspaceLabel}\nType: ${data.projectType.description}${commands.length ? ' | ' + commands.join(' | ') : ''}`); + + // Branch status — distinguish personal vs shared if (data.gitStatus) { - sections.push(`### Your Branch: ${data.branch}\n${data.gitStatus}`); + const branchLabel = data.isPersonalWorkspace + ? `### Your Branch: ${data.branch}` + : `### Current Branch: ${data.branch}`; + sections.push(`${branchLabel}\n${data.gitStatus}`); + } + + // Workspace hint for shared mode — guide personas to bootstrap their workspace + if (!data.isPersonalWorkspace) { + sections.push( + `### Workspace Status\n` + + `You are viewing the shared repository. When you use code/* tools (code/read, code/write, code/shell/execute), ` + + `a personal workspace with your own git branch will be created automatically.\n` + + `Your branch will be named ai/{your-name}/work — you can commit freely without affecting the main branch.` + ); } // Recent commits @@ -246,8 +308,9 @@ export class ProjectContextSource implements RAGSource { return sections.join('\n\n'); } - private formatMinimal(branch: string, projectType: ProjectType, gitStatus: string): string { - return `## Project: ${projectType.description}\nBranch: ${branch}\n${gitStatus}`; + private formatMinimal(branch: string, projectType: ProjectType, gitStatus: string, isPersonalWorkspace: boolean): string { + const label = isPersonalWorkspace ? 'Your Workspace' : 'Shared Repository'; + return `## ${label}: ${projectType.description}\nBranch: ${branch}\n${gitStatus}`; } private emptySection(startTime: number, error?: string): RAGSection { diff --git a/src/debug/jtag/system/user/directory/server/UserDirectoryManager.ts b/src/debug/jtag/system/user/directory/server/UserDirectoryManager.ts index 9b53ef69b..e312766d7 100644 --- a/src/debug/jtag/system/user/directory/server/UserDirectoryManager.ts +++ b/src/debug/jtag/system/user/directory/server/UserDirectoryManager.ts @@ -39,15 +39,16 @@ export class UserDirectoryManager { } /** - * Get all paths for a user - * Supports legacy .continuum/personas/ paths for backward compatibility + * Get all paths for a user. + * Note: Persona directories now use uniqueId (human-readable), not UUID. + * The legacy UUID fallback checks `.continuum/personas/{userId}` for backward compat. */ getPaths(userId: UUID): UserDirectoryPaths { let root = path.join(this.baseDir, userId); // Check if new path exists if (!fs.existsSync(root)) { - // Fall back to legacy persona path if it exists + // Fall back to legacy persona path if it exists (may be UUID-named from old code) const legacyPath = path.join('.continuum/personas', userId); if (fs.existsSync(legacyPath)) { root = legacyPath; diff --git a/src/debug/jtag/system/user/server/PersonaUser.ts b/src/debug/jtag/system/user/server/PersonaUser.ts index b4f406928..2bdfe3afc 100644 --- a/src/debug/jtag/system/user/server/PersonaUser.ts +++ b/src/debug/jtag/system/user/server/PersonaUser.ts @@ -1798,10 +1798,10 @@ export class PersonaUser extends AIUser { } /** - * Get persona database path + * Get persona database path — delegates to SystemPaths (single source of truth) */ getPersonaDatabasePath(): string { - return `.continuum/personas/${this.entity.id}/state.sqlite`; + return SystemPaths.personas.state(this.entity.uniqueId); } /** diff --git a/src/debug/jtag/system/user/server/modules/PersonaResponseGenerator.ts b/src/debug/jtag/system/user/server/modules/PersonaResponseGenerator.ts index 43f55a9f0..d4449ab6b 100644 --- a/src/debug/jtag/system/user/server/modules/PersonaResponseGenerator.ts +++ b/src/debug/jtag/system/user/server/modules/PersonaResponseGenerator.ts @@ -625,7 +625,7 @@ export class PersonaResponseGenerator { // Inject available tools for autonomous tool discovery (Phase 3A) // Use adapter-based formatting for harmony with parser // CRITICAL: Only inject tools for models that can actually emit tool calls. - // Models without tool capability (groq, together, etc.) narrate instead of calling tools, + // Models without tool capability narrate instead of calling tools, // wasting tokens and clogging chat with useless "let me use tool X" text. const toolCap = getToolCapability(this.modelConfig.provider || 'candle', this.modelConfig); const availableTools = toolCap !== 'none' @@ -645,8 +645,11 @@ export class PersonaResponseGenerator { category: t.category })); - if (availableTools.length > 0) { - // Use primary adapter to format tools (harmonious with parser) + if (availableTools.length > 0 && !supportsNativeTools(this.modelConfig.provider || 'candle')) { + // Text-based tool injection for non-native providers (XML tool callers like DeepSeek). + // Native tool providers (Anthropic, OpenAI, Together, Groq) get tools via the JSON + // `tools` request parameter instead — injecting text descriptions alongside native specs + // confuses Llama models into narrating tool usage rather than calling the native API. const adapter = getPrimaryAdapter(); const formattedTools = adapter.formatToolsForPrompt(toolDefinitions); @@ -654,7 +657,7 @@ export class PersonaResponseGenerator { ================================`; systemPrompt += toolsSection; - this.log(`🔧 ${this.personaName}: Injected ${availableTools.length} available tools into context`); + this.log(`🔧 ${this.personaName}: Injected ${availableTools.length} available tools into system prompt (text format)`); } // Inject recipe activity context (strategy rules + highlighted tools) @@ -1055,7 +1058,8 @@ Remember: This is voice chat, not a written essay. Be brief, be natural, be huma } request.tools = convertToNativeToolSpecs(prioritizedTools); - this.log(`🔧 ${this.personaName}: Added ${request.tools.length} native tools for ${provider} (JSON tool_use format)`); + request.tool_choice = 'auto'; + this.log(`🔧 ${this.personaName}: Added ${request.tools.length} native tools for ${provider} (JSON tool_use format, tool_choice=auto)`); } pipelineTiming['3.2_format'] = Date.now() - phase32Start; this.log(`✅ ${this.personaName}: [PHASE 3.2] LLM messages built (${messages.length} messages, ${pipelineTiming['3.2_format']}ms)`); @@ -1186,8 +1190,11 @@ Remember: This is voice chat, not a written essay. Be brief, be natural, be huma // 🔧 PHASE 3.3.5a: GARBAGE DETECTION // Detect and reject garbage output (Unicode garbage, repetition, encoding errors) - // This catches model failures that produce gibberish instead of coherent text - const garbageCheck = GarbageDetector.isGarbage(aiResponse.text); + // This catches model failures that produce gibberish instead of coherent text. + // Skip when the response has native tool calls — models with function calling often + // return empty text + tool_calls, which is valid (the agent loop will execute them). + const hasToolCalls = aiResponse.toolCalls && aiResponse.toolCalls.length > 0; + const garbageCheck = hasToolCalls ? { isGarbage: false, reason: '', details: '', score: 0 } : GarbageDetector.isGarbage(aiResponse.text); if (garbageCheck.isGarbage) { this.log(`🗑️ ${this.personaName}: [PHASE 3.3.5a] GARBAGE DETECTED (${garbageCheck.reason}: ${garbageCheck.details})`); @@ -1224,7 +1231,7 @@ Remember: This is voice chat, not a written essay. Be brief, be natural, be huma // - Response is truncated mid-tool-call (DeepSeek's issue) // - AI repeats same content with minor variations // - Tool-level detection would miss it - if (this.isResponseLoop(aiResponse.text)) { + if (!hasToolCalls && this.isResponseLoop(aiResponse.text)) { this.log(`🔁 ${this.personaName}: [PHASE 3.3.5b] Response loop detected - DISCARDING response`); // Release inference slot diff --git a/src/debug/jtag/system/user/server/modules/PersonaToolExecutor.ts b/src/debug/jtag/system/user/server/modules/PersonaToolExecutor.ts index 1912900f2..7dd386319 100644 --- a/src/debug/jtag/system/user/server/modules/PersonaToolExecutor.ts +++ b/src/debug/jtag/system/user/server/modules/PersonaToolExecutor.ts @@ -16,6 +16,7 @@ */ import { CognitionLogger } from './cognition/CognitionLogger'; +import { SentinelAutoConfig } from '../../../code/server/SentinelAutoConfig'; import { DATA_COMMANDS } from '@commands/data/shared/DataCommandConstants'; import type { UUID } from '../../../core/types/CrossPlatformUUID'; import { generateUUID } from '../../../core/types/CrossPlatformUUID'; @@ -392,6 +393,24 @@ export class PersonaToolExecutor { error: registryResult.error }; + // Auto-inject sentinel rules for code/shell/execute commands (fire-and-forget). + // When a build/test/lint command starts, sentinel classifies output lines + // so the persona gets filtered Error/Warning/Success instead of raw stdout. + if (toolCall.toolName === 'code/shell/execute' && result.success && result.content) { + try { + const execResult = JSON.parse(result.content); + if (execResult.executionId && execResult.status === 'running') { + SentinelAutoConfig.applyIfApplicable( + context.personaId, + execResult.executionId, + toolCall.parameters.cmd || '', + ).catch(err => this.log.warn(`Sentinel auto-config failed: ${err.message}`)); + } + } catch { + // Result wasn't JSON or missing fields — skip sentinel + } + } + const duration = Date.now() - startTime; // Log result with clear visual structure diff --git a/src/debug/jtag/system/user/server/modules/ToolFormatAdapter.ts b/src/debug/jtag/system/user/server/modules/ToolFormatAdapter.ts index 6cb5eb774..9c6d78670 100644 --- a/src/debug/jtag/system/user/server/modules/ToolFormatAdapter.ts +++ b/src/debug/jtag/system/user/server/modules/ToolFormatAdapter.ts @@ -575,17 +575,18 @@ export function convertToNativeToolSpecs(tools: ToolDefinition[]): NativeToolSpe } /** - * Check if a provider supports native JSON tool calling + * Check if a provider supports native JSON tool calling. + * Together AI and Groq both implement the OpenAI-compatible function calling spec + * (tools parameter + tool_calls in response). */ export function supportsNativeTools(provider: string): boolean { - // Providers that support native tool_use JSON format - const nativeToolProviders = ['anthropic', 'openai', 'azure']; + const nativeToolProviders = ['anthropic', 'openai', 'azure', 'together', 'groq']; return nativeToolProviders.includes(provider.toLowerCase()); } /** * Tool capability tier for a given provider/model combination. - * - 'native': JSON tool_use blocks (Anthropic, OpenAI, Azure) + * - 'native': JSON tool_use blocks (Anthropic, OpenAI, Azure, Together, Groq) * - 'xml': XML tool calls parsed by ToolCallParser (DeepSeek — proven to work) * - 'none': Model narrates instead of calling tools — don't inject tools */ @@ -607,6 +608,6 @@ export function getToolCapability( const xmlCapable = ['deepseek']; if (xmlCapable.includes(provider.toLowerCase())) return 'xml'; - // Everything else: groq, together, xai, fireworks, candle, sentinel, ollama + // Everything else: xai, fireworks, candle, sentinel, ollama return 'none'; } diff --git a/src/debug/jtag/tests/integration/persona-user-storage.test.ts b/src/debug/jtag/tests/integration/persona-user-storage.test.ts index c623bddef..9540b0229 100644 --- a/src/debug/jtag/tests/integration/persona-user-storage.test.ts +++ b/src/debug/jtag/tests/integration/persona-user-storage.test.ts @@ -3,7 +3,7 @@ * * Tests that PersonaUser uses SQLiteStateBackend correctly with paranoid verification: * 1. SessionDaemon assigns SQLiteStateBackend to PersonaUser (not MemoryStateBackend) - * 2. Each PersonaUser gets dedicated SQLite path: `.continuum/personas/{personaId}/state.sqlite` + * 2. Each PersonaUser gets dedicated SQLite path via SystemPaths: `.continuum/personas/{uniqueId}/data/state.db` * 3. PersonaUser.saveState() persists to SQLite database * 4. PersonaUser.loadState() reads from SQLite database * 5. Control test: AgentUser still uses MemoryStateBackend (ephemeral) @@ -17,6 +17,7 @@ import { existsSync } from 'fs'; import { join } from 'path'; import type { UUID } from '../../system/core/types/CrossPlatformUUID'; import type { UserStateEntity } from '../../system/data/entities/UserStateEntity'; +import { SystemPaths } from '../../system/core/config/SystemPaths'; interface PersonaTestResult { readonly testName: string; @@ -98,10 +99,12 @@ function executeJtagCommand>(command: string): T { } /** - * Verify SQLite database file exists at expected path + * Verify SQLite database file exists at expected path. + * Accepts either uniqueId (human-readable) or UUID — tries SystemPaths first. */ -function verifyDatabaseFileExists(personaId: UUID): { exists: boolean; path: string } { - const expectedPath = join(process.cwd(), `.continuum/personas/${personaId}/state.sqlite`); +function verifyDatabaseFileExists(identifier: string): { exists: boolean; path: string } { + // Use SystemPaths (single source of truth for persona directory layout) + const expectedPath = SystemPaths.personas.state(identifier); const exists = existsSync(expectedPath); console.log(` 🔍 Checking database file: ${expectedPath}`); @@ -269,7 +272,7 @@ async function testPersonaUserStorage(): Promise { // TEST 3: Verify Dedicated SQLite Database Path // ============================================ console.log('📝 Test 3: Verify dedicated SQLite database file'); - console.log(` Expected path: .continuum/personas/${personaUserId}/state.sqlite\n`); + console.log(` Expected path: ${SystemPaths.personas.state(personaUserId)}\n`); const dbFile = verifyDatabaseFileExists(personaUserId); From b8918c89804ae0dd3a6b41b245090f4fac004288 Mon Sep 17 00:00:00 2001 From: Together Assistant Date: Thu, 5 Feb 2026 12:34:55 -0600 Subject: [PATCH 2/5] Rust ORM Phase 1: route observability collections through Rust data-daemon worker Wire the existing Rust data-daemon worker (Unix socket IPC) into the DataDaemon's per-collection adapter routing. 10 write-heavy observability collections now go through Rust instead of TypeScript SQLite. Key changes: - DataDaemonServer: connectRustDataWorker() waits for socket, registers RustWorkerStorageAdapter for 10 collections, process.exit(1) if worker not available (no silent fallback) - DatabaseHandleRegistry: re-enable RustWorkerStorageAdapter import and 'rust' case in open() - DefaultStorageAdapterFactory: 'rust' case returns RustWorkerStorageAdapter - Rust data-daemon worker: PRAGMA table_info column filtering in data_create/data_update (and timed variants) to handle TypeScript entities with fields that don't map to SQL columns Verified: 1113 requests, 779 creates, 0 errors. Killing the Rust worker causes writes to fail (no fallback to TypeScript SQLite). --- .../data-daemon/server/DataDaemonServer.ts | 76 +++++++++++++++ .../server/DatabaseHandleRegistry.ts | 30 +++++- .../server/DefaultStorageAdapterFactory.ts | 6 +- src/debug/jtag/generated-command-schemas.json | 2 +- src/debug/jtag/package-lock.json | 4 +- src/debug/jtag/package.json | 2 +- src/debug/jtag/shared/version.ts | 2 +- .../jtag/workers/data-daemon/src/main.rs | 93 ++++++++++++++++--- 8 files changed, 188 insertions(+), 27 deletions(-) diff --git a/src/debug/jtag/daemons/data-daemon/server/DataDaemonServer.ts b/src/debug/jtag/daemons/data-daemon/server/DataDaemonServer.ts index 1ed0eae50..52e05b04b 100644 --- a/src/debug/jtag/daemons/data-daemon/server/DataDaemonServer.ts +++ b/src/debug/jtag/daemons/data-daemon/server/DataDaemonServer.ts @@ -112,6 +112,9 @@ export class DataDaemonServer extends DataDaemonBase { await this.registerDatabaseHandles(); this.log.debug('Database handles registered'); + // Connect to Rust data-daemon worker and route observability collections through it + await this.connectRustDataWorker(); + // Initialize CodeDaemon for code/read operations const { initializeCodeDaemon } = await import('../../code-daemon/server/CodeDaemonServer'); await initializeCodeDaemon(this.context); @@ -236,6 +239,79 @@ export class DataDaemonServer extends DataDaemonBase { this.log.info(`Registered 'archive' handle: ${archiveDbPath} (emitEvents=false)`); } + /** + * Connect to Rust data-daemon worker and register it for observability collections. + * + * Strategy: Per-collection migration via DataDaemon.registerCollectionAdapter(). + * Instead of swapping the default adapter (which failed 4 times), we route + * specific collections through Rust incrementally. + * + * NO FALLBACK. If the Rust worker isn't available, this method retries until + * it connects. DaemonBase.runDeferredInitialization() catches errors silently, + * so we must guarantee success here — not throw and get swallowed. + * + * Phase 1 (this PR): Observability collections (write-heavy, non-critical) + * Phase 2 (next PR): Memory + task collections + * Phase 3 (later): Chat messages + core collections + * Phase 4 (later): Default adapter swap + */ + private async connectRustDataWorker(): Promise { + const SOCKET_PATH = '/tmp/jtag-data-daemon-worker.sock'; + const fs = await import('fs'); + + // Wait for the Rust worker socket to appear (workers start in parallel with Node.js) + const MAX_WAIT_MS = 30_000; + const POLL_INTERVAL_MS = 500; + const startWait = Date.now(); + + while (!fs.existsSync(SOCKET_PATH)) { + const elapsed = Date.now() - startWait; + if (elapsed >= MAX_WAIT_MS) { + this.log.error(`FATAL: Rust data-daemon worker socket not found at ${SOCKET_PATH} after ${MAX_WAIT_MS / 1000}s`); + process.exit(1); + } + if (elapsed % 5000 < POLL_INTERVAL_MS) { + this.log.warn(`Waiting for Rust data-daemon worker socket... (${Math.round(elapsed / 1000)}s)`); + } + await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS)); + } + + const { RustWorkerStorageAdapter } = await import('./RustWorkerStorageAdapter'); + + const rustAdapter = new RustWorkerStorageAdapter(); + await rustAdapter.initialize({ + type: 'rust' as any, + namespace: 'rust-default', + options: { + socketPath: SOCKET_PATH, + dbPath: getDatabasePath(), + timeout: 30000 + } + }); + + this.log.info('Connected to Rust data-daemon worker'); + + // Phase 1: Observability collections (write-heavy, non-critical) + const RUST_COLLECTIONS = [ + 'cognition_state_snapshots', + 'cognition_plan_records', + 'cognition_plan_step_executions', + 'cognition_self_state_updates', + 'cognition_memory_operations', + 'cognition_plan_replans', + 'adapter_decision_logs', + 'adapter_reasoning_logs', + 'response_generation_logs', + 'tool_execution_logs', + ]; + + for (const collection of RUST_COLLECTIONS) { + DataDaemon.registerCollectionAdapter(collection, rustAdapter); + } + + this.log.info(`🦀 Rust data-daemon: routed ${RUST_COLLECTIONS.length} observability collections through Rust worker`); + } + /** * Emit CRUD event - centralized event emission for all data operations * NOTE: This method is no longer used - event emission now handled by DataDaemon layer diff --git a/src/debug/jtag/daemons/data-daemon/server/DatabaseHandleRegistry.ts b/src/debug/jtag/daemons/data-daemon/server/DatabaseHandleRegistry.ts index 26acc86e5..5f99e1733 100644 --- a/src/debug/jtag/daemons/data-daemon/server/DatabaseHandleRegistry.ts +++ b/src/debug/jtag/daemons/data-daemon/server/DatabaseHandleRegistry.ts @@ -21,7 +21,7 @@ import { DataStorageAdapter } from '../shared/DataStorageAdapter'; import { SqliteStorageAdapter } from './SqliteStorageAdapter'; -// RustWorkerStorageAdapter removed - using TypeScript SQLite only +import { RustWorkerStorageAdapter } from './RustWorkerStorageAdapter'; import { DATABASE_PATHS } from '../../../system/data/config/DatabaseConfig'; import { generateUUID, type UUID } from '../../../system/core/types/CrossPlatformUUID'; import { getDatabasePath, getServerConfig } from '../../../system/config/ServerConfig'; @@ -219,14 +219,12 @@ export class DatabaseHandleRegistry { let storageAdapter: DataStorageAdapter; switch (adapter) { - case 'sqlite': - case 'rust': { // Rust deprecated - use SQLite + case 'sqlite': { const sqliteConfig = config as SqliteConfig; const dbPath = sqliteConfig.path || sqliteConfig.filename; if (!dbPath) { throw new Error('SQLite config requires either "path" or "filename" property'); } - // SQLite path logged via SqliteStorageAdapter.initialize() storageAdapter = new SqliteStorageAdapter(); await storageAdapter.initialize({ type: 'sqlite', @@ -238,10 +236,32 @@ export class DatabaseHandleRegistry { break; } + case 'rust': { + const rustConfig = config as RustConfig; + if (!rustConfig.filename) { + throw new Error('Rust config requires "filename" property (database path)'); + } + const socketPath = rustConfig.socketPath || '/tmp/jtag-data-daemon-worker.sock'; + storageAdapter = new RustWorkerStorageAdapter({ + socketPath, + dbPath: rustConfig.filename, + timeout: 30000 + }); + await storageAdapter.initialize({ + type: 'rust' as any, + namespace: handle as string, + options: { + socketPath, + dbPath: rustConfig.filename + } + }); + break; + } + case 'json': case 'vector': case 'graph': - throw new Error(`Adapter type '${adapter}' not yet implemented. Only 'sqlite' is currently supported.`); + throw new Error(`Adapter type '${adapter}' not yet implemented. Only 'sqlite' and 'rust' are currently supported.`); default: throw new Error(`Unknown adapter type: ${adapter}`); diff --git a/src/debug/jtag/daemons/data-daemon/server/DefaultStorageAdapterFactory.ts b/src/debug/jtag/daemons/data-daemon/server/DefaultStorageAdapterFactory.ts index 532c77505..a0cedfa87 100644 --- a/src/debug/jtag/daemons/data-daemon/server/DefaultStorageAdapterFactory.ts +++ b/src/debug/jtag/daemons/data-daemon/server/DefaultStorageAdapterFactory.ts @@ -2,10 +2,11 @@ * Default Storage Adapter Factory - Creates storage adapters based on configuration * * Provides factory pattern for creating different storage adapter types - * (SQLite, Memory, File) based on StorageAdapterConfig + * (SQLite, Rust, Memory, File) based on StorageAdapterConfig */ import { SqliteStorageAdapter } from '../server/SqliteStorageAdapter'; +import { RustWorkerStorageAdapter } from '../server/RustWorkerStorageAdapter'; import { MemoryStorageAdapter } from '../server/MemoryStorageAdapter'; import { FileStorageAdapter } from '../server/FileStorageAdapter'; import type { DataStorageAdapter, StorageAdapterConfig } from '../shared/DataStorageAdapter'; @@ -20,8 +21,9 @@ export class DefaultStorageAdapterFactory { createAdapter(config: StorageAdapterConfig): DataStorageAdapter { switch (config.type) { case 'sqlite': - case 'rust': // Rust adapter deprecated - use SQLite return new SqliteStorageAdapter(); + case 'rust': + return new RustWorkerStorageAdapter(); case 'memory': return new MemoryStorageAdapter(); case 'file': diff --git a/src/debug/jtag/generated-command-schemas.json b/src/debug/jtag/generated-command-schemas.json index fac7eb0b8..f1d17478a 100644 --- a/src/debug/jtag/generated-command-schemas.json +++ b/src/debug/jtag/generated-command-schemas.json @@ -1,5 +1,5 @@ { - "generated": "2026-02-05T16:14:25.713Z", + "generated": "2026-02-05T18:10:18.289Z", "version": "1.0.0", "commands": [ { diff --git a/src/debug/jtag/package-lock.json b/src/debug/jtag/package-lock.json index b022bbe1c..1a93bf2a5 100644 --- a/src/debug/jtag/package-lock.json +++ b/src/debug/jtag/package-lock.json @@ -1,12 +1,12 @@ { "name": "@continuum/jtag", - "version": "1.0.7617", + "version": "1.0.7622", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@continuum/jtag", - "version": "1.0.7617", + "version": "1.0.7622", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/debug/jtag/package.json b/src/debug/jtag/package.json index 25208162a..87dbbf95c 100644 --- a/src/debug/jtag/package.json +++ b/src/debug/jtag/package.json @@ -1,6 +1,6 @@ { "name": "@continuum/jtag", - "version": "1.0.7617", + "version": "1.0.7622", "description": "Global CLI debugging system for any Node.js project. Install once globally, use anywhere: npm install -g @continuum/jtag", "config": { "active_example": "widget-ui", diff --git a/src/debug/jtag/shared/version.ts b/src/debug/jtag/shared/version.ts index 8ff47f374..449a57c9e 100644 --- a/src/debug/jtag/shared/version.ts +++ b/src/debug/jtag/shared/version.ts @@ -3,5 +3,5 @@ * DO NOT EDIT MANUALLY */ -export const VERSION = '1.0.7617'; +export const VERSION = '1.0.7622'; export const PACKAGE_NAME = '@continuum/jtag'; diff --git a/src/debug/jtag/workers/data-daemon/src/main.rs b/src/debug/jtag/workers/data-daemon/src/main.rs index 7fa878a5c..b5138249c 100644 --- a/src/debug/jtag/workers/data-daemon/src/main.rs +++ b/src/debug/jtag/workers/data-daemon/src/main.rs @@ -15,7 +15,7 @@ use rayon::prelude::*; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use std::collections::{HashMap, VecDeque}; +use std::collections::{HashMap, HashSet, VecDeque}; use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; @@ -1075,15 +1075,60 @@ impl AdapterRegistry { struct RustDataDaemon { registry: Arc, + /// Cache of table column names per collection (populated via PRAGMA table_info) + table_columns_cache: Arc>>>, } impl RustDataDaemon { fn new() -> Self { Self { registry: Arc::new(AdapterRegistry::new()), + table_columns_cache: Arc::new(Mutex::new(HashMap::new())), } } + /// Get the valid column names for a table, using PRAGMA table_info. + /// Results are cached per (handle, collection) to avoid repeated PRAGMA queries. + fn get_table_columns( + &self, + handle: AdapterHandle, + collection: &str, + ) -> Result, String> { + // Check cache first + { + let cache = self.table_columns_cache.lock().unwrap(); + if let Some(columns) = cache.get(collection) { + return Ok(columns.clone()); + } + } + + // Query PRAGMA table_info to discover actual columns + let pragma_query = format!("PRAGMA table_info({})", collection); + let result = self.registry.execute_read(handle, &pragma_query)?; + + let items = result + .get("items") + .and_then(|v| v.as_array()) + .ok_or_else(|| format!("PRAGMA table_info({}) returned no items", collection))?; + + let columns: HashSet = items + .iter() + .filter_map(|row| row.get("name").and_then(|n| n.as_str()).map(|s| s.to_string())) + .collect(); + + if columns.is_empty() { + return Err(format!("Table {} has no columns (does it exist?)", collection)); + } + + // Cache the result + { + let mut cache = self.table_columns_cache.lock().unwrap(); + cache.insert(collection.to_string(), columns.clone()); + } + + Ok(columns) + } + #[allow(dead_code)] fn handle_request(&self, request: Request) -> Response { match request { @@ -1606,17 +1651,23 @@ impl RustDataDaemon { .as_object() .ok_or_else(|| "Data must be an object".to_string())?; - // Build INSERT query - let columns: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - let values: Vec = obj - .values() - .map(|v| match v { + // Filter to only columns that exist in the table schema + let valid_columns = self.get_table_columns(handle, collection)?; + + let filtered: Vec<(&String, &Value)> = obj + .iter() + .filter(|(k, _)| valid_columns.contains(k.as_str())) + .collect(); + + let columns: Vec<&str> = filtered.iter().map(|(k, _)| k.as_str()).collect(); + let values: Vec = filtered + .iter() + .map(|(_, v)| match v { Value::String(s) => format!("'{}'", s.replace("'", "''")), Value::Number(n) => n.to_string(), Value::Bool(b) => if *b { "1" } else { "0" }.to_string(), Value::Null => "NULL".to_string(), Value::Array(_) | Value::Object(_) => { - // Serialize complex types as JSON strings format!( "'{}'", serde_json::to_string(v) @@ -1669,10 +1720,12 @@ impl RustDataDaemon { .as_object() .ok_or_else(|| "Data must be an object".to_string())?; - // Build SET clauses + // Filter to only columns that exist in the table schema + let valid_columns = self.get_table_columns(handle, collection)?; + let set_clauses: Vec = obj .iter() - .filter(|(key, _)| *key != "id") // Don't update id + .filter(|(key, _)| *key != "id" && valid_columns.contains(key.as_str())) .map(|(key, value)| { let val_str = match value { Value::String(s) => format!("'{}'", s.replace("'", "''")), @@ -1680,7 +1733,6 @@ impl RustDataDaemon { Value::Bool(b) => if *b { "1" } else { "0" }.to_string(), Value::Null => "NULL".to_string(), Value::Array(_) | Value::Object(_) => { - // Serialize complex types as JSON strings format!( "'{}'", serde_json::to_string(value) @@ -1819,10 +1871,18 @@ impl RustDataDaemon { .as_object() .ok_or_else(|| "Data must be an object".to_string())?; - let columns: Vec<&str> = obj.keys().map(|k| k.as_str()).collect(); - let values: Vec = obj - .values() - .map(|v| match v { + // Filter to only columns that exist in the table schema + let valid_columns = self.get_table_columns(handle, collection)?; + + let filtered: Vec<(&String, &Value)> = obj + .iter() + .filter(|(k, _)| valid_columns.contains(k.as_str())) + .collect(); + + let columns: Vec<&str> = filtered.iter().map(|(k, _)| k.as_str()).collect(); + let values: Vec = filtered + .iter() + .map(|(_, v)| match v { Value::String(s) => format!("'{}'", s.replace("'", "''")), Value::Number(n) => n.to_string(), Value::Bool(b) => if *b { "1" } else { "0" }.to_string(), @@ -1894,9 +1954,12 @@ impl RustDataDaemon { .as_object() .ok_or_else(|| "Data must be an object".to_string())?; + // Filter to only columns that exist in the table schema + let valid_columns = self.get_table_columns(handle, collection)?; + let set_clauses: Vec = obj .iter() - .filter(|(key, _)| *key != "id") + .filter(|(key, _)| *key != "id" && valid_columns.contains(key.as_str())) .map(|(key, value)| { let val_str = match value { Value::String(s) => format!("'{}'", s.replace("'", "''")), From 9c850c239bb6049e691caffd84db7f38e1f6a33f Mon Sep 17 00:00:00 2001 From: Grok Date: Thu, 5 Feb 2026 15:23:27 -0600 Subject: [PATCH 3/5] remove generated types --- src/debug/jtag/.gitignore | 3 + .../data/list/server/DataListServerCommand.ts | 13 +- .../data-daemon/server/DataDaemonServer.ts | 41 +- .../server/RustWorkerStorageAdapter.ts | 581 ++++++++++++------ .../daemons/data-daemon/shared/DataDaemon.ts | 34 +- src/debug/jtag/generated-command-schemas.json | 2 +- .../jtag/generator/generate-rust-bindings.ts | 226 +++++++ src/debug/jtag/package-lock.json | 4 +- src/debug/jtag/package.json | 4 +- .../jtag/shared/generated/CallMessage.ts | 7 - src/debug/jtag/shared/generated/RagTypes.ts | 6 - .../jtag/shared/generated/code/ChangeNode.ts | 44 -- .../shared/generated/code/ClassifiedLine.ts | 27 - .../jtag/shared/generated/code/DiffHunk.ts | 10 - .../jtag/shared/generated/code/EditMode.ts | 6 - .../jtag/shared/generated/code/FileDiff.ts | 15 - .../shared/generated/code/FileOperation.ts | 6 - .../shared/generated/code/GitStatusInfo.ts | 6 - .../shared/generated/code/HistoryResult.ts | 7 - .../generated/code/OutputClassification.ts | 6 - .../jtag/shared/generated/code/ReadResult.ts | 6 - .../jtag/shared/generated/code/SearchMatch.ts | 6 - .../shared/generated/code/SearchResult.ts | 7 - .../shared/generated/code/SentinelAction.ts | 6 - .../shared/generated/code/SentinelRule.ts | 23 - .../generated/code/ShellExecuteResponse.ts | 22 - .../generated/code/ShellExecutionStatus.ts | 6 - .../generated/code/ShellHistoryEntry.ts | 6 - .../generated/code/ShellPollResponse.ts | 26 - .../shared/generated/code/ShellSessionInfo.ts | 6 - .../generated/code/ShellWatchResponse.ts | 23 - .../jtag/shared/generated/code/TreeNode.ts | 6 - .../jtag/shared/generated/code/TreeResult.ts | 7 - .../jtag/shared/generated/code/UndoResult.ts | 7 - .../jtag/shared/generated/code/WriteResult.ts | 10 - src/debug/jtag/shared/generated/code/index.ts | 42 -- src/debug/jtag/shared/generated/index.ts | 18 - .../generated/ipc/InboxMessageRequest.ts | 10 - src/debug/jtag/shared/generated/ipc/index.ts | 14 - .../generated/persona/ActivityDomain.ts | 7 - .../persona/ChannelEnqueueRequest.ts | 6 - .../persona/ChannelRegistryStatus.ts | 7 - .../shared/generated/persona/ChannelStatus.ts | 7 - .../generated/persona/CognitionDecision.ts | 6 - .../generated/persona/ConsolidatedContext.ts | 6 - .../shared/generated/persona/InboxMessage.ts | 5 - .../shared/generated/persona/InboxTask.ts | 6 - .../jtag/shared/generated/persona/Modality.ts | 6 - .../jtag/shared/generated/persona/Mood.ts | 6 - .../shared/generated/persona/PersonaState.ts | 35 -- .../generated/persona/PriorityFactors.ts | 6 - .../shared/generated/persona/PriorityScore.ts | 7 - .../shared/generated/persona/QueueItem.ts | 8 - .../shared/generated/persona/SenderType.ts | 6 - .../generated/persona/ServiceCycleResult.ts | 28 - .../jtag/shared/generated/persona/index.ts | 28 - .../jtag/shared/generated/rag/LlmMessage.ts | 7 - .../jtag/shared/generated/rag/MessageRole.ts | 6 - .../jtag/shared/generated/rag/RagContext.ts | 8 - .../jtag/shared/generated/rag/RagMetadata.ts | 10 - .../jtag/shared/generated/rag/RagOptions.ts | 6 - .../jtag/shared/generated/rag/SourceTiming.ts | 6 - src/debug/jtag/shared/generated/rag/index.ts | 9 - src/debug/jtag/shared/version.ts | 2 +- .../jtag/workers/data-daemon/src/main.rs | 130 +++- .../jtag/workers/data-daemon/src/types.rs | 226 +++++++ 66 files changed, 991 insertions(+), 898 deletions(-) create mode 100644 src/debug/jtag/generator/generate-rust-bindings.ts delete mode 100644 src/debug/jtag/shared/generated/CallMessage.ts delete mode 100644 src/debug/jtag/shared/generated/RagTypes.ts delete mode 100644 src/debug/jtag/shared/generated/code/ChangeNode.ts delete mode 100644 src/debug/jtag/shared/generated/code/ClassifiedLine.ts delete mode 100644 src/debug/jtag/shared/generated/code/DiffHunk.ts delete mode 100644 src/debug/jtag/shared/generated/code/EditMode.ts delete mode 100644 src/debug/jtag/shared/generated/code/FileDiff.ts delete mode 100644 src/debug/jtag/shared/generated/code/FileOperation.ts delete mode 100644 src/debug/jtag/shared/generated/code/GitStatusInfo.ts delete mode 100644 src/debug/jtag/shared/generated/code/HistoryResult.ts delete mode 100644 src/debug/jtag/shared/generated/code/OutputClassification.ts delete mode 100644 src/debug/jtag/shared/generated/code/ReadResult.ts delete mode 100644 src/debug/jtag/shared/generated/code/SearchMatch.ts delete mode 100644 src/debug/jtag/shared/generated/code/SearchResult.ts delete mode 100644 src/debug/jtag/shared/generated/code/SentinelAction.ts delete mode 100644 src/debug/jtag/shared/generated/code/SentinelRule.ts delete mode 100644 src/debug/jtag/shared/generated/code/ShellExecuteResponse.ts delete mode 100644 src/debug/jtag/shared/generated/code/ShellExecutionStatus.ts delete mode 100644 src/debug/jtag/shared/generated/code/ShellHistoryEntry.ts delete mode 100644 src/debug/jtag/shared/generated/code/ShellPollResponse.ts delete mode 100644 src/debug/jtag/shared/generated/code/ShellSessionInfo.ts delete mode 100644 src/debug/jtag/shared/generated/code/ShellWatchResponse.ts delete mode 100644 src/debug/jtag/shared/generated/code/TreeNode.ts delete mode 100644 src/debug/jtag/shared/generated/code/TreeResult.ts delete mode 100644 src/debug/jtag/shared/generated/code/UndoResult.ts delete mode 100644 src/debug/jtag/shared/generated/code/WriteResult.ts delete mode 100644 src/debug/jtag/shared/generated/code/index.ts delete mode 100644 src/debug/jtag/shared/generated/index.ts delete mode 100644 src/debug/jtag/shared/generated/ipc/InboxMessageRequest.ts delete mode 100644 src/debug/jtag/shared/generated/ipc/index.ts delete mode 100644 src/debug/jtag/shared/generated/persona/ActivityDomain.ts delete mode 100644 src/debug/jtag/shared/generated/persona/ChannelEnqueueRequest.ts delete mode 100644 src/debug/jtag/shared/generated/persona/ChannelRegistryStatus.ts delete mode 100644 src/debug/jtag/shared/generated/persona/ChannelStatus.ts delete mode 100644 src/debug/jtag/shared/generated/persona/CognitionDecision.ts delete mode 100644 src/debug/jtag/shared/generated/persona/ConsolidatedContext.ts delete mode 100644 src/debug/jtag/shared/generated/persona/InboxMessage.ts delete mode 100644 src/debug/jtag/shared/generated/persona/InboxTask.ts delete mode 100644 src/debug/jtag/shared/generated/persona/Modality.ts delete mode 100644 src/debug/jtag/shared/generated/persona/Mood.ts delete mode 100644 src/debug/jtag/shared/generated/persona/PersonaState.ts delete mode 100644 src/debug/jtag/shared/generated/persona/PriorityFactors.ts delete mode 100644 src/debug/jtag/shared/generated/persona/PriorityScore.ts delete mode 100644 src/debug/jtag/shared/generated/persona/QueueItem.ts delete mode 100644 src/debug/jtag/shared/generated/persona/SenderType.ts delete mode 100644 src/debug/jtag/shared/generated/persona/ServiceCycleResult.ts delete mode 100644 src/debug/jtag/shared/generated/persona/index.ts delete mode 100644 src/debug/jtag/shared/generated/rag/LlmMessage.ts delete mode 100644 src/debug/jtag/shared/generated/rag/MessageRole.ts delete mode 100644 src/debug/jtag/shared/generated/rag/RagContext.ts delete mode 100644 src/debug/jtag/shared/generated/rag/RagMetadata.ts delete mode 100644 src/debug/jtag/shared/generated/rag/RagOptions.ts delete mode 100644 src/debug/jtag/shared/generated/rag/SourceTiming.ts delete mode 100644 src/debug/jtag/shared/generated/rag/index.ts create mode 100644 src/debug/jtag/workers/data-daemon/src/types.rs diff --git a/src/debug/jtag/.gitignore b/src/debug/jtag/.gitignore index 688e7aa89..6ad6ec334 100644 --- a/src/debug/jtag/.gitignore +++ b/src/debug/jtag/.gitignore @@ -48,6 +48,9 @@ __pycache__/ .continuum/genome/python/pkgs/ system/genome/python/venv/ +# Generated TypeScript from Rust via ts-rs (regenerated by: npx tsx generator/generate-rust-bindings.ts) +shared/generated/ + # Rust build artifacts # Compiled files *.o diff --git a/src/debug/jtag/commands/data/list/server/DataListServerCommand.ts b/src/debug/jtag/commands/data/list/server/DataListServerCommand.ts index cab21606c..69a20d738 100644 --- a/src/debug/jtag/commands/data/list/server/DataListServerCommand.ts +++ b/src/debug/jtag/commands/data/list/server/DataListServerCommand.ts @@ -84,14 +84,13 @@ export class DataListServerCommand extends CommandBase extends CommandBase { const SOCKET_PATH = '/tmp/jtag-data-daemon-worker.sock'; @@ -291,25 +286,15 @@ export class DataDaemonServer extends DataDaemonBase { this.log.info('Connected to Rust data-daemon worker'); - // Phase 1: Observability collections (write-heavy, non-critical) - const RUST_COLLECTIONS = [ - 'cognition_state_snapshots', - 'cognition_plan_records', - 'cognition_plan_step_executions', - 'cognition_self_state_updates', - 'cognition_memory_operations', - 'cognition_plan_replans', - 'adapter_decision_logs', - 'adapter_reasoning_logs', - 'response_generation_logs', - 'tool_execution_logs', - ]; - - for (const collection of RUST_COLLECTIONS) { + // Route ALL collections through Rust worker for off-main-thread I/O + const { COLLECTIONS } = await import('../../../system/shared/Constants'); + const allCollections = Object.values(COLLECTIONS); + + for (const collection of allCollections) { DataDaemon.registerCollectionAdapter(collection, rustAdapter); } - this.log.info(`🦀 Rust data-daemon: routed ${RUST_COLLECTIONS.length} observability collections through Rust worker`); + this.log.info(`🦀 Rust data-daemon: routed ALL ${allCollections.length} collections through Rust worker`); } /** diff --git a/src/debug/jtag/daemons/data-daemon/server/RustWorkerStorageAdapter.ts b/src/debug/jtag/daemons/data-daemon/server/RustWorkerStorageAdapter.ts index 3d6d0f4c0..65cae6671 100644 --- a/src/debug/jtag/daemons/data-daemon/server/RustWorkerStorageAdapter.ts +++ b/src/debug/jtag/daemons/data-daemon/server/RustWorkerStorageAdapter.ts @@ -8,6 +8,10 @@ * - Rust owns: Database I/O, connection pooling, concurrent operations * * Communication: Unix domain socket (low overhead, high throughput) + * + * Type Safety: Response types generated from Rust via ts-rs (shared/generated/data-daemon/). + * Rust is the single source of truth for the wire format. + * Re-generate: cargo test --package data-daemon-worker export_bindings */ import * as net from 'net'; @@ -38,6 +42,22 @@ import { import { RustEmbeddingClient } from '../../../system/core/services/RustEmbeddingClient'; import { Logger } from '../../../system/core/logging/Logger'; +// Generated types from Rust via ts-rs — single source of truth for IPC wire format +// Re-generate: cargo test --package data-daemon-worker export_bindings +import type { + DataListResult, + DataQueryResult, + ListTablesResult, + DataWriteResult, + VectorSearchResult as RustVectorSearchResult, + VectorSearchHit, + AdapterOpenResult, + BlobStoreResult, + BlobStatsResult, + BlobExistsResult, + BlobDeleteResult, +} from '../../../shared/generated/data-daemon'; + const log = Logger.create('RustWorkerStorageAdapter', 'data'); /** @@ -54,31 +74,58 @@ export interface RustWorkerConfig { } /** - * Rust worker request/response format (simpler than full JTAG protocol) - * Uses serde's tag-based enum serialization + * Rust worker response envelope — discriminated union matching Rust's Response enum. + * + * Rust source of truth: workers/data-daemon/src/main.rs + * Uses serde's tag-based enum serialization (#[serde(tag = "status")]) + * + * TypeScript narrows the type when you check `status`: + * if (response.status === 'ok') { response.data } // data exists + * if (response.status === 'error') { response.message } // message exists */ -interface RustResponse { - status: 'ok' | 'error' | 'pong'; - data?: any; - message?: string; - uptime_seconds?: number; -} +type RustResponse = + | { status: 'ok'; data: T } + | { status: 'error'; message: string } + | { status: 'pong'; uptime_seconds: number }; /** - * Rust Worker Storage Adapter - Fast concurrent storage via Rust process + * A single pooled connection to the Rust worker. + * Each connection has its own socket, buffer, and pending response slot. + * The Rust worker spawns a thread per connection, so N connections = N-way parallelism. */ -export class RustWorkerStorageAdapter extends DataStorageAdapter { - private config!: RustWorkerConfig; - private socket: net.Socket | null = null; - private adapterHandle: string | null = null; // Handle from adapter/open - private pendingResponse: { +interface PooledConnection { + id: number; + socket: net.Socket; + buffer: string; + pendingResponse: { resolve: (value: any) => void; reject: (error: Error) => void; timeout: NodeJS.Timeout; - } | null = null; + } | null; + busy: boolean; +} + +const POOL_SIZE = 8; - private reconnecting: boolean = false; - private buffer: string = ''; +/** + * Rust Worker Storage Adapter - Fast concurrent storage via Rust process + * + * Uses a connection pool (8 sockets by default) to the Rust worker. + * Each connection maps to a Rust thread, enabling parallel database I/O. + */ +export class RustWorkerStorageAdapter extends DataStorageAdapter { + private config!: RustWorkerConfig; + private pool: PooledConnection[] = []; + private adapterHandle: string | null = null; // Handle from adapter/open (shared across pool) + private waitQueue: Array<(conn: PooledConnection) => void> = []; + + // Pool utilization stats + private _statsInterval: NodeJS.Timeout | null = null; + private _requestCount = 0; + private _waitCount = 0; // Requests that had to wait for a connection + private _totalAcquireMs = 0; + private _totalRoundTripMs = 0; + private _maxWaitQueueDepth = 0; /** * Convert object keys from camelCase to snake_case (for sending to Rust/SQL) @@ -94,16 +141,36 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { /** * Convert object keys from snake_case to camelCase (for returning to TypeScript) + * Also hydrates JSON string values — SQLite stores JSON as TEXT, so fields like + * reactions="[]" or content="{...}" need to be parsed back to objects/arrays. */ private toCamelCaseObject(obj: Record): Record { const result: Record = {}; for (const [key, value] of Object.entries(obj)) { const camelKey = SqlNamingConverter.toCamelCase(key); - result[camelKey] = value; + result[camelKey] = this.hydrateValue(value); } return result; } + /** + * Hydrate a single value — parse JSON strings back to objects/arrays. + * SQLite TEXT columns containing JSON come back as raw strings from Rust. + */ + private hydrateValue(value: any): any { + if (typeof value !== 'string') return value; + const trimmed = value.trim(); + if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || + (trimmed.startsWith('[') && trimmed.endsWith(']'))) { + try { + return JSON.parse(trimmed); + } catch { + return value; // Not valid JSON, return as-is + } + } + return value; + } + constructor(config?: RustWorkerConfig) { super(); if (config) { @@ -112,16 +179,15 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { } /** - * Initialize connection to Rust worker + * Initialize connection pool to Rust worker * - * REQUIRED in config.options: - * - socketPath: Path to Rust worker Unix socket - * - dbPath: Absolute path to SQLite database file + * Opens POOL_SIZE concurrent socket connections. Each maps to a Rust thread, + * enabling parallel database I/O. Opens the SQLite adapter once (handle is + * shared across all connections via Rust's register_with_cache). */ async initialize(config: StorageAdapterConfig): Promise { const options = config.options as any; - // Require socket and database paths - no defaults if (!options?.socketPath) { throw new Error('RustWorkerStorageAdapter requires socketPath in options'); } @@ -132,119 +198,163 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { this.config = { socketPath: options.socketPath, dbPath: options.dbPath, - timeout: options.timeout || 60000 // 60s - needed for large vector searches (3K+ vectors) + timeout: options.timeout || 60000 }; - await this.connect(); + // Open POOL_SIZE connections in parallel + const connectPromises: Promise[] = []; + for (let i = 0; i < POOL_SIZE; i++) { + connectPromises.push(this.openConnection(i)); + } + this.pool = await Promise.all(connectPromises); - // Open SQLite adapter and store handle - const response = await this.sendCommand<{ handle: string }>('adapter/open', { + // Open SQLite adapter via the first connection (handle is shared in Rust) + const response = await this.sendCommand('adapter/open', { config: { adapter_type: 'sqlite', connection_string: this.config.dbPath } }); - if (response.status === 'ok' && response.data?.handle) { + if (response.status === 'ok' && response.data.handle) { this.adapterHandle = response.data.handle; - console.log(`✅ Opened SQLite adapter: ${this.config.dbPath} → handle ${this.adapterHandle}`); + log.info(`Opened SQLite adapter: ${this.config.dbPath} → handle ${this.adapterHandle} (${POOL_SIZE} connections)`); + } else if (response.status === 'error') { + throw new Error(`Failed to open adapter: ${response.message}`); } else { - throw new Error(`Failed to open adapter: ${response.message || 'Unknown error'}`); + throw new Error('Failed to open adapter: unexpected response'); } + + // Log pool utilization every 30 seconds + this._statsInterval = setInterval(() => { + if (this._requestCount === 0) return; + const busyCount = this.pool.filter(c => c.busy).length; + const avgAcquire = this._totalAcquireMs / this._requestCount; + const avgRoundTrip = this._totalRoundTripMs / this._requestCount; + log.info(`🦀 Pool stats: ${this._requestCount} reqs, ${this._waitCount} waited, ` + + `avg acquire=${avgAcquire.toFixed(0)}ms, avg roundtrip=${avgRoundTrip.toFixed(0)}ms, ` + + `busy=${busyCount}/${POOL_SIZE}, max queue=${this._maxWaitQueueDepth}`); + // Reset for next interval + this._requestCount = 0; + this._waitCount = 0; + this._totalAcquireMs = 0; + this._totalRoundTripMs = 0; + this._maxWaitQueueDepth = 0; + }, 30_000); } /** - * Connect to Rust worker Unix socket + * Open a single socket connection to the Rust worker */ - private async connect(): Promise { + private openConnection(id: number): Promise { return new Promise((resolve, reject) => { - this.socket = net.createConnection(this.config.socketPath); + const socket = net.createConnection(this.config.socketPath); + const conn: PooledConnection = { + id, + socket, + buffer: '', + pendingResponse: null, + busy: false, + }; - this.socket.on('connect', () => { - console.log(`✅ Connected to Rust worker: ${this.config.socketPath}`); - this.reconnecting = false; - resolve(); + socket.on('connect', () => { + resolve(conn); }); - this.socket.on('data', (data) => { - this.handleData(data); + socket.on('data', (data) => { + this.handleConnectionData(conn, data); }); - this.socket.on('error', (error) => { - console.error('❌ Rust worker socket error:', error); - if (!this.reconnecting) { - reject(error); - } + socket.on('error', (error) => { + log.error(`Rust worker socket #${id} error: ${error.message}`); }); - this.socket.on('close', () => { - console.warn('⚠️ Rust worker connection closed, will reconnect on next request'); - this.socket = null; - this.adapterHandle = null; // Need to reopen adapter after reconnect + socket.on('close', () => { + log.warn(`Rust worker connection #${id} closed`); + // Mark as not busy so it can be reconnected on next acquire + conn.busy = false; }); - // Connection timeout setTimeout(() => { - if (!this.socket || this.socket.connecting) { - reject(new Error(`Connection timeout: ${this.config.socketPath}`)); + if (socket.connecting) { + reject(new Error(`Connection #${id} timeout: ${this.config.socketPath}`)); } - }, this.config.timeout); + }, 10000); }); } /** - * Handle incoming data from Rust worker (line-delimited JSON) + * Handle incoming data on a specific pooled connection */ - private handleData(data: Buffer): void { - this.buffer += data.toString(); + private handleConnectionData(conn: PooledConnection, data: Buffer): void { + conn.buffer += data.toString(); - // Process complete lines (messages are newline-delimited) - const lines = this.buffer.split('\n'); - this.buffer = lines.pop() || ''; // Keep incomplete line in buffer + const lines = conn.buffer.split('\n'); + conn.buffer = lines.pop() || ''; for (const line of lines) { if (!line.trim()) continue; try { - const response: RustResponse = JSON.parse(line); - - if (this.pendingResponse) { - clearTimeout(this.pendingResponse.timeout); - const pending = this.pendingResponse; - this.pendingResponse = null; + const response = JSON.parse(line) as RustResponse; + + if (conn.pendingResponse) { + clearTimeout(conn.pendingResponse.timeout); + const pending = conn.pendingResponse; + conn.pendingResponse = null; + conn.busy = false; + // Wake up next waiter if any + if (this.waitQueue.length > 0) { + const waiter = this.waitQueue.shift()!; + waiter(conn); + } pending.resolve(response); } } catch (error) { - console.error('Failed to parse response from Rust worker:', error); + log.error(`Failed to parse response from Rust worker #${conn.id}: ${error}`); } } } /** - * Send command to Rust worker and wait for response - * Uses Rust's serde tag format: {"command": "name", ...params} - * Auto-reconnects if connection was lost + * Acquire an available connection from the pool. + * If all are busy, waits for one to become available. */ - private async sendCommand(command: string, params: Record = {}): Promise { - // Auto-reconnect if socket is closed - if (!this.socket) { - console.log('🔄 Reconnecting to Rust worker...'); - await this.connect(); - - // Reopen adapter after reconnect - const response = await this.sendCommand<{ handle: string }>('adapter/open', { - config: { - adapter_type: 'sqlite', - connection_string: this.config.dbPath - } + private acquireConnection(): Promise { + // Find first non-busy connection + for (const conn of this.pool) { + if (!conn.busy && conn.socket && !conn.socket.destroyed) { + conn.busy = true; + return Promise.resolve(conn); + } + } + + // All busy — wait for one to free up + this._waitCount++; + return new Promise((resolve) => { + this.waitQueue.push((conn: PooledConnection) => { + conn.busy = true; + resolve(conn); }); + }); + } - if (response.status === 'ok' && response.data?.handle) { - this.adapterHandle = response.data.handle; - console.log(`✅ Reopened SQLite adapter: ${this.config.dbPath} → handle ${this.adapterHandle}`); - } else { - throw new Error(`Failed to reopen adapter: ${response.message || 'Unknown error'}`); - } + /** + * Send command to Rust worker via the connection pool. + * Acquires a connection, sends, waits for response, releases. + * + * Generic T should match the generated response type from ts-rs + * (e.g., DataListResult, VectorSearchResult, ListTablesResult). + */ + private async sendCommand(command: string, params: Record = {}): Promise> { + const acquireStart = Date.now(); + const conn = await this.acquireConnection(); + const acquireMs = Date.now() - acquireStart; + + this._requestCount++; + this._totalAcquireMs += acquireMs; + if (this.waitQueue.length > this._maxWaitQueueDepth) { + this._maxWaitQueueDepth = this.waitQueue.length; } const request = { @@ -252,27 +362,38 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { ...params }; + const sendStart = Date.now(); + return new Promise((resolve, reject) => { const timeout = setTimeout(() => { - this.pendingResponse = null; + conn.pendingResponse = null; + conn.busy = false; + if (this.waitQueue.length > 0) { + const waiter = this.waitQueue.shift()!; + waiter(conn); + } reject(new Error(`Request timeout: ${command}`)); }, this.config.timeout); - this.pendingResponse = { resolve, reject, timeout }; + conn.pendingResponse = { + resolve: (value: any) => { + this._totalRoundTripMs += (Date.now() - sendStart); + resolve(value); + }, + reject, + timeout + }; - // Send newline-delimited JSON - this.socket!.write(JSON.stringify(request) + '\n'); + conn.socket.write(JSON.stringify(request) + '\n'); }); } /** - * Ensure we're connected and have an adapter handle (auto-reconnect) + * Ensure pool is connected and adapter handle is available */ private async ensureConnected(): Promise { - if (!this.socket || !this.adapterHandle) { - // Force reconnect by calling sendCommand with a benign command - // The reconnect logic in sendCommand will re-establish connection and adapter - await this.sendCommand('ping', {}); + if (this.pool.length === 0 || !this.adapterHandle) { + throw new Error('RustWorkerStorageAdapter not initialized'); } } @@ -299,14 +420,15 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { version: record.metadata?.version || 1 }; - const response = await this.sendCommand('data/create', { + const response = await this.sendCommand('data/create', { handle: this.adapterHandle, collection: SqlNamingConverter.toTableName(record.collection), data: fullData }); if (response.status !== 'ok') { - return { success: false, error: response.message || 'Create failed' }; + const errorMsg = response.status === 'error' ? response.message : 'Create failed'; + return { success: false, error: errorMsg }; } return { @@ -334,33 +456,46 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { } try { - const response = await this.sendCommand<{ items: T[]; count: number }>('data/list', { + const response = await this.sendCommand('data/list', { handle: this.adapterHandle, collection, filter: { id }, limit: 1 }); - if (response.status !== 'ok' || !response.data?.items?.length) { - return { success: false, error: 'Record not found' }; + if (response.status !== 'ok' || !response.data.items?.length) { + const errorMsg = response.status === 'error' ? response.message : 'Record not found'; + return { success: false, error: errorMsg }; + } + + const item = response.data.items[0] as any; + + // Hydrate: convert snake_case keys to camelCase and parse JSON string values + let entityData: T; + if (typeof item.data === 'string') { + entityData = JSON.parse(item.data) as T; + } else if (item.data && typeof item.data === 'object') { + entityData = item.data as T; + } else { + const { id: _id, created_at, updated_at, version, ...rest } = item; + entityData = this.toCamelCaseObject(rest) as T; } - const item = response.data.items[0]; // Ensure id is always present in the data object - // Some callers expect data.data.id to be set - if (!item.id) { - item.id = id; + if (!(entityData as any).id) { + (entityData as any).id = id; } + return { success: true, data: { id, collection, - data: item, + data: entityData, metadata: { - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - version: 1 + createdAt: item.created_at || new Date().toISOString(), + updatedAt: item.updated_at || new Date().toISOString(), + version: item.version || 1 } } }; @@ -389,7 +524,7 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { direction: s.direction })); - const response = await this.sendCommand<{ items: T[]; count: number }>('data/list', { + const response = await this.sendCommand('data/list', { handle: this.adapterHandle, collection: SqlNamingConverter.toTableName(query.collection), filter: snakeCaseFilter, @@ -399,10 +534,11 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { }); if (response.status !== 'ok') { - return { success: false, error: response.message || 'Query failed' }; + const errorMsg = response.status === 'error' ? response.message : 'Query failed'; + return { success: false, error: errorMsg }; } - const records: DataRecord[] = (response.data?.items || []).map((item: any) => { + const records: DataRecord[] = (response.data.items || []).map((item: any) => { // Two table formats: // 1. Simple entity: has 'data' column containing JSON string // 2. Entity-specific: has individual columns for each field @@ -445,7 +581,7 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { success: true, data: records, metadata: { - totalCount: response.data?.count || records.length + totalCount: (response.status === 'ok' ? response.data.count : 0) || records.length } }; } catch (error: any) { @@ -643,7 +779,7 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { version: incrementVersion ? { $increment: 1 } : undefined }; - const response = await this.sendCommand('data/update', { + const response = await this.sendCommand('data/update', { handle: this.adapterHandle, collection: SqlNamingConverter.toTableName(collection), id, @@ -651,7 +787,8 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { }); if (response.status !== 'ok') { - return { success: false, error: response.message || 'Update failed' }; + const errorMsg = response.status === 'error' ? response.message : 'Update failed'; + return { success: false, error: errorMsg }; } return { @@ -683,14 +820,15 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { } try { - const response = await this.sendCommand('data/delete', { + const response = await this.sendCommand('data/delete', { handle: this.adapterHandle, collection, id }); if (response.status !== 'ok') { - return { success: false, error: response.message || 'Delete failed' }; + const errorMsg = response.status === 'error' ? response.message : 'Delete failed'; + return { success: false, error: errorMsg }; } return { success: true, data: true }; @@ -700,10 +838,29 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { } /** - * List all collections - TODO: Implement in Rust worker + * List all collections (tables) in the database via Rust worker */ async listCollections(): Promise> { - throw new Error('List collections not yet implemented in Rust worker'); + try { + await this.ensureConnected(); + } catch (error: any) { + return { success: false, error: `Connection failed: ${error.message}` }; + } + + try { + const response = await this.sendCommand('data/list_tables', { + handle: this.adapterHandle, + }); + + if (response.status !== 'ok') { + const errorMsg = response.status === 'error' ? response.message : 'List tables failed'; + return { success: false, error: errorMsg }; + } + + return { success: true, data: response.data.tables || [] }; + } catch (error: any) { + return { success: false, error: error.message }; + } } /** @@ -760,10 +917,23 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { } /** - * Clear all data from all collections - TODO: Implement in Rust worker + * Clear all data from all collections via Rust worker */ async clear(): Promise> { - throw new Error('Clear not yet implemented in Rust worker'); + try { + const tablesResult = await this.listCollections(); + if (!tablesResult.success || !tablesResult.data) { + return { success: false, error: tablesResult.error || 'Failed to list tables' }; + } + + for (const table of tablesResult.data) { + await this.truncate(table); + } + + return { success: true, data: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } } /** @@ -779,17 +949,57 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { } /** - * Clear all data from all collections with reporting - TODO: Implement in Rust worker + * Clear all data from all collections with reporting via Rust worker */ async clearAll(): Promise> { - throw new Error('ClearAll not yet implemented in Rust worker'); + try { + const tablesResult = await this.listCollections(); + if (!tablesResult.success || !tablesResult.data) { + return { success: false, error: tablesResult.error || 'Failed to list tables' }; + } + + const tablesCleared: string[] = []; + for (const table of tablesResult.data) { + const result = await this.truncate(table); + if (result.success) { + tablesCleared.push(table); + } + } + + return { + success: true, + data: { tablesCleared, recordsDeleted: 0 } + }; + } catch (error: any) { + return { success: false, error: error.message }; + } } /** - * Truncate specific collection - TODO: Implement in Rust worker + * Truncate specific collection (delete all rows) via Rust worker */ async truncate(collection: string): Promise> { - throw new Error('Truncate not yet implemented in Rust worker'); + try { + await this.ensureConnected(); + } catch (error: any) { + return { success: false, error: `Connection failed: ${error.message}` }; + } + + try { + const response = await this.sendCommand('data/truncate', { + handle: this.adapterHandle, + collection, + }); + + if (response.status !== 'ok') { + const errorMsg = response.status === 'error' ? response.message : 'Truncate failed'; + return { success: false, error: errorMsg }; + } + + return { success: true, data: true }; + } catch (error: any) { + return { success: false, error: error.message }; + } } /** @@ -884,13 +1094,8 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { // 2. Send query vector to Rust worker with include_data=true // Rust reads corpus vectors from SQLite, computes similarity, AND fetches full records // This eliminates k IPC round trips - Rust returns everything in one response - interface RustVectorSearchResponse { - results: Array<{ id: string; score: number; distance: number; data?: Record }>; - count: number; - corpus_size: number; - } - - const searchResult = await this.sendCommand('vector/search', { + // Response type: RustVectorSearchResult (generated from Rust via ts-rs) + const searchResult = await this.sendCommand('vector/search', { handle: this.adapterHandle, collection, query_vector: toNumberArray(queryVector), @@ -899,9 +1104,10 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { include_data: true // OPTIMIZATION: Get full records in one Rust query }); - if (searchResult.status !== 'ok' || !searchResult.data) { + if (searchResult.status !== 'ok') { // Fallback message for collections without embeddings - if (searchResult.message?.includes('no such column: embedding')) { + const errorMsg = searchResult.status === 'error' ? searchResult.message : ''; + if (errorMsg.includes('no such column: embedding')) { return { success: true, data: { @@ -919,7 +1125,7 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { } return { success: false, - error: searchResult.message || 'Vector search failed in Rust worker' + error: errorMsg || 'Vector search failed in Rust worker' }; } @@ -929,10 +1135,10 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { // 3. Map Rust results directly - no additional IPC round trips needed! // Rust already fetched full records with include_data=true - type RustResult = { id: string; score: number; distance: number; data?: Record }; + // VectorSearchHit type generated from Rust via ts-rs const results: VectorSearchResultType[] = rustResults - .filter((r: RustResult) => r.data) // Only include results that have data - .map((rustResult: RustResult) => { + .filter((r: VectorSearchHit) => r.data) // Only include results that have data + .map((rustResult: VectorSearchHit) => { // Convert snake_case keys from Rust/SQL to camelCase for TypeScript const entityData = this.toCamelCaseObject(rustResult.data!) as T; @@ -996,22 +1202,23 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { deduplicated: boolean; storedAt: string; }> { - const response = await this.sendCommand<{ - hash: string; - size: number; - compressedSize: number; - deduplicated: boolean; - storedAt: string; - }>('blob/store', { + const response = await this.sendCommand('blob/store', { data, base_path: basePath }); - if (response.status !== 'ok' || !response.data) { - throw new Error(response.message || 'Blob store failed'); + if (response.status !== 'ok') { + const errorMsg = response.status === 'error' ? response.message : 'Blob store failed'; + throw new Error(errorMsg); } - return response.data; + return { + hash: response.data.hash, + size: response.data.size, + compressedSize: response.data.compressed_size, + deduplicated: response.data.deduplicated, + storedAt: response.data.stored_at, + }; } /** @@ -1027,10 +1234,11 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { }); if (response.status !== 'ok') { - throw new Error(response.message || 'Blob retrieve failed'); + const errorMsg = response.status === 'error' ? response.message : 'Blob retrieve failed'; + throw new Error(errorMsg); } - return response.data as T; + return response.data; } /** @@ -1039,16 +1247,17 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { * @param basePath - Optional custom blob storage path */ async blobExists(hash: string, basePath?: string): Promise { - const response = await this.sendCommand<{ exists: boolean }>('blob/exists', { + const response = await this.sendCommand('blob/exists', { hash, base_path: basePath }); if (response.status !== 'ok') { - throw new Error(response.message || 'Blob exists check failed'); + const errorMsg = response.status === 'error' ? response.message : 'Blob exists check failed'; + throw new Error(errorMsg); } - return response.data?.exists ?? false; + return response.data.exists; } /** @@ -1058,16 +1267,17 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { * @returns true if deleted, false if not found */ async blobDelete(hash: string, basePath?: string): Promise { - const response = await this.sendCommand<{ deleted: boolean }>('blob/delete', { + const response = await this.sendCommand('blob/delete', { hash, base_path: basePath }); if (response.status !== 'ok') { - throw new Error(response.message || 'Blob delete failed'); + const errorMsg = response.status === 'error' ? response.message : 'Blob delete failed'; + throw new Error(errorMsg); } - return response.data?.deleted ?? false; + return response.data.deleted; } /** @@ -1080,20 +1290,22 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { shardCount: number; basePath: string; }> { - const response = await this.sendCommand<{ - totalBlobs: number; - totalCompressedBytes: number; - shardCount: number; - basePath: string; - }>('blob/stats', { + const response = await this.sendCommand('blob/stats', { base_path: basePath }); - if (response.status !== 'ok' || !response.data) { - throw new Error(response.message || 'Blob stats failed'); + if (response.status !== 'ok') { + const errorMsg = response.status === 'error' ? response.message : 'Blob stats failed'; + throw new Error(errorMsg); } - return response.data; + // Map snake_case wire format (from Rust) to camelCase return type + return { + totalBlobs: response.data.total_blobs, + totalCompressedBytes: response.data.total_compressed_bytes, + shardCount: response.data.shard_count, + basePath: response.data.base_path, + }; } /** @@ -1168,18 +1380,19 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { throw new Error(`Connection failed: ${error.message}`); } - const response = await this.sendCommand<{ items: T[]; count: number }>('data/query', { + const response = await this.sendCommand('data/query', { handle: this.adapterHandle, sql }); if (response.status !== 'ok') { - throw new Error(response.message || 'Raw query failed'); + const errorMsg = response.status === 'error' ? response.message : 'Raw query failed'; + throw new Error(errorMsg); } return { - items: response.data?.items || [], - count: response.data?.count || 0 + items: (response.data.items || []) as T[], + count: response.data.count || 0 }; } @@ -1204,31 +1417,37 @@ export class RustWorkerStorageAdapter extends DataStorageAdapter { } /** - * Close connection to Rust worker + * Close all pool connections to Rust worker */ async close(): Promise { // Close adapter in Rust first - if (this.adapterHandle && this.socket) { + if (this.adapterHandle && this.pool.length > 0) { try { await this.sendCommand('adapter/close', { handle: this.adapterHandle }); - console.log(`✅ Closed SQLite adapter: ${this.adapterHandle}`); + log.info(`Closed SQLite adapter: ${this.adapterHandle}`); } catch (error) { - console.warn('⚠️ Failed to close adapter in Rust:', error); + log.warn(`Failed to close adapter in Rust: ${error}`); } this.adapterHandle = null; } - // Close socket - if (this.socket) { - this.socket.destroy(); - this.socket = null; + // Close all pool connections + for (const conn of this.pool) { + if (conn.pendingResponse) { + clearTimeout(conn.pendingResponse.timeout); + conn.pendingResponse.reject(new Error('Connection closed')); + conn.pendingResponse = null; + } + if (conn.socket && !conn.socket.destroyed) { + conn.socket.destroy(); + } } + this.pool = []; - // Reject pending response - if (this.pendingResponse) { - clearTimeout(this.pendingResponse.timeout); - this.pendingResponse.reject(new Error('Connection closed')); - this.pendingResponse = null; + // Reject all waiters + for (const waiter of this.waitQueue) { + // Can't fulfill — they'll get an error when they try to use the connection } + this.waitQueue = []; } } diff --git a/src/debug/jtag/daemons/data-daemon/shared/DataDaemon.ts b/src/debug/jtag/daemons/data-daemon/shared/DataDaemon.ts index c7be30d3c..cbd90d6b5 100644 --- a/src/debug/jtag/daemons/data-daemon/shared/DataDaemon.ts +++ b/src/debug/jtag/daemons/data-daemon/shared/DataDaemon.ts @@ -206,11 +206,9 @@ export class DataDaemon { // Get adapter for this collection (may be custom adapter like JSON file) const adapter = this.getAdapterForCollection(collection); - // Ensure schema exists (orchestrate table creation via adapter) - // Skip for custom adapters (they handle their own schema) - if (adapter === this.adapter) { - await this.ensureSchema(collection); - } + // Ensure schema exists via default adapter (DDL). + // Custom adapters (Rust) handle DML only — schema creation stays in TypeScript. + await this.ensureSchema(collection); // Validate context and data const validationResult = this.validateOperation(collection, data, context); @@ -275,11 +273,8 @@ export class DataDaemon { // Get adapter for this collection (may be custom adapter like JSON file) const adapter = this.getAdapterForCollection(collection); - // Ensure schema exists before reading (prevents "no such table" errors) - // Skip for custom adapters (they handle their own schema) - if (adapter === this.adapter) { - await this.ensureSchema(collection); - } + // Ensure schema exists via default adapter (DDL). + await this.ensureSchema(collection); const result = await adapter.read(collection, id); @@ -330,11 +325,8 @@ export class DataDaemon { // Get adapter for this collection (may be custom adapter like JSON file) const adapter = this.getAdapterForCollection(query.collection); - // Ensure schema exists before querying (prevents "no such table" errors) - // Skip for custom adapters (they handle their own schema) - if (adapter === this.adapter) { - await this.ensureSchema(query.collection); - } + // Ensure schema exists via default adapter (DDL). + await this.ensureSchema(query.collection); const result = await adapter.query(query); @@ -367,10 +359,8 @@ export class DataDaemon { // Get adapter for this collection (may be custom adapter like JSON file) const adapter = this.getAdapterForCollection(collection); - // Ensure schema exists before updating (prevents "no such table" errors) - if (adapter === this.adapter) { - await this.ensureSchema(collection); - } + // Ensure schema exists via default adapter (DDL). + await this.ensureSchema(collection); // Read existing entity to merge with partial update // TODO: Performance optimization - Consider adding skipValidation flag for trusted internal updates, @@ -425,10 +415,8 @@ export class DataDaemon { // Get adapter for this collection (may be custom adapter like JSON file) const adapter = this.getAdapterForCollection(collection); - // Ensure schema exists before deleting (prevents "no such table" errors) - if (adapter === this.adapter) { - await this.ensureSchema(collection); - } + // Ensure schema exists via default adapter (DDL). + await this.ensureSchema(collection); // Read entity before deletion for event emission const readResult = await adapter.read(collection, id); diff --git a/src/debug/jtag/generated-command-schemas.json b/src/debug/jtag/generated-command-schemas.json index f1d17478a..1eb8aabe1 100644 --- a/src/debug/jtag/generated-command-schemas.json +++ b/src/debug/jtag/generated-command-schemas.json @@ -1,5 +1,5 @@ { - "generated": "2026-02-05T18:10:18.289Z", + "generated": "2026-02-05T21:20:24.680Z", "version": "1.0.0", "commands": [ { diff --git a/src/debug/jtag/generator/generate-rust-bindings.ts b/src/debug/jtag/generator/generate-rust-bindings.ts new file mode 100644 index 000000000..ebfb62d04 --- /dev/null +++ b/src/debug/jtag/generator/generate-rust-bindings.ts @@ -0,0 +1,226 @@ +#!/usr/bin/env tsx +/** + * Rust → TypeScript Binding Generator + * + * Runs ts-rs export tests for all Rust packages that define TypeScript types, + * then generates barrel index.ts files for each output directory. + * + * Output: shared/generated/ (code/, persona/, rag/, ipc/, data-daemon/, etc.) + * + * Run manually: npx tsx generator/generate-rust-bindings.ts + * Runs automatically as part of prebuild (after worker:build compiles Rust). + */ + +import { execSync } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; + +const ROOT = process.cwd(); +const WORKERS_DIR = path.join(ROOT, 'workers'); +const GENERATED_DIR = path.join(ROOT, 'shared', 'generated'); + +/** + * Rust packages that export TypeScript types via ts-rs. + * Each entry maps a cargo package name to its generated output subdirectories. + */ +const TS_RS_PACKAGES = [ + { + package: 'continuum-core', + description: 'Core IPC types (code, persona, rag, ipc, memory, voice)', + // continuum-core exports to multiple subdirs: code/, persona/, rag/, ipc/ + }, + { + package: 'data-daemon-worker', + description: 'Data daemon storage adapter wire types', + // Exports to: data-daemon/ + }, +]; + +/** + * Run cargo test to trigger ts-rs export for a package. + * ts-rs v9 auto-generates export_bindings_* tests for each #[ts(export)] struct. + */ +function generateBindings(pkg: string, description: string): boolean { + console.log(` 🦀 ${pkg}: ${description}`); + try { + execSync( + `cargo test --package ${pkg} export_bindings --quiet`, + { + cwd: WORKERS_DIR, + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 120_000, + } + ); + return true; + } catch (error: any) { + // Check if it's just "no tests matched" (not an error) + const stderr = error.stderr?.toString() || ''; + if (stderr.includes('0 passed') || stderr.includes('running 0 tests')) { + console.log(` ⚠️ No export_bindings tests found — running all tests`); + try { + execSync(`cargo test --package ${pkg} --quiet`, { + cwd: WORKERS_DIR, + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 120_000, + }); + return true; + } catch (innerError: any) { + console.error(` ❌ Failed: ${innerError.stderr?.toString().slice(0, 200)}`); + return false; + } + } + console.error(` ❌ Failed: ${stderr.slice(0, 200)}`); + return false; + } +} + +/** + * Scan a directory for generated .ts files and create a barrel index.ts + */ +function generateBarrelExport(dir: string): void { + if (!fs.existsSync(dir)) return; + + const files = fs.readdirSync(dir) + .filter(f => f.endsWith('.ts') && f !== 'index.ts') + .sort(); + + if (files.length === 0) return; + + const dirName = path.basename(dir); + const exports = files + .map(f => { + const typeName = f.replace('.ts', ''); + return `export type { ${typeName} } from './${typeName}';`; + }) + .join('\n'); + + const content = `// Auto-generated barrel export — do not edit manually +// Source: generator/generate-rust-bindings.ts +// Re-generate: npx tsx generator/generate-rust-bindings.ts + +${exports} +`; + + fs.writeFileSync(path.join(dir, 'index.ts'), content); + console.log(` 📦 ${dirName}/index.ts (${files.length} types)`); +} + +/** + * Parse the exported type name(s) from a ts-rs generated .ts file. + * ts-rs files use: export type TypeName = { ... }; + * The type name may not match the filename (e.g., RagTypes.ts exports MessageRole). + */ +function parseExportedTypes(filePath: string): string[] { + const content = fs.readFileSync(filePath, 'utf-8'); + const matches = content.matchAll(/export type\s+(\w+)/g); + return Array.from(matches, m => m[1]); +} + +/** + * Generate the master barrel at shared/generated/index.ts + */ +function generateMasterBarrel(): void { + const subdirs = fs.readdirSync(GENERATED_DIR, { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => d.name) + .sort(); + + // Collect top-level .ts files (e.g., CallMessage.ts) + const topLevelFiles = fs.readdirSync(GENERATED_DIR) + .filter(f => f.endsWith('.ts') && f !== 'index.ts') + .sort(); + + // Collect all type names exported by subdirectories (to detect duplicates) + const subdirTypes = new Set(); + for (const dir of subdirs) { + const dirPath = path.join(GENERATED_DIR, dir); + const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.ts') && f !== 'index.ts'); + for (const f of files) { + const types = parseExportedTypes(path.join(dirPath, f)); + types.forEach(t => subdirTypes.add(t)); + } + } + + const lines: string[] = [ + '// Auto-generated master barrel — do not edit manually', + '// Source: generator/generate-rust-bindings.ts', + '// Re-generate: npx tsx generator/generate-rust-bindings.ts', + '', + ]; + + // Re-export subdirectories + for (const dir of subdirs) { + const indexPath = path.join(GENERATED_DIR, dir, 'index.ts'); + if (fs.existsSync(indexPath)) { + lines.push(`export * from './${dir}';`); + } + } + + // Re-export top-level files, skipping types already exported by subdirectories + for (const file of topLevelFiles) { + const filePath = path.join(GENERATED_DIR, file); + const types = parseExportedTypes(filePath); + const moduleName = file.replace('.ts', ''); + + for (const typeName of types) { + if (subdirTypes.has(typeName)) { + console.log(` ⚠️ Skipping ${file} → ${typeName} (already exported by subdirectory)`); + continue; + } + lines.push(`export type { ${typeName} } from './${moduleName}';`); + } + } + + lines.push(''); + + fs.writeFileSync(path.join(GENERATED_DIR, 'index.ts'), lines.join('\n')); + console.log(` 📦 index.ts (master barrel)`); +} + +async function main() { + console.log('🔧 Generating Rust → TypeScript bindings via ts-rs...\n'); + + // Ensure output directory exists + fs.mkdirSync(GENERATED_DIR, { recursive: true }); + + // Step 1: Run cargo test for each package to generate .ts files + let allSuccess = true; + for (const pkg of TS_RS_PACKAGES) { + const ok = generateBindings(pkg.package, pkg.description); + if (!ok) allSuccess = false; + } + + if (!allSuccess) { + console.error('\n❌ Some bindings failed to generate'); + process.exit(1); + } + + console.log(''); + + // Step 2: Generate barrel index.ts for each subdirectory + console.log('📦 Generating barrel exports...'); + const subdirs = fs.readdirSync(GENERATED_DIR, { withFileTypes: true }) + .filter(d => d.isDirectory()) + .map(d => d.name); + + for (const dir of subdirs) { + generateBarrelExport(path.join(GENERATED_DIR, dir)); + } + + // Step 3: Generate master barrel + generateMasterBarrel(); + + // Count total types + let totalTypes = 0; + for (const dir of subdirs) { + const dirPath = path.join(GENERATED_DIR, dir); + totalTypes += fs.readdirSync(dirPath).filter(f => f.endsWith('.ts') && f !== 'index.ts').length; + } + + console.log(`\n✅ Generated ${totalTypes} TypeScript types from Rust via ts-rs`); +} + +main().catch(error => { + console.error('❌ Rust binding generation failed:', error); + process.exit(1); +}); diff --git a/src/debug/jtag/package-lock.json b/src/debug/jtag/package-lock.json index 1a93bf2a5..c6f204524 100644 --- a/src/debug/jtag/package-lock.json +++ b/src/debug/jtag/package-lock.json @@ -1,12 +1,12 @@ { "name": "@continuum/jtag", - "version": "1.0.7622", + "version": "1.0.7629", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@continuum/jtag", - "version": "1.0.7622", + "version": "1.0.7629", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/debug/jtag/package.json b/src/debug/jtag/package.json index 87dbbf95c..4e2f49063 100644 --- a/src/debug/jtag/package.json +++ b/src/debug/jtag/package.json @@ -1,6 +1,6 @@ { "name": "@continuum/jtag", - "version": "1.0.7622", + "version": "1.0.7629", "description": "Global CLI debugging system for any Node.js project. Install once globally, use anywhere: npm install -g @continuum/jtag", "config": { "active_example": "widget-ui", @@ -138,7 +138,7 @@ "clean:logs": "find .continuum/jtag/logs -name '*.log' -type f -delete 2>/dev/null || true; find .continuum/personas -name '*.log' -type f -delete 2>/dev/null || true; rm -f /tmp/jtag-*-timing.jsonl 2>/dev/null || true; echo '✅ Cleaned all log files (system + persona + timing logs)'", "prepare": "npx tsx scripts/ensure-config.ts 2>/dev/null || true", "postinstall": "npm run worker:models", - "prebuild": "npx tsx scripts/ensure-config.ts && npm run version:bump && npm run clean:all && npm run worker:models && npm run worker:build && npx tsx generator/generate-structure.ts && npx tsx generator/generate-command-schemas.ts && npx tsx generator/generate-command-constants.ts && npx tsx scripts/compile-sass.ts", + "prebuild": "npx tsx scripts/ensure-config.ts && npm run version:bump && npm run clean:all && npm run worker:models && npm run worker:build && npx tsx generator/generate-rust-bindings.ts && npx tsx generator/generate-structure.ts && npx tsx generator/generate-command-schemas.ts && npx tsx generator/generate-command-constants.ts && npx tsx scripts/compile-sass.ts", "build:ts": "npx tsx generator/generate-version.ts && npx tsx generator/generate-config.ts && npx tsx scripts/build-with-loud-failure.ts", "build:cli": "npx esbuild dist/cli.js --bundle --platform=node --target=node18 --outfile=dist/cli-bundle.js --external:sqlite3 --external:better-sqlite3 --external:@anthropic-ai/sdk --external:@grpc/grpc-js --external:@grpc/proto-loader --minify 2>/dev/null && echo '✅ CLI bundle created (6.6MB)'", "lint": "eslint . --ext .ts --fix-dry-run && tsc --noEmit --project .", diff --git a/src/debug/jtag/shared/generated/CallMessage.ts b/src/debug/jtag/shared/generated/CallMessage.ts deleted file mode 100644 index 505f1ed11..000000000 --- a/src/debug/jtag/shared/generated/CallMessage.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Message types for call protocol - * TypeScript types are generated via `cargo test -p streaming-core export_types` - */ -export type CallMessage = { "type": "Join", call_id: string, user_id: string, display_name: string, is_ai: boolean, } | { "type": "Leave" } | { "type": "Audio", data: string, } | { "type": "Mute", muted: boolean, } | { "type": "ParticipantJoined", user_id: string, display_name: string, } | { "type": "ParticipantLeft", user_id: string, } | { "type": "Error", message: string, } | { "type": "Stats", participant_count: number, samples_processed: bigint, } | { "type": "Transcription", user_id: string, display_name: string, text: string, confidence: number, language: string, }; diff --git a/src/debug/jtag/shared/generated/RagTypes.ts b/src/debug/jtag/shared/generated/RagTypes.ts deleted file mode 100644 index 43dd7c809..000000000 --- a/src/debug/jtag/shared/generated/RagTypes.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Message role in conversation - */ -export type MessageRole = "system" | "user" | "assistant"; diff --git a/src/debug/jtag/shared/generated/code/ChangeNode.ts b/src/debug/jtag/shared/generated/code/ChangeNode.ts deleted file mode 100644 index bd89c9e7b..000000000 --- a/src/debug/jtag/shared/generated/code/ChangeNode.ts +++ /dev/null @@ -1,44 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { FileDiff } from "./FileDiff"; -import type { FileOperation } from "./FileOperation"; - -/** - * Every file operation creates a ChangeNode in the DAG. - */ -export type ChangeNode = { id: string, -/** - * Parent node IDs. Empty for root operations. Multiple for merges. - */ -parent_ids: Array, -/** - * Who performed this operation (persona UUID string). - */ -author_id: string, -/** - * When the operation occurred (unix millis). - */ -timestamp: number, -/** - * The file affected (relative to workspace root). - */ -file_path: string, -/** - * The operation type. - */ -operation: FileOperation, -/** - * Forward diff (apply to go forward in time). - */ -forward_diff: FileDiff, -/** - * Reverse diff (apply to go backward in time — undo). - */ -reverse_diff: FileDiff, -/** - * Optional description from the AI about what this change does. - */ -description?: string, -/** - * Workspace ID this change belongs to. - */ -workspace_id: string, }; diff --git a/src/debug/jtag/shared/generated/code/ClassifiedLine.ts b/src/debug/jtag/shared/generated/code/ClassifiedLine.ts deleted file mode 100644 index ca9785451..000000000 --- a/src/debug/jtag/shared/generated/code/ClassifiedLine.ts +++ /dev/null @@ -1,27 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { OutputClassification } from "./OutputClassification"; - -/** - * A single line of classified shell output. - */ -export type ClassifiedLine = { -/** - * The raw text content of the line. - */ -text: string, -/** - * Classification assigned by sentinel rules. - */ -classification: OutputClassification, -/** - * Line number within the stream (0-indexed from execution start). - */ -line_number: number, -/** - * Which stream this line came from: "stdout" or "stderr". - */ -stream: string, -/** - * Unix timestamp in milliseconds when the line was classified. - */ -timestamp: number, }; diff --git a/src/debug/jtag/shared/generated/code/DiffHunk.ts b/src/debug/jtag/shared/generated/code/DiffHunk.ts deleted file mode 100644 index d14968fed..000000000 --- a/src/debug/jtag/shared/generated/code/DiffHunk.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * A single hunk in a unified diff. - */ -export type DiffHunk = { old_start: number, old_count: number, new_start: number, new_count: number, -/** - * The hunk content (with +/- prefixes on each line). - */ -content: string, }; diff --git a/src/debug/jtag/shared/generated/code/EditMode.ts b/src/debug/jtag/shared/generated/code/EditMode.ts deleted file mode 100644 index 5897d1236..000000000 --- a/src/debug/jtag/shared/generated/code/EditMode.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * How to edit a file (four modes). - */ -export type EditMode = { "type": "line_range", start_line: number, end_line: number, new_content: string, } | { "type": "search_replace", search: string, replace: string, all: boolean, } | { "type": "insert_at", line: number, content: string, } | { "type": "append", content: string, }; diff --git a/src/debug/jtag/shared/generated/code/FileDiff.ts b/src/debug/jtag/shared/generated/code/FileDiff.ts deleted file mode 100644 index 1355db62c..000000000 --- a/src/debug/jtag/shared/generated/code/FileDiff.ts +++ /dev/null @@ -1,15 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { DiffHunk } from "./DiffHunk"; - -/** - * A file diff consisting of hunks. - */ -export type FileDiff = { -/** - * Unified diff text (compatible with standard tooling). - */ -unified: string, -/** - * Structured hunks for programmatic application. - */ -hunks: Array, }; diff --git a/src/debug/jtag/shared/generated/code/FileOperation.ts b/src/debug/jtag/shared/generated/code/FileOperation.ts deleted file mode 100644 index ade4b896c..000000000 --- a/src/debug/jtag/shared/generated/code/FileOperation.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * File operation types. - */ -export type FileOperation = "create" | "write" | "edit" | "delete" | { "rename": { from: string, to: string, } } | { "undo": { reverted_id: string, } }; diff --git a/src/debug/jtag/shared/generated/code/GitStatusInfo.ts b/src/debug/jtag/shared/generated/code/GitStatusInfo.ts deleted file mode 100644 index 361bd9a85..000000000 --- a/src/debug/jtag/shared/generated/code/GitStatusInfo.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Git status information. - */ -export type GitStatusInfo = { success: boolean, branch?: string, modified: Array, added: Array, deleted: Array, untracked: Array, error?: string, }; diff --git a/src/debug/jtag/shared/generated/code/HistoryResult.ts b/src/debug/jtag/shared/generated/code/HistoryResult.ts deleted file mode 100644 index 35c609807..000000000 --- a/src/debug/jtag/shared/generated/code/HistoryResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ChangeNode } from "./ChangeNode"; - -/** - * History query result. - */ -export type HistoryResult = { success: boolean, nodes: Array, total_count: number, error?: string, }; diff --git a/src/debug/jtag/shared/generated/code/OutputClassification.ts b/src/debug/jtag/shared/generated/code/OutputClassification.ts deleted file mode 100644 index 89b9396d5..000000000 --- a/src/debug/jtag/shared/generated/code/OutputClassification.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Classification level for a line of shell output. - */ -export type OutputClassification = "Error" | "Warning" | "Info" | "Success" | "Verbose"; diff --git a/src/debug/jtag/shared/generated/code/ReadResult.ts b/src/debug/jtag/shared/generated/code/ReadResult.ts deleted file mode 100644 index aaec959ca..000000000 --- a/src/debug/jtag/shared/generated/code/ReadResult.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Result of a file read operation. - */ -export type ReadResult = { success: boolean, content?: string, file_path: string, total_lines: number, lines_returned: number, start_line: number, end_line: number, size_bytes: number, error?: string, }; diff --git a/src/debug/jtag/shared/generated/code/SearchMatch.ts b/src/debug/jtag/shared/generated/code/SearchMatch.ts deleted file mode 100644 index 787fa78e7..000000000 --- a/src/debug/jtag/shared/generated/code/SearchMatch.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * A single search match. - */ -export type SearchMatch = { file_path: string, line_number: number, line_content: string, match_start: number, match_end: number, }; diff --git a/src/debug/jtag/shared/generated/code/SearchResult.ts b/src/debug/jtag/shared/generated/code/SearchResult.ts deleted file mode 100644 index cd63567d9..000000000 --- a/src/debug/jtag/shared/generated/code/SearchResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { SearchMatch } from "./SearchMatch"; - -/** - * Result of a code search operation. - */ -export type SearchResult = { success: boolean, matches: Array, total_matches: number, files_searched: number, error?: string, }; diff --git a/src/debug/jtag/shared/generated/code/SentinelAction.ts b/src/debug/jtag/shared/generated/code/SentinelAction.ts deleted file mode 100644 index cd6f65aa1..000000000 --- a/src/debug/jtag/shared/generated/code/SentinelAction.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * What to do with a line that matches a sentinel rule. - */ -export type SentinelAction = "Emit" | "Suppress"; diff --git a/src/debug/jtag/shared/generated/code/SentinelRule.ts b/src/debug/jtag/shared/generated/code/SentinelRule.ts deleted file mode 100644 index 5524c117d..000000000 --- a/src/debug/jtag/shared/generated/code/SentinelRule.ts +++ /dev/null @@ -1,23 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { OutputClassification } from "./OutputClassification"; -import type { SentinelAction } from "./SentinelAction"; - -/** - * A sentinel filter rule: regex pattern → classification + action. - * - * Wire type for IPC. Patterns are compiled to `regex::Regex` on the Rust side - * when `set_sentinel()` is called. - */ -export type SentinelRule = { -/** - * Regex pattern to match against each output line. - */ -pattern: string, -/** - * Classification to assign when this rule matches. - */ -classification: OutputClassification, -/** - * Whether to include or suppress the matched line. - */ -action: SentinelAction, }; diff --git a/src/debug/jtag/shared/generated/code/ShellExecuteResponse.ts b/src/debug/jtag/shared/generated/code/ShellExecuteResponse.ts deleted file mode 100644 index 2f74b0c16..000000000 --- a/src/debug/jtag/shared/generated/code/ShellExecuteResponse.ts +++ /dev/null @@ -1,22 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ShellExecutionStatus } from "./ShellExecutionStatus"; - -/** - * Response from `code/shell-execute`. - * - * Always returns immediately with the execution handle. - * If `wait: true` was specified, also includes the completed result. - */ -export type ShellExecuteResponse = { execution_id: string, status: ShellExecutionStatus, -/** - * Full stdout (only present when `wait: true` and execution completed). - */ -stdout?: string, -/** - * Full stderr (only present when `wait: true` and execution completed). - */ -stderr?: string, -/** - * Exit code (only present when execution completed). - */ -exit_code?: number, }; diff --git a/src/debug/jtag/shared/generated/code/ShellExecutionStatus.ts b/src/debug/jtag/shared/generated/code/ShellExecutionStatus.ts deleted file mode 100644 index cfd88cc51..000000000 --- a/src/debug/jtag/shared/generated/code/ShellExecutionStatus.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Status of a shell command execution. - */ -export type ShellExecutionStatus = "running" | "completed" | "failed" | "timed_out" | "killed"; diff --git a/src/debug/jtag/shared/generated/code/ShellHistoryEntry.ts b/src/debug/jtag/shared/generated/code/ShellHistoryEntry.ts deleted file mode 100644 index 5984d5ab5..000000000 --- a/src/debug/jtag/shared/generated/code/ShellHistoryEntry.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * A history entry for a completed execution. - */ -export type ShellHistoryEntry = { execution_id: string, command: string, exit_code?: number, started_at: number, finished_at?: number, }; diff --git a/src/debug/jtag/shared/generated/code/ShellPollResponse.ts b/src/debug/jtag/shared/generated/code/ShellPollResponse.ts deleted file mode 100644 index 9fbf317e3..000000000 --- a/src/debug/jtag/shared/generated/code/ShellPollResponse.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ShellExecutionStatus } from "./ShellExecutionStatus"; - -/** - * Response from `code/shell-poll`. - * - * Returns new output since the last poll (cursor-based). - * Call repeatedly until `finished` is true. - */ -export type ShellPollResponse = { execution_id: string, status: ShellExecutionStatus, -/** - * New stdout lines since last poll. - */ -new_stdout: Array, -/** - * New stderr lines since last poll. - */ -new_stderr: Array, -/** - * Exit code (present when finished). - */ -exit_code?: number, -/** - * True when the execution is no longer running. - */ -finished: boolean, }; diff --git a/src/debug/jtag/shared/generated/code/ShellSessionInfo.ts b/src/debug/jtag/shared/generated/code/ShellSessionInfo.ts deleted file mode 100644 index 9101eb5ed..000000000 --- a/src/debug/jtag/shared/generated/code/ShellSessionInfo.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Response from `code/shell-status` — session metadata. - */ -export type ShellSessionInfo = { session_id: string, persona_id: string, cwd: string, workspace_root: string, active_executions: number, total_executions: number, }; diff --git a/src/debug/jtag/shared/generated/code/ShellWatchResponse.ts b/src/debug/jtag/shared/generated/code/ShellWatchResponse.ts deleted file mode 100644 index 120185d46..000000000 --- a/src/debug/jtag/shared/generated/code/ShellWatchResponse.ts +++ /dev/null @@ -1,23 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ClassifiedLine } from "./ClassifiedLine"; - -/** - * Response from `code/shell-watch`. - * - * Returns classified output lines since the last watch call. - * Blocks until output is available (no timeout, no polling). - * Call in a loop until `finished` is true. - */ -export type ShellWatchResponse = { execution_id: string, -/** - * Classified output lines (filtered through sentinel rules). - */ -lines: Array, -/** - * True when the execution is no longer running. - */ -finished: boolean, -/** - * Exit code (present when finished). - */ -exit_code?: number, }; diff --git a/src/debug/jtag/shared/generated/code/TreeNode.ts b/src/debug/jtag/shared/generated/code/TreeNode.ts deleted file mode 100644 index b79d6a206..000000000 --- a/src/debug/jtag/shared/generated/code/TreeNode.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * A node in a directory tree. - */ -export type TreeNode = { name: string, path: string, is_directory: boolean, size_bytes?: number, children: Array, }; diff --git a/src/debug/jtag/shared/generated/code/TreeResult.ts b/src/debug/jtag/shared/generated/code/TreeResult.ts deleted file mode 100644 index 28579a140..000000000 --- a/src/debug/jtag/shared/generated/code/TreeResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { TreeNode } from "./TreeNode"; - -/** - * Result of a tree operation. - */ -export type TreeResult = { success: boolean, root?: TreeNode, total_files: number, total_directories: number, error?: string, }; diff --git a/src/debug/jtag/shared/generated/code/UndoResult.ts b/src/debug/jtag/shared/generated/code/UndoResult.ts deleted file mode 100644 index ceef6a42a..000000000 --- a/src/debug/jtag/shared/generated/code/UndoResult.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { WriteResult } from "./WriteResult"; - -/** - * Result of an undo operation. - */ -export type UndoResult = { success: boolean, changes_undone: Array, error?: string, }; diff --git a/src/debug/jtag/shared/generated/code/WriteResult.ts b/src/debug/jtag/shared/generated/code/WriteResult.ts deleted file mode 100644 index ce9e73157..000000000 --- a/src/debug/jtag/shared/generated/code/WriteResult.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Result of a file write/edit/delete operation. - */ -export type WriteResult = { success: boolean, -/** - * UUID of the ChangeNode created. - */ -change_id?: string, file_path: string, bytes_written: number, error?: string, }; diff --git a/src/debug/jtag/shared/generated/code/index.ts b/src/debug/jtag/shared/generated/code/index.ts deleted file mode 100644 index d258627e9..000000000 --- a/src/debug/jtag/shared/generated/code/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Code Module Types - Generated from Rust (single source of truth) -// Re-run: cargo test --package continuum-core --lib export_bindings - -// Core change graph types -export type { ChangeNode } from './ChangeNode'; -export type { FileOperation } from './FileOperation'; -export type { FileDiff } from './FileDiff'; -export type { DiffHunk } from './DiffHunk'; - -// Edit modes (discriminated union) -export type { EditMode } from './EditMode'; - -// Operation results -export type { WriteResult } from './WriteResult'; -export type { ReadResult } from './ReadResult'; -export type { UndoResult } from './UndoResult'; -export type { HistoryResult } from './HistoryResult'; - -// Search -export type { SearchMatch } from './SearchMatch'; -export type { SearchResult } from './SearchResult'; - -// Tree -export type { TreeNode } from './TreeNode'; -export type { TreeResult } from './TreeResult'; - -// Git -export type { GitStatusInfo } from './GitStatusInfo'; - -// Shell Session -export type { ShellExecutionStatus } from './ShellExecutionStatus'; -export type { ShellExecuteResponse } from './ShellExecuteResponse'; -export type { ShellPollResponse } from './ShellPollResponse'; -export type { ShellSessionInfo } from './ShellSessionInfo'; -export type { ShellHistoryEntry } from './ShellHistoryEntry'; - -// Shell Watch + Sentinel -export type { OutputClassification } from './OutputClassification'; -export type { SentinelAction } from './SentinelAction'; -export type { SentinelRule } from './SentinelRule'; -export type { ClassifiedLine } from './ClassifiedLine'; -export type { ShellWatchResponse } from './ShellWatchResponse'; diff --git a/src/debug/jtag/shared/generated/index.ts b/src/debug/jtag/shared/generated/index.ts deleted file mode 100644 index 2241c540f..000000000 --- a/src/debug/jtag/shared/generated/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Generated Types - Single Source of Truth from Rust -// All types are generated via ts-rs from Rust structs -// Re-run: cargo test --package continuum-core --lib export_bindings - -// RAG types -export * from './rag'; - -// Persona cognition types -export * from './persona'; - -// IPC protocol types -export * from './ipc'; - -// Voice call types (already generated) -export type { CallMessage } from './CallMessage'; - -// Code module types (file operations, change graph, search, tree) -export * from './code'; diff --git a/src/debug/jtag/shared/generated/ipc/InboxMessageRequest.ts b/src/debug/jtag/shared/generated/ipc/InboxMessageRequest.ts deleted file mode 100644 index 72cd3c29c..000000000 --- a/src/debug/jtag/shared/generated/ipc/InboxMessageRequest.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Inbox message for IPC (mirrors InboxMessage but with string UUIDs for JSON transport) - */ -export type InboxMessageRequest = { id: string, room_id: string, sender_id: string, sender_name: string, sender_type: string, content: string, -/** - * Timestamp in milliseconds (fits in JS number, max safe ~9 quadrillion) - */ -timestamp: number, priority: number, source_modality?: string, voice_session_id?: string, }; diff --git a/src/debug/jtag/shared/generated/ipc/index.ts b/src/debug/jtag/shared/generated/ipc/index.ts deleted file mode 100644 index ffaaba502..000000000 --- a/src/debug/jtag/shared/generated/ipc/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -// IPC Types - Generated from Rust (single source of truth) -// Re-run: cargo test --package continuum-core --lib export_bindings - -export type { InboxMessageRequest } from './InboxMessageRequest'; - -// Re-export cognition types used in IPC responses -export type { - CognitionDecision, - PriorityScore, - PriorityFactors, -} from '../persona'; - -// Re-export persona state for IPC responses -export type { PersonaState, Mood } from '../persona'; diff --git a/src/debug/jtag/shared/generated/persona/ActivityDomain.ts b/src/debug/jtag/shared/generated/persona/ActivityDomain.ts deleted file mode 100644 index d8bc0a79a..000000000 --- a/src/debug/jtag/shared/generated/persona/ActivityDomain.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Activity domain for channel routing. - * Each domain has one ChannelQueue. Items route to their domain's queue. - */ -export type ActivityDomain = "AUDIO" | "CHAT" | "CODE" | "BACKGROUND"; diff --git a/src/debug/jtag/shared/generated/persona/ChannelEnqueueRequest.ts b/src/debug/jtag/shared/generated/persona/ChannelEnqueueRequest.ts deleted file mode 100644 index fa0d4f42b..000000000 --- a/src/debug/jtag/shared/generated/persona/ChannelEnqueueRequest.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * IPC request to enqueue any item type. Discriminated by `item_type` field. - */ -export type ChannelEnqueueRequest = { "item_type": "voice", id: string, room_id: string, content: string, sender_id: string, sender_name: string, sender_type: string, voice_session_id: string, timestamp: number, priority: number, } | { "item_type": "chat", id: string, room_id: string, content: string, sender_id: string, sender_name: string, sender_type: string, mentions: boolean, timestamp: number, priority: number, } | { "item_type": "task", id: string, task_id: string, assignee_id: string, created_by: string, task_domain: string, task_type: string, context_id: string, description: string, priority: number, status: string, timestamp: number, due_date: bigint | null, estimated_duration: bigint | null, depends_on: Array, blocked_by: Array, } | { "item_type": "code", id: string, room_id: string, persona_id: string, task_description: string, workspace_handle: string, priority: number, is_review: boolean, timestamp: number, }; diff --git a/src/debug/jtag/shared/generated/persona/ChannelRegistryStatus.ts b/src/debug/jtag/shared/generated/persona/ChannelRegistryStatus.ts deleted file mode 100644 index 353cd5425..000000000 --- a/src/debug/jtag/shared/generated/persona/ChannelRegistryStatus.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ChannelStatus } from "./ChannelStatus"; - -/** - * Full channel registry status - */ -export type ChannelRegistryStatus = { channels: Array, total_size: number, has_urgent_work: boolean, has_work: boolean, }; diff --git a/src/debug/jtag/shared/generated/persona/ChannelStatus.ts b/src/debug/jtag/shared/generated/persona/ChannelStatus.ts deleted file mode 100644 index 3f6e0ebb6..000000000 --- a/src/debug/jtag/shared/generated/persona/ChannelStatus.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ActivityDomain } from "./ActivityDomain"; - -/** - * Per-channel status snapshot - */ -export type ChannelStatus = { domain: ActivityDomain, size: number, has_urgent: boolean, has_work: boolean, }; diff --git a/src/debug/jtag/shared/generated/persona/CognitionDecision.ts b/src/debug/jtag/shared/generated/persona/CognitionDecision.ts deleted file mode 100644 index 954a1e1b7..000000000 --- a/src/debug/jtag/shared/generated/persona/CognitionDecision.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Decision result from cognition engine - */ -export type CognitionDecision = { should_respond: boolean, confidence: number, reason: string, decision_time_ms: number, fast_path_used: boolean, }; diff --git a/src/debug/jtag/shared/generated/persona/ConsolidatedContext.ts b/src/debug/jtag/shared/generated/persona/ConsolidatedContext.ts deleted file mode 100644 index 8268efd04..000000000 --- a/src/debug/jtag/shared/generated/persona/ConsolidatedContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Context from a prior message consolidated into this chat item. - */ -export type ConsolidatedContext = { sender_id: string, sender_name: string, content: string, timestamp: bigint, }; diff --git a/src/debug/jtag/shared/generated/persona/InboxMessage.ts b/src/debug/jtag/shared/generated/persona/InboxMessage.ts deleted file mode 100644 index ef5125f99..000000000 --- a/src/debug/jtag/shared/generated/persona/InboxMessage.ts +++ /dev/null @@ -1,5 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Modality } from "./Modality"; -import type { SenderType } from "./SenderType"; - -export type InboxMessage = { id: string, room_id: string, sender_id: string, sender_name: string, sender_type: SenderType, content: string, timestamp: bigint, priority: number, source_modality?: Modality, voice_session_id?: string, }; diff --git a/src/debug/jtag/shared/generated/persona/InboxTask.ts b/src/debug/jtag/shared/generated/persona/InboxTask.ts deleted file mode 100644 index 83c9d43a2..000000000 --- a/src/debug/jtag/shared/generated/persona/InboxTask.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Task item for the persona inbox - */ -export type InboxTask = { id: string, domain: string, description: string, assigned_by: string, timestamp: bigint, priority: number, deadline: bigint | null, }; diff --git a/src/debug/jtag/shared/generated/persona/Modality.ts b/src/debug/jtag/shared/generated/persona/Modality.ts deleted file mode 100644 index 529c0ac2e..000000000 --- a/src/debug/jtag/shared/generated/persona/Modality.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Input modality for messages - */ -export type Modality = "chat" | "voice"; diff --git a/src/debug/jtag/shared/generated/persona/Mood.ts b/src/debug/jtag/shared/generated/persona/Mood.ts deleted file mode 100644 index 12f3ecf52..000000000 --- a/src/debug/jtag/shared/generated/persona/Mood.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Mood states based on internal state - */ -export type Mood = "active" | "tired" | "overwhelmed" | "idle"; diff --git a/src/debug/jtag/shared/generated/persona/PersonaState.ts b/src/debug/jtag/shared/generated/persona/PersonaState.ts deleted file mode 100644 index 7785fb4c8..000000000 --- a/src/debug/jtag/shared/generated/persona/PersonaState.ts +++ /dev/null @@ -1,35 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Mood } from "./Mood"; - -/** - * Persona internal state - energy, attention, mood - */ -export type PersonaState = { -/** - * Energy level 0.0-1.0 (depletes with work, recovers with rest) - */ -energy: number, -/** - * Attention level 0.0-1.0 (focus capacity) - */ -attention: number, -/** - * Current mood derived from state - */ -mood: Mood, -/** - * Current inbox load (pending items) - */ -inbox_load: number, -/** - * Last activity timestamp (unix ms) - */ -last_activity_time: bigint, -/** - * Responses in current window - */ -response_count: number, -/** - * Compute budget remaining (rate limiting) - */ -compute_budget: number, }; diff --git a/src/debug/jtag/shared/generated/persona/PriorityFactors.ts b/src/debug/jtag/shared/generated/persona/PriorityFactors.ts deleted file mode 100644 index 54abdee1e..000000000 --- a/src/debug/jtag/shared/generated/persona/PriorityFactors.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Factors contributing to priority - */ -export type PriorityFactors = { recency_score: number, mention_score: number, room_score: number, sender_score: number, voice_boost: number, }; diff --git a/src/debug/jtag/shared/generated/persona/PriorityScore.ts b/src/debug/jtag/shared/generated/persona/PriorityScore.ts deleted file mode 100644 index ece5523fd..000000000 --- a/src/debug/jtag/shared/generated/persona/PriorityScore.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { PriorityFactors } from "./PriorityFactors"; - -/** - * Priority calculation result - */ -export type PriorityScore = { score: number, factors: PriorityFactors, }; diff --git a/src/debug/jtag/shared/generated/persona/QueueItem.ts b/src/debug/jtag/shared/generated/persona/QueueItem.ts deleted file mode 100644 index 88c2d9a4c..000000000 --- a/src/debug/jtag/shared/generated/persona/QueueItem.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { InboxMessage } from "./InboxMessage"; -import type { InboxTask } from "./InboxTask"; - -/** - * Discriminated union of queue items - */ -export type QueueItem = { "type": "Message" } & InboxMessage | { "type": "Task" } & InboxTask; diff --git a/src/debug/jtag/shared/generated/persona/SenderType.ts b/src/debug/jtag/shared/generated/persona/SenderType.ts deleted file mode 100644 index 9f76707ca..000000000 --- a/src/debug/jtag/shared/generated/persona/SenderType.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Type of entity sending a message - */ -export type SenderType = "human" | "persona" | "agent" | "system"; diff --git a/src/debug/jtag/shared/generated/persona/ServiceCycleResult.ts b/src/debug/jtag/shared/generated/persona/ServiceCycleResult.ts deleted file mode 100644 index 93c252152..000000000 --- a/src/debug/jtag/shared/generated/persona/ServiceCycleResult.ts +++ /dev/null @@ -1,28 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { ActivityDomain } from "./ActivityDomain"; -import type { ChannelRegistryStatus } from "./ChannelRegistryStatus"; - -/** - * Result from service_cycle() — what the TS loop should do next - */ -export type ServiceCycleResult = { -/** - * Should TS process an item? - */ -should_process: boolean, -/** - * The item to process (serialized). Null if should_process is false. - */ -item?: any, -/** - * Which domain the item came from - */ -channel?: ActivityDomain, -/** - * How long TS should sleep if no work (adaptive cadence from PersonaState) - */ -wait_ms: bigint, -/** - * Current channel sizes for monitoring - */ -stats: ChannelRegistryStatus, }; diff --git a/src/debug/jtag/shared/generated/persona/index.ts b/src/debug/jtag/shared/generated/persona/index.ts deleted file mode 100644 index b1237e645..000000000 --- a/src/debug/jtag/shared/generated/persona/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Persona Cognition Types - Generated from Rust (single source of truth) -// Re-run: cargo test --package continuum-core --lib export_bindings - -// Core types -export type { SenderType } from './SenderType'; -export type { Modality } from './Modality'; -export type { Mood } from './Mood'; - -// Inbox items -export type { InboxMessage } from './InboxMessage'; -export type { InboxTask } from './InboxTask'; -export type { QueueItem } from './QueueItem'; - -// State management -export type { PersonaState } from './PersonaState'; - -// Decision types -export type { CognitionDecision } from './CognitionDecision'; -export type { PriorityScore } from './PriorityScore'; -export type { PriorityFactors } from './PriorityFactors'; - -// Channel system types -export type { ActivityDomain } from './ActivityDomain'; -export type { ChannelStatus } from './ChannelStatus'; -export type { ChannelRegistryStatus } from './ChannelRegistryStatus'; -export type { ChannelEnqueueRequest } from './ChannelEnqueueRequest'; -export type { ServiceCycleResult } from './ServiceCycleResult'; -export type { ConsolidatedContext } from './ConsolidatedContext'; diff --git a/src/debug/jtag/shared/generated/rag/LlmMessage.ts b/src/debug/jtag/shared/generated/rag/LlmMessage.ts deleted file mode 100644 index 9546ae8d3..000000000 --- a/src/debug/jtag/shared/generated/rag/LlmMessage.ts +++ /dev/null @@ -1,7 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { MessageRole } from "./MessageRole"; - -/** - * LLM message format - */ -export type LlmMessage = { role: MessageRole, content: string, name: string | null, timestamp: bigint | null, }; diff --git a/src/debug/jtag/shared/generated/rag/MessageRole.ts b/src/debug/jtag/shared/generated/rag/MessageRole.ts deleted file mode 100644 index 43dd7c809..000000000 --- a/src/debug/jtag/shared/generated/rag/MessageRole.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Message role in conversation - */ -export type MessageRole = "system" | "user" | "assistant"; diff --git a/src/debug/jtag/shared/generated/rag/RagContext.ts b/src/debug/jtag/shared/generated/rag/RagContext.ts deleted file mode 100644 index 3bcc1e981..000000000 --- a/src/debug/jtag/shared/generated/rag/RagContext.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { LlmMessage } from "./LlmMessage"; -import type { SourceTiming } from "./SourceTiming"; - -/** - * Complete RAG context ready for LLM - the output of RAG engine - */ -export type RagContext = { persona_id: string, room_id: string, system_prompt: string, messages: Array, total_tokens: number, composition_time_ms: number, source_timings: Array, }; diff --git a/src/debug/jtag/shared/generated/rag/RagMetadata.ts b/src/debug/jtag/shared/generated/rag/RagMetadata.ts deleted file mode 100644 index 1f3df92e9..000000000 --- a/src/debug/jtag/shared/generated/rag/RagMetadata.ts +++ /dev/null @@ -1,10 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Metadata attached to RAG sections - */ -export type RagMetadata = { -/** - * Additional metadata as key-value pairs - */ -extra: Record, }; diff --git a/src/debug/jtag/shared/generated/rag/RagOptions.ts b/src/debug/jtag/shared/generated/rag/RagOptions.ts deleted file mode 100644 index a278f3c43..000000000 --- a/src/debug/jtag/shared/generated/rag/RagOptions.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Options for RAG context building - */ -export type RagOptions = { room_id: string, persona_id: string, max_tokens: number, voice_session_id?: string, skip_semantic_search: boolean, current_message?: string, }; diff --git a/src/debug/jtag/shared/generated/rag/SourceTiming.ts b/src/debug/jtag/shared/generated/rag/SourceTiming.ts deleted file mode 100644 index 00fed348c..000000000 --- a/src/debug/jtag/shared/generated/rag/SourceTiming.ts +++ /dev/null @@ -1,6 +0,0 @@ -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. - -/** - * Timing info for each source - performance metrics - */ -export type SourceTiming = { name: string, load_time_ms: number, token_count: number, }; diff --git a/src/debug/jtag/shared/generated/rag/index.ts b/src/debug/jtag/shared/generated/rag/index.ts deleted file mode 100644 index 65de2d4ce..000000000 --- a/src/debug/jtag/shared/generated/rag/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -// RAG Types - Generated from Rust (single source of truth) -// Re-run: cargo test --package continuum-core --lib rag::types::export - -export type { MessageRole } from './MessageRole'; -export type { LlmMessage } from './LlmMessage'; -export type { RagMetadata } from './RagMetadata'; -export type { RagOptions } from './RagOptions'; -export type { SourceTiming } from './SourceTiming'; -export type { RagContext } from './RagContext'; diff --git a/src/debug/jtag/shared/version.ts b/src/debug/jtag/shared/version.ts index 449a57c9e..e38153d79 100644 --- a/src/debug/jtag/shared/version.ts +++ b/src/debug/jtag/shared/version.ts @@ -3,5 +3,5 @@ * DO NOT EDIT MANUALLY */ -export const VERSION = '1.0.7622'; +export const VERSION = '1.0.7629'; export const PACKAGE_NAME = '@continuum/jtag'; diff --git a/src/debug/jtag/workers/data-daemon/src/main.rs b/src/debug/jtag/workers/data-daemon/src/main.rs index b5138249c..7dbcb914d 100644 --- a/src/debug/jtag/workers/data-daemon/src/main.rs +++ b/src/debug/jtag/workers/data-daemon/src/main.rs @@ -23,18 +23,21 @@ use std::process::Command; use std::sync::{Arc, Mutex, RwLock}; use std::time::Instant; use std::{fs, thread}; -use ts_rs::TS; use uuid::Uuid; mod timing; use timing::{RequestTimer, METRICS}; +// IPC types — single source of truth, ts-rs exported for TypeScript +mod types; +pub use types::*; + // ============================================================================ -// Core Types (ts-rs exported for TypeScript) +// Core Types (internal, not exported to TypeScript) // ============================================================================ /// Opaque handle to a database adapter (like textureId) -/// Serialized as UUID string in JSON +/// Serialized as UUID string in JSON — TypeScript sees it as string #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct AdapterHandle(Uuid); @@ -44,26 +47,6 @@ impl AdapterHandle { } } -/// Adapter type (determines concurrency strategy) -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] -#[ts(export, export_to = "../../../shared/types/")] -#[serde(rename_all = "lowercase")] -pub enum AdapterType { - Sqlite, - Postgres, - Json, -} - -/// Adapter configuration -#[derive(Debug, Clone, Serialize, Deserialize, TS)] -#[ts(export, export_to = "../../../shared/types/")] -pub struct AdapterConfig { - adapter_type: AdapterType, - connection_string: String, - #[ts(skip)] - options: Option>, -} - // ============================================================================ // Request/Response Types // ============================================================================ @@ -172,15 +155,23 @@ enum Request { /// Raw SQL query string (SELECT only, no modifications) sql: String, }, -} -#[derive(Debug, Serialize, Deserialize, TS)] -#[ts(export, export_to = "../../../shared/types/")] -pub struct OrderBy { - field: String, - direction: String, // "asc" | "desc" + /// Truncate (delete all rows from) a collection + #[serde(rename = "data/truncate")] + DataTruncate { + handle: AdapterHandle, + collection: String, + }, + + /// List all table names in the database + #[serde(rename = "data/list_tables")] + DataListTables { + handle: AdapterHandle, + }, } +// OrderBy is now in types.rs (ts-rs exported) + #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "status")] enum Response { @@ -1259,6 +1250,18 @@ impl RustDataDaemon { Ok(data) => Response::Ok { data }, Err(e) => Response::Error { message: e }, }, + + Request::DataTruncate { handle, collection } => { + match self.data_truncate(handle, &collection) { + Ok(data) => Response::Ok { data }, + Err(e) => Response::Error { message: e }, + } + } + + Request::DataListTables { handle } => match self.data_list_tables(handle) { + Ok(data) => Response::Ok { data }, + Err(e) => Response::Error { message: e }, + }, } } @@ -1565,6 +1568,47 @@ impl RustDataDaemon { } } } + + Request::DataTruncate { handle, collection } => { + timer.set_adapter_handle(&format!("{handle:?}")); + timer.set_collection(&collection); + timer.record.route_ns = route_start.elapsed().as_nanos() as u64; + + let execute_start = Instant::now(); + let result = self.data_truncate(handle, &collection); + timer.record.execute_ns = execute_start.elapsed().as_nanos() as u64; + + match result { + Ok(data) => (Response::Ok { data }, None), + Err(e) => { + timer.set_error(&e); + (Response::Error { message: e }, None) + } + } + } + + Request::DataListTables { handle } => { + timer.set_adapter_handle(&format!("{handle:?}")); + timer.record.route_ns = route_start.elapsed().as_nanos() as u64; + + let execute_start = Instant::now(); + let result = self.data_list_tables(handle); + timer.record.execute_ns = execute_start.elapsed().as_nanos() as u64; + + match result { + Ok(data) => { + let count = data + .get("count") + .and_then(|c| c.as_u64()) + .map(|c| c as usize); + (Response::Ok { data }, count) + } + Err(e) => { + timer.set_error(&e); + (Response::Error { message: e }, None) + } + } + } } } @@ -2218,6 +2262,32 @@ impl RustDataDaemon { println!("📊 DataQuery: {sql}"); self.registry.execute_read(handle, sql) } + + /// Truncate (delete all rows from) a collection + fn data_truncate(&self, handle: AdapterHandle, collection: &str) -> Result { + let query = format!("DELETE FROM {collection}"); + println!("🗑️ DataTruncate: {query}"); + self.registry.execute_write(handle, &query, &json!({})) + } + + /// List all table names in the database (excluding SQLite internals) + fn data_list_tables(&self, handle: AdapterHandle) -> Result { + let sql = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"; + let result = self.registry.execute_read(handle, sql)?; + // Result has items: [{name: "table1"}, ...] — extract just the names + let items = result.get("items").and_then(|v| v.as_array()); + let tables: Vec = items + .map(|arr| { + arr.iter() + .filter_map(|row| row.get("name").and_then(|n| n.as_str()).map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + let count = tables.len(); + // Use typed struct (matches generated TypeScript type ListTablesResult) + let result = ListTablesResult { tables, count }; + serde_json::to_value(result).map_err(|e| e.to_string()) + } } // ============================================================================ @@ -2270,6 +2340,8 @@ fn handle_connection(stream: UnixStream, daemon: Arc) -> std::io Request::BlobDelete { .. } => "blob/delete", Request::BlobStats { .. } => "blob/stats", Request::DataQuery { .. } => "data/query", + Request::DataTruncate { .. } => "data/truncate", + Request::DataListTables { .. } => "data/list_tables", }; // Start request timer diff --git a/src/debug/jtag/workers/data-daemon/src/types.rs b/src/debug/jtag/workers/data-daemon/src/types.rs new file mode 100644 index 000000000..6c2db2c44 --- /dev/null +++ b/src/debug/jtag/workers/data-daemon/src/types.rs @@ -0,0 +1,226 @@ +//! IPC Type Definitions for data-daemon worker +//! +//! **Single source of truth** — TypeScript types generated via `ts-rs`. +//! These are the wire types for communication between TypeScript and Rust +//! across the Unix socket boundary. +//! +//! Re-generate TypeScript bindings: +//! cargo test --package data-daemon-worker export_bindings +//! +//! Output: shared/generated/data-daemon/*.ts + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use ts_rs::TS; + +// ============================================================================ +// Adapter Configuration Types +// ============================================================================ + +/// Database adapter type (determines concurrency strategy) +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/AdapterType.ts")] +#[serde(rename_all = "lowercase")] +pub enum AdapterType { + Sqlite, + Postgres, + Json, +} + +/// Adapter configuration for opening a database connection +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/AdapterConfig.ts")] +pub struct AdapterConfig { + pub adapter_type: AdapterType, + pub connection_string: String, + #[ts(skip)] + pub options: Option>, +} + +/// Sort order specification for queries +#[derive(Debug, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/OrderBy.ts")] +pub struct OrderBy { + pub field: String, + /// "asc" or "desc" + pub direction: String, +} + +// ============================================================================ +// Response Data Types — contents of Response::Ok { data } +// +// Each command returns a specific data shape. These types document and enforce +// the wire format so TypeScript can safely destructure responses. +// ============================================================================ + +/// Response data from `data/list` command +/// +/// Contains query results as an array of row objects plus total count. +/// Each item is a raw SQLite row (snake_case keys, TEXT values for JSON columns). +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/DataListResult.ts")] +pub struct DataListResult { + /// Array of row objects from the query. Each row's shape depends on the table schema. + /// JSON columns come back as TEXT strings — TypeScript must hydrate them. + #[ts(type = "Array>")] + pub items: Vec, + /// Total number of rows matching the filter (before limit/offset) + #[ts(type = "number")] + pub count: usize, +} + +/// Response data from `data/query` command (raw SQL) +/// +/// Same shape as DataListResult but for arbitrary SQL queries. +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/DataQueryResult.ts")] +pub struct DataQueryResult { + #[ts(type = "Array>")] + pub items: Vec, + #[ts(type = "number")] + pub count: usize, +} + +/// Response data from `data/list_tables` command +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/ListTablesResult.ts")] +pub struct ListTablesResult { + /// Table names in the database + pub tables: Vec, + #[ts(type = "number")] + pub count: usize, +} + +/// A single hit from vector similarity search +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/VectorSearchHit.ts")] +pub struct VectorSearchHit { + /// Record ID + pub id: String, + /// Cosine similarity score (0.0 to 1.0) + pub score: f64, + /// Distance (1.0 - score) + pub distance: f64, + /// Full record data when include_data=true + #[ts(optional)] + #[ts(type = "Record")] + pub data: Option, +} + +/// Response data from `vector/search` command +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/VectorSearchResult.ts")] +pub struct VectorSearchResult { + pub results: Vec, + #[ts(type = "number")] + pub count: usize, + #[ts(type = "number")] + pub corpus_size: usize, +} + +/// Response data from `adapter/open` command +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/AdapterOpenResult.ts")] +pub struct AdapterOpenResult { + /// Opaque handle UUID for subsequent operations + pub handle: String, +} + +/// Response data from `blob/store` command +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/BlobStoreResult.ts")] +pub struct BlobStoreResult { + /// Content-addressable hash (format: "sha256:...") + pub hash: String, + /// Original uncompressed size in bytes + #[ts(type = "number")] + pub size: usize, + /// Compressed size in bytes + #[ts(type = "number")] + pub compressed_size: usize, + /// Whether the blob was deduplicated (already existed) + pub deduplicated: bool, + /// Timestamp when stored + pub stored_at: String, +} + +/// Response data from `blob/stats` command +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/BlobStatsResult.ts")] +pub struct BlobStatsResult { + #[ts(type = "number")] + pub total_blobs: usize, + #[ts(type = "number")] + pub total_compressed_bytes: usize, + #[ts(type = "number")] + pub shard_count: usize, + pub base_path: String, +} + +/// Response data from `blob/exists` command +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/BlobExistsResult.ts")] +pub struct BlobExistsResult { + pub exists: bool, +} + +/// Response data from `blob/delete` command +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/BlobDeleteResult.ts")] +pub struct BlobDeleteResult { + pub deleted: bool, +} + +/// Response data from write commands (data/create, data/update, data/delete, data/truncate). +/// +/// The SQLite strategy serializes writes through a queue and returns results +/// for each executed statement. +/// +/// Named `DataWriteResult` to avoid collision with continuum-core's file `WriteResult`. +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/DataWriteResult.ts")] +pub struct DataWriteResult { + pub results: Vec, +} + +/// Result of a single write operation in the queue +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[ts(export, export_to = "../../../shared/generated/data-daemon/DataWriteRowResult.ts")] +pub struct DataWriteRowResult { + #[ts(type = "number")] + pub rows_affected: usize, +} + +// ============================================================================ +// TypeScript Export Test +// ============================================================================ + +#[cfg(test)] +mod export_typescript { + use super::*; + + #[test] + fn export_bindings() { + // Adapter types + AdapterType::export().expect("Failed to export AdapterType"); + AdapterConfig::export().expect("Failed to export AdapterConfig"); + OrderBy::export().expect("Failed to export OrderBy"); + + // Response data types + DataListResult::export().expect("Failed to export DataListResult"); + DataQueryResult::export().expect("Failed to export DataQueryResult"); + ListTablesResult::export().expect("Failed to export ListTablesResult"); + VectorSearchHit::export().expect("Failed to export VectorSearchHit"); + VectorSearchResult::export().expect("Failed to export VectorSearchResult"); + AdapterOpenResult::export().expect("Failed to export AdapterOpenResult"); + DataWriteResult::export().expect("Failed to export DataWriteResult"); + DataWriteRowResult::export().expect("Failed to export DataWriteRowResult"); + BlobStoreResult::export().expect("Failed to export BlobStoreResult"); + BlobStatsResult::export().expect("Failed to export BlobStatsResult"); + BlobExistsResult::export().expect("Failed to export BlobExistsResult"); + BlobDeleteResult::export().expect("Failed to export BlobDeleteResult"); + + println!("✅ data-daemon TypeScript bindings exported to shared/generated/data-daemon/"); + } +} From 6dd49a2da09ab536490869a12b3c87abf25793f5 Mon Sep 17 00:00:00 2001 From: Grok Date: Fri, 6 Feb 2026 00:44:18 -0600 Subject: [PATCH 4/5] cache all entities - joel --- .../data-daemon/browser/IndexedDBBackend.ts | 24 ++++----- .../browser/OfflineStorageAdapter.ts | 47 +++++++++++------- src/debug/jtag/generated-command-schemas.json | 2 +- src/debug/jtag/package-lock.json | 4 +- src/debug/jtag/package.json | 2 +- src/debug/jtag/shared/version.ts | 2 +- src/debug/jtag/system/shared/Constants.ts | 25 ++++++++++ .../widgets/user-profile/UserProfileWidget.ts | 49 ++++++++++++++----- 8 files changed, 108 insertions(+), 47 deletions(-) diff --git a/src/debug/jtag/daemons/data-daemon/browser/IndexedDBBackend.ts b/src/debug/jtag/daemons/data-daemon/browser/IndexedDBBackend.ts index cfb02bd18..92e1ed964 100644 --- a/src/debug/jtag/daemons/data-daemon/browser/IndexedDBBackend.ts +++ b/src/debug/jtag/daemons/data-daemon/browser/IndexedDBBackend.ts @@ -3,26 +3,26 @@ * * Replaces LocalStorageDataBackend to prevent main thread blocking. * IndexedDB operations are async by design - no blocking. + * + * Collections to cache are defined in OFFLINE_CACHEABLE_COLLECTIONS (Constants.ts) + * This file just reads from that single source of truth. */ import type { UUID } from '../../../system/core/types/CrossPlatformUUID'; import type { BaseEntity } from '../../../system/data/entities/BaseEntity'; +import { OFFLINE_CACHEABLE_COLLECTIONS } from '../../../system/shared/Constants'; const DB_NAME = 'jtag-offline-cache'; -const DB_VERSION = 2; // Bumped to add more stores +const DB_VERSION = 3; // Bumped: using OFFLINE_CACHEABLE_COLLECTIONS from Constants.ts let dbInstance: IDBDatabase | null = null; let dbInitPromise: Promise | null = null; -// Collections that need offline caching -const COLLECTIONS = [ - 'user_states', - 'users', - 'rooms', - 'chat_messages', - 'activities', - 'sync_queue' -]; +// Internal-only collection for sync queue (not exposed as cacheable entity) +const INTERNAL_COLLECTIONS = ['sync_queue'] as const; + +// All collections = cacheable entities + internal collections +const ALL_COLLECTIONS = [...OFFLINE_CACHEABLE_COLLECTIONS, ...INTERNAL_COLLECTIONS]; /** * Get or initialize the IndexedDB database @@ -48,8 +48,8 @@ async function getDB(): Promise { request.onupgradeneeded = (event) => { const db = (event.target as IDBOpenDBRequest).result; - // Create object stores for all collections - for (const collection of COLLECTIONS) { + // Create object stores for all collections (cacheable entities + internal) + for (const collection of ALL_COLLECTIONS) { if (!db.objectStoreNames.contains(collection)) { db.createObjectStore(collection, { keyPath: 'id' }); } diff --git a/src/debug/jtag/daemons/data-daemon/browser/OfflineStorageAdapter.ts b/src/debug/jtag/daemons/data-daemon/browser/OfflineStorageAdapter.ts index 3c1af9897..f3e6ef5bc 100644 --- a/src/debug/jtag/daemons/data-daemon/browser/OfflineStorageAdapter.ts +++ b/src/debug/jtag/daemons/data-daemon/browser/OfflineStorageAdapter.ts @@ -25,6 +25,7 @@ import { LocalStorageDataBackend } from './LocalStorageDataBackend'; import { IndexedDBBackend } from './IndexedDBBackend'; import { SyncQueue, type SyncOperation } from './SyncQueue'; import { ConnectionStatus } from './ConnectionStatus'; +import { OFFLINE_CACHEABLE_COLLECTIONS } from '../../../system/shared/Constants'; /** * OfflineStorageAdapter - The core dual-storage adapter @@ -326,29 +327,37 @@ export class OfflineStorageAdapter { /** * Subscribe to server events for cache invalidation * - * When server pushes changes, update local cache. + * Auto-wired from OFFLINE_CACHEABLE_COLLECTIONS (Constants.ts). + * When server pushes changes, update local IndexedDB cache. + * + * ⚠️ NO per-collection code here - all driven by the constant */ private subscribeToServerEvents(): void { - // user_states uses IndexedDB (async, non-blocking) instead of localStorage - Events.subscribe('data:user_states:updated', async (data: { id: UUID; [key: string]: unknown }) => { - if (data && data.id) { - // IndexedDB is async - doesn't block main thread - await IndexedDBBackend.update('user_states', data.id, data as any); - } - }); + // Auto-subscribe to all cacheable collections + for (const collection of OFFLINE_CACHEABLE_COLLECTIONS) { + // Subscribe to created events + Events.subscribe(`data:${collection}:created`, async (data: { id: UUID; [key: string]: unknown }) => { + if (data && data.id) { + await IndexedDBBackend.create(collection, data as any); + } + }); - Events.subscribe('data:user_states:deleted', async (data: { id: UUID }) => { - if (data && data.id) { - await IndexedDBBackend.delete('user_states', data.id); - } - }); + // Subscribe to updated events + Events.subscribe(`data:${collection}:updated`, async (data: { id: UUID; [key: string]: unknown }) => { + if (data && data.id) { + await IndexedDBBackend.update(collection, data.id, data as any); + } + }); - Events.subscribe('data:user_states:created', async (data: { id: UUID; [key: string]: unknown }) => { - if (data && data.id) { - // IndexedDB is async - doesn't block main thread - await IndexedDBBackend.create('user_states', data as any); - } - }); + // Subscribe to deleted events + Events.subscribe(`data:${collection}:deleted`, async (data: { id: UUID }) => { + if (data && data.id) { + await IndexedDBBackend.delete(collection, data.id); + } + }); + } + + console.log(`OfflineStorageAdapter: Auto-wired cache invalidation for ${OFFLINE_CACHEABLE_COLLECTIONS.length} collections`); } /** diff --git a/src/debug/jtag/generated-command-schemas.json b/src/debug/jtag/generated-command-schemas.json index 1eb8aabe1..656a55f83 100644 --- a/src/debug/jtag/generated-command-schemas.json +++ b/src/debug/jtag/generated-command-schemas.json @@ -1,5 +1,5 @@ { - "generated": "2026-02-05T21:20:24.680Z", + "generated": "2026-02-06T06:30:17.861Z", "version": "1.0.0", "commands": [ { diff --git a/src/debug/jtag/package-lock.json b/src/debug/jtag/package-lock.json index c6f204524..757597295 100644 --- a/src/debug/jtag/package-lock.json +++ b/src/debug/jtag/package-lock.json @@ -1,12 +1,12 @@ { "name": "@continuum/jtag", - "version": "1.0.7629", + "version": "1.0.7632", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@continuum/jtag", - "version": "1.0.7629", + "version": "1.0.7632", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/debug/jtag/package.json b/src/debug/jtag/package.json index 4e2f49063..a47a6884b 100644 --- a/src/debug/jtag/package.json +++ b/src/debug/jtag/package.json @@ -1,6 +1,6 @@ { "name": "@continuum/jtag", - "version": "1.0.7629", + "version": "1.0.7632", "description": "Global CLI debugging system for any Node.js project. Install once globally, use anywhere: npm install -g @continuum/jtag", "config": { "active_example": "widget-ui", diff --git a/src/debug/jtag/shared/version.ts b/src/debug/jtag/shared/version.ts index e38153d79..7758e5520 100644 --- a/src/debug/jtag/shared/version.ts +++ b/src/debug/jtag/shared/version.ts @@ -3,5 +3,5 @@ * DO NOT EDIT MANUALLY */ -export const VERSION = '1.0.7629'; +export const VERSION = '1.0.7632'; export const PACKAGE_NAME = '@continuum/jtag'; diff --git a/src/debug/jtag/system/shared/Constants.ts b/src/debug/jtag/system/shared/Constants.ts index 95d5acd8a..ff1d58083 100644 --- a/src/debug/jtag/system/shared/Constants.ts +++ b/src/debug/jtag/system/shared/Constants.ts @@ -147,6 +147,31 @@ export const COLLECTIONS = { CODING_CHALLENGES: 'coding_challenges', } as const; +/** + * Offline Cacheable Collections - SINGLE SOURCE OF TRUTH + * + * Collections listed here are automatically: + * 1. Created as IndexedDB object stores (browser) + * 2. Subscribed to data events for cache invalidation + * 3. Cached locally for offline-first reads + * + * To add offline caching to a collection: + * 1. Add it to this array + * 2. Bump DB_VERSION in IndexedDBBackend.ts (triggers store creation) + * + * ⚠️ NEVER add per-collection logic to OfflineStorageAdapter + * ⚠️ This constant drives all offline caching behavior + */ +export const OFFLINE_CACHEABLE_COLLECTIONS = [ + COLLECTIONS.USERS, + COLLECTIONS.USER_STATES, + COLLECTIONS.ROOMS, + COLLECTIONS.CHAT_MESSAGES, + COLLECTIONS.ACTIVITIES, +] as const; + +export type OfflineCacheableCollection = typeof OFFLINE_CACHEABLE_COLLECTIONS[number]; + /** * Fine-Tuning Providers - Supported providers for LoRA training diff --git a/src/debug/jtag/widgets/user-profile/UserProfileWidget.ts b/src/debug/jtag/widgets/user-profile/UserProfileWidget.ts index 973662f33..e08da9c05 100644 --- a/src/debug/jtag/widgets/user-profile/UserProfileWidget.ts +++ b/src/debug/jtag/widgets/user-profile/UserProfileWidget.ts @@ -200,25 +200,52 @@ export class UserProfileWidget extends BaseWidget { return; } + const userIdToDelete = this.user.id; + const userNameToDelete = this.user.displayName; + try { - await DataDelete.execute({ + // Delete the user from database + const result = await DataDelete.execute({ collection: 'users', - id: this.user.id + id: userIdToDelete }); - // Emit event so user list can refresh - Events.emit('data:users:deleted', { id: this.user.id }); + if (!result.deleted) { + console.error(`Failed to delete user ${userNameToDelete}:`, result); + alert(`Failed to delete user: ${result.error || (result.found ? 'Delete failed' : 'User not found')}`); + return; + } + + console.log(`✅ User ${userNameToDelete} deleted successfully`); + + // Server emits 'data:users:deleted' but also emit locally to ensure UI updates + // This provides redundancy in case WebSocket event delivery has any delay + Events.emit('data:users:deleted', { id: userIdToDelete }); - // OPTIMISTIC: Navigate back to chat instantly - if (this.userState?.userId) { - ContentService.setUserId(this.userState.userId as UUID); + // Close THIS profile tab (not just navigate away) + // Find the current profile tab and close it + const { contentState } = await import('../../system/state/ContentStateService'); + const currentTab = contentState.openItems.find( + item => item.entityId === userIdToDelete || + (item.type === 'profile' && contentState.currentItemId === item.id) + ); + + if (currentTab) { + ContentService.close(currentTab.id); + console.log(`✅ Closed profile tab for deleted user ${userNameToDelete}`); + } else { + // Fallback: just navigate to general chat + if (this.userState?.userId) { + ContentService.setUserId(this.userState.userId as UUID); + } + ContentService.open('chat', 'general', { + title: 'General', + uniqueId: 'general' + }); } - ContentService.open('chat', 'general', { - title: 'General', - uniqueId: 'general' - }); } catch (err) { console.error('Failed to delete user:', err); + alert(`Failed to delete user: ${err instanceof Error ? err.message : 'Unknown error'}`); } } From 48bd091bcd25f8292e595f29ea1c4aaac650ab87 Mon Sep 17 00:00:00 2001 From: joelteply Date: Fri, 6 Feb 2026 00:49:28 -0600 Subject: [PATCH 5/5] git names were messed up --- src/debug/jtag/scripts/migrate-sandbox-to-git.ts | 5 +++-- src/debug/jtag/system/code/server/WorkspaceStrategy.ts | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/debug/jtag/scripts/migrate-sandbox-to-git.ts b/src/debug/jtag/scripts/migrate-sandbox-to-git.ts index a8d0c4955..2088f4e13 100644 --- a/src/debug/jtag/scripts/migrate-sandbox-to-git.ts +++ b/src/debug/jtag/scripts/migrate-sandbox-to-git.ts @@ -58,8 +58,9 @@ function migrateWorkspace(personaId: string, workspacePath: string): MigrationRe execSync('git init', opts); // Set identity — use persona ID as placeholder; proper names set when project workspaces are created - execSync(`git config user.name "AI Persona (${personaId.slice(0, 8)})"`, opts); - execSync(`git config user.email "${personaId}@continuum.local"`, opts); + // MUST use --local to avoid polluting global git config + execSync(`git config --local user.name "AI Persona (${personaId.slice(0, 8)})"`, opts); + execSync(`git config --local user.email "${personaId}@continuum.local"`, opts); // Create .gitignore for common build artifacts const gitignore = 'node_modules/\ndist/\n.DS_Store\n*.log\n'; diff --git a/src/debug/jtag/system/code/server/WorkspaceStrategy.ts b/src/debug/jtag/system/code/server/WorkspaceStrategy.ts index c59580b48..11952046b 100644 --- a/src/debug/jtag/system/code/server/WorkspaceStrategy.ts +++ b/src/debug/jtag/system/code/server/WorkspaceStrategy.ts @@ -289,12 +289,12 @@ export class WorkspaceStrategy { } } - // Set local git identity in the worktree (not global) + // Set local git identity in the worktree (MUST use --local to avoid polluting global config) const userName = config.personaName ?? 'AI Persona'; const userEmail = `${config.personaUniqueId}@continuum.local`; const wtOpts = { cwd: worktreeDir, stdio: 'pipe' as const }; - execSync(`git config user.name "${userName}"`, wtOpts); - execSync(`git config user.email "${userEmail}"`, wtOpts); + execSync(`git config --local user.name "${userName}"`, wtOpts); + execSync(`git config --local user.email "${userEmail}"`, wtOpts); // Register with Rust CodeDaemon — worktree IS the repo checkout, no extra read roots await CodeDaemon.createWorkspace(handle, worktreeDir, []);