From 26e0c204a9ecb3a4dffeb8de85c762e8602295b7 Mon Sep 17 00:00:00 2001 From: joelteply Date: Fri, 13 Feb 2026 12:03:08 -0600 Subject: [PATCH 1/2] Observability Phase 1: Non-blocking Rust logging + Sentinel log commands Rust logging improvements: - Add clog_info!, clog_warn!, clog_error!, clog_debug! macros for easy logging - Auto-route logs by module_path!() to appropriate directories - Wire to LoggerModule's concurrent writer (sync_channel + try_send = NEVER blocks) - Convert println!/eprintln! to clog_* in orm/sqlite, voice/orchestrator, voice/tts/phonemizer, ai/adapter, concurrent/message_processor New commands: - logging/enable, logging/disable, logging/status - control Rust logging - sentinel/logs/list - list log streams for a sentinel with size/mtime - sentinel/logs/read - read log content with offset/limit - sentinel/logs/tail - get last N lines (like Unix tail) SentinelLogWriter: - Non-blocking async TypeScript log writer for per-sentinel logs - Event streaming via sentinel:{handle}:log for real-time UI - Directory structure: .sentinel-workspaces/{handle}/logs/ Documentation: - docs/OBSERVABILITY-ARCHITECTURE.md - overall vision - docs/SENTINEL-LOGGING-PLAN.md - sentinel logging design --- src/debug/jtag/browser/generated.ts | 20 +- .../jtag/commands/logging/disable/.npmignore | 20 + .../jtag/commands/logging/disable/README.md | 166 ++++ .../browser/LoggingDisableBrowserCommand.ts | 21 + .../commands/logging/disable/package.json | 35 + .../server/LoggingDisableServerCommand.ts | 74 ++ .../disable/shared/LoggingDisableTypes.ts | 104 +++ .../LoggingDisableIntegration.test.ts | 196 +++++ .../test/unit/LoggingDisableCommand.test.ts | 259 +++++++ .../jtag/commands/logging/enable/.npmignore | 20 + .../jtag/commands/logging/enable/README.md | 165 ++++ .../browser/LoggingEnableBrowserCommand.ts | 21 + .../jtag/commands/logging/enable/package.json | 35 + .../server/LoggingEnableServerCommand.ts | 71 ++ .../enable/shared/LoggingEnableTypes.ts | 99 +++ .../LoggingEnableIntegration.test.ts | 196 +++++ .../test/unit/LoggingEnableCommand.test.ts | 259 +++++++ .../jtag/commands/logging/status/.npmignore | 20 + .../jtag/commands/logging/status/README.md | 166 ++++ .../browser/LoggingStatusBrowserCommand.ts | 21 + .../jtag/commands/logging/status/package.json | 35 + .../server/LoggingStatusServerCommand.ts | 87 +++ .../status/shared/LoggingStatusTypes.ts | 105 +++ .../LoggingStatusIntegration.test.ts | 196 +++++ .../test/unit/LoggingStatusCommand.test.ts | 259 +++++++ .../server/SentinelLogsListServerCommand.ts | 88 +++ .../logs/list/shared/SentinelLogsListTypes.ts | 43 ++ .../server/SentinelLogsReadServerCommand.ts | 104 +++ .../logs/read/shared/SentinelLogsReadTypes.ts | 38 + .../server/SentinelLogsTailServerCommand.ts | 86 +++ .../logs/tail/shared/SentinelLogsTailTypes.ts | 33 + .../jtag/docs/OBSERVABILITY-ARCHITECTURE.md | 724 ++++++++++++++++++ src/debug/jtag/docs/SENTINEL-LOGGING-PLAN.md | 704 +++++++++++++++++ src/debug/jtag/generated-command-schemas.json | 103 ++- .../jtag/generator/specs/logging-disable.json | 53 ++ .../jtag/generator/specs/logging-enable.json | 48 ++ .../jtag/generator/specs/logging-status.json | 52 ++ src/debug/jtag/package-lock.json | 4 +- src/debug/jtag/package.json | 2 +- src/debug/jtag/server/generated.ts | 38 +- .../shared/generated-command-constants.ts | 6 + src/debug/jtag/shared/version.ts | 2 +- .../jtag/system/sentinel/SentinelLogWriter.ts | 203 +++++ .../workers/continuum-core/src/ai/adapter.rs | 5 +- .../src/concurrent/message_processor.rs | 5 +- .../jtag/workers/continuum-core/src/lib.rs | 4 +- .../workers/continuum-core/src/logging/mod.rs | 233 ++++++ .../continuum-core/src/modules/logger.rs | 100 ++- .../workers/continuum-core/src/orm/sqlite.rs | 13 +- .../continuum-core/src/voice/orchestrator.rs | 15 +- .../src/voice/tts/phonemizer.rs | 9 +- 51 files changed, 5328 insertions(+), 37 deletions(-) create mode 100644 src/debug/jtag/commands/logging/disable/.npmignore create mode 100644 src/debug/jtag/commands/logging/disable/README.md create mode 100644 src/debug/jtag/commands/logging/disable/browser/LoggingDisableBrowserCommand.ts create mode 100644 src/debug/jtag/commands/logging/disable/package.json create mode 100644 src/debug/jtag/commands/logging/disable/server/LoggingDisableServerCommand.ts create mode 100644 src/debug/jtag/commands/logging/disable/shared/LoggingDisableTypes.ts create mode 100644 src/debug/jtag/commands/logging/disable/test/integration/LoggingDisableIntegration.test.ts create mode 100644 src/debug/jtag/commands/logging/disable/test/unit/LoggingDisableCommand.test.ts create mode 100644 src/debug/jtag/commands/logging/enable/.npmignore create mode 100644 src/debug/jtag/commands/logging/enable/README.md create mode 100644 src/debug/jtag/commands/logging/enable/browser/LoggingEnableBrowserCommand.ts create mode 100644 src/debug/jtag/commands/logging/enable/package.json create mode 100644 src/debug/jtag/commands/logging/enable/server/LoggingEnableServerCommand.ts create mode 100644 src/debug/jtag/commands/logging/enable/shared/LoggingEnableTypes.ts create mode 100644 src/debug/jtag/commands/logging/enable/test/integration/LoggingEnableIntegration.test.ts create mode 100644 src/debug/jtag/commands/logging/enable/test/unit/LoggingEnableCommand.test.ts create mode 100644 src/debug/jtag/commands/logging/status/.npmignore create mode 100644 src/debug/jtag/commands/logging/status/README.md create mode 100644 src/debug/jtag/commands/logging/status/browser/LoggingStatusBrowserCommand.ts create mode 100644 src/debug/jtag/commands/logging/status/package.json create mode 100644 src/debug/jtag/commands/logging/status/server/LoggingStatusServerCommand.ts create mode 100644 src/debug/jtag/commands/logging/status/shared/LoggingStatusTypes.ts create mode 100644 src/debug/jtag/commands/logging/status/test/integration/LoggingStatusIntegration.test.ts create mode 100644 src/debug/jtag/commands/logging/status/test/unit/LoggingStatusCommand.test.ts create mode 100644 src/debug/jtag/commands/sentinel/logs/list/server/SentinelLogsListServerCommand.ts create mode 100644 src/debug/jtag/commands/sentinel/logs/list/shared/SentinelLogsListTypes.ts create mode 100644 src/debug/jtag/commands/sentinel/logs/read/server/SentinelLogsReadServerCommand.ts create mode 100644 src/debug/jtag/commands/sentinel/logs/read/shared/SentinelLogsReadTypes.ts create mode 100644 src/debug/jtag/commands/sentinel/logs/tail/server/SentinelLogsTailServerCommand.ts create mode 100644 src/debug/jtag/commands/sentinel/logs/tail/shared/SentinelLogsTailTypes.ts create mode 100644 src/debug/jtag/docs/OBSERVABILITY-ARCHITECTURE.md create mode 100644 src/debug/jtag/docs/SENTINEL-LOGGING-PLAN.md create mode 100644 src/debug/jtag/generator/specs/logging-disable.json create mode 100644 src/debug/jtag/generator/specs/logging-enable.json create mode 100644 src/debug/jtag/generator/specs/logging-status.json create mode 100644 src/debug/jtag/system/sentinel/SentinelLogWriter.ts diff --git a/src/debug/jtag/browser/generated.ts b/src/debug/jtag/browser/generated.ts index f3517754e..acb7ab9f8 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 201 commands and 2 adapters and 28 widgets. + * Contains 11 daemons and 204 commands and 2 adapters and 28 widgets. * Generated by scripts/generate-structure.ts - DO NOT EDIT MANUALLY */ @@ -146,6 +146,9 @@ import { WebFetchBrowserCommand } from './../commands/interface/web/fetch/browse import { InterfaceWebmcpCallBrowserCommand } from './../commands/interface/webmcp/call/browser/InterfaceWebmcpCallBrowserCommand'; import { InterfaceWebmcpDiscoverBrowserCommand } from './../commands/interface/webmcp/discover/browser/InterfaceWebmcpDiscoverBrowserCommand'; import { ListBrowserCommand } from './../commands/list/browser/ListBrowserCommand'; +import { LoggingDisableBrowserCommand } from './../commands/logging/disable/browser/LoggingDisableBrowserCommand'; +import { LoggingEnableBrowserCommand } from './../commands/logging/enable/browser/LoggingEnableBrowserCommand'; +import { LoggingStatusBrowserCommand } from './../commands/logging/status/browser/LoggingStatusBrowserCommand'; import { LogsConfigBrowserCommand } from './../commands/logs/config/browser/LogsConfigBrowserCommand'; import { LogsListBrowserCommand } from './../commands/logs/list/browser/LogsListBrowserCommand'; import { LogsReadBrowserCommand } from './../commands/logs/read/browser/LogsReadBrowserCommand'; @@ -958,6 +961,21 @@ export const BROWSER_COMMANDS: CommandEntry[] = [ className: 'ListBrowserCommand', commandClass: ListBrowserCommand }, +{ + name: 'logging/disable', + className: 'LoggingDisableBrowserCommand', + commandClass: LoggingDisableBrowserCommand + }, +{ + name: 'logging/enable', + className: 'LoggingEnableBrowserCommand', + commandClass: LoggingEnableBrowserCommand + }, +{ + name: 'logging/status', + className: 'LoggingStatusBrowserCommand', + commandClass: LoggingStatusBrowserCommand + }, { name: 'logs/config', className: 'LogsConfigBrowserCommand', diff --git a/src/debug/jtag/commands/logging/disable/.npmignore b/src/debug/jtag/commands/logging/disable/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/logging/disable/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/logging/disable/README.md b/src/debug/jtag/commands/logging/disable/README.md new file mode 100644 index 000000000..ad15f717a --- /dev/null +++ b/src/debug/jtag/commands/logging/disable/README.md @@ -0,0 +1,166 @@ +# Logging Disable Command + +Disable logging for a persona. Persists to .continuum/logging.json + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag logging/disable --persona= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('logging/disable', { + // your parameters here +}); +``` + +## Parameters + +- **persona** (required): `string` - Persona uniqueId to disable logging for (e.g., 'helper', 'codereview') +- **category** (optional): `string` - Specific category to disable. If not specified, disables all logging for the persona + +## Result + +Returns `LoggingDisableResult` with: + +Returns CommandResult with: +- **persona**: `string` - The persona that was disabled +- **enabled**: `boolean` - Whether any logging remains enabled for this persona +- **categories**: `string[]` - Categories still enabled (empty if all disabled) +- **message**: `string` - Human-readable status message + +## Examples + +### Disable all logging for helper persona + +```bash +./jtag logging/disable --persona=helper +``` + +**Expected result:** +{ persona: 'helper', enabled: false, categories: [], message: 'Disabled all logging for helper' } + +### Disable only training logs + +```bash +./jtag logging/disable --persona=helper --category=training +``` + +**Expected result:** +{ persona: 'helper', enabled: true, categories: ['cognition'], message: 'Disabled training logging for helper' } + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help logging/disable +``` + +**Tool:** +```typescript +// Use your help tool with command name 'logging/disable' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme logging/disable +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'logging/disable' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/Logging Disable/test/unit/LoggingDisableCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/Logging Disable/test/integration/LoggingDisableIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/LoggingDisableTypes.ts` +- **Browser**: Browser-specific implementation in `browser/LoggingDisableBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/LoggingDisableServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/LoggingDisableCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/LoggingDisableIntegration.test.ts` diff --git a/src/debug/jtag/commands/logging/disable/browser/LoggingDisableBrowserCommand.ts b/src/debug/jtag/commands/logging/disable/browser/LoggingDisableBrowserCommand.ts new file mode 100644 index 000000000..96a7ef542 --- /dev/null +++ b/src/debug/jtag/commands/logging/disable/browser/LoggingDisableBrowserCommand.ts @@ -0,0 +1,21 @@ +/** + * Logging Disable Command - Browser Implementation + * + * Disable logging for a persona. Persists to .continuum/logging.json + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { LoggingDisableParams, LoggingDisableResult } from '../shared/LoggingDisableTypes'; + +export class LoggingDisableBrowserCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('logging/disable', context, subpath, commander); + } + + async execute(params: LoggingDisableParams): Promise { + console.log('๐ŸŒ BROWSER: Delegating Logging Disable to server'); + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/logging/disable/package.json b/src/debug/jtag/commands/logging/disable/package.json new file mode 100644 index 000000000..5abfd6b95 --- /dev/null +++ b/src/debug/jtag/commands/logging/disable/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/logging/disable", + "version": "1.0.0", + "description": "Disable logging for a persona. Persists to .continuum/logging.json", + "main": "server/LoggingDisableServerCommand.ts", + "types": "shared/LoggingDisableTypes.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/LoggingDisableIntegration.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", + "logging/disable" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/logging/disable/server/LoggingDisableServerCommand.ts b/src/debug/jtag/commands/logging/disable/server/LoggingDisableServerCommand.ts new file mode 100644 index 000000000..fe0738f62 --- /dev/null +++ b/src/debug/jtag/commands/logging/disable/server/LoggingDisableServerCommand.ts @@ -0,0 +1,74 @@ +/** + * Logging Disable Command - Server Implementation + * + * Disable logging for a persona. Persists to .continuum/logging.json + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { ValidationError } from '@system/core/types/ErrorTypes'; +import type { LoggingDisableParams, LoggingDisableResult } from '../shared/LoggingDisableTypes'; +import { createLoggingDisableResultFromParams } from '../shared/LoggingDisableTypes'; +import { LoggingConfig, LOGGING_CATEGORIES } from '@system/core/logging/LoggingConfig'; + +export class LoggingDisableServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('logging/disable', context, subpath, commander); + } + + async execute(params: LoggingDisableParams): Promise { + // Validate persona parameter + if (!params.persona || params.persona.trim() === '') { + throw new ValidationError( + 'persona', + `Missing required parameter 'persona'. ` + + `Use: ./jtag logging/disable --persona=helper [--category=cognition]` + ); + } + + const persona = params.persona.trim(); + const category = params.category?.trim(); + + // Disable logging + if (category) { + // Disable specific category + LoggingConfig.setEnabled(persona, category, false); + } else { + // Disable all logging for persona + LoggingConfig.setPersonaEnabled(persona, false); + } + + // Get current state after update + const config = LoggingConfig.getConfig(); + const normalizedId = persona.toLowerCase().replace(/\s+ai$/i, '').replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + const personaConfig = config.personas[normalizedId]; + + // Determine remaining enabled categories + let categories: string[] = []; + let enabled = false; + + if (personaConfig && personaConfig.enabled) { + enabled = true; + if (!personaConfig.categories || personaConfig.categories.length === 0) { + categories = Object.values(LOGGING_CATEGORIES); + } else if (personaConfig.categories.includes('*')) { + categories = Object.values(LOGGING_CATEGORIES); + } else { + categories = personaConfig.categories; + } + } + + const message = category + ? `Disabled ${category} logging for ${persona}` + : `Disabled all logging for ${persona}`; + + return createLoggingDisableResultFromParams(params, { + success: true, + persona, + enabled, + categories, + message, + }); + } +} diff --git a/src/debug/jtag/commands/logging/disable/shared/LoggingDisableTypes.ts b/src/debug/jtag/commands/logging/disable/shared/LoggingDisableTypes.ts new file mode 100644 index 000000000..c828f9355 --- /dev/null +++ b/src/debug/jtag/commands/logging/disable/shared/LoggingDisableTypes.ts @@ -0,0 +1,104 @@ +/** + * Logging Disable Command - Shared Types + * + * Disable logging for a persona. Persists to .continuum/logging.json + */ + +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'; + +/** + * Logging Disable Command Parameters + */ +export interface LoggingDisableParams extends CommandParams { + // Persona uniqueId to disable logging for (e.g., 'helper', 'codereview') + persona: string; + // Specific category to disable. If not specified, disables all logging for the persona + category?: string; +} + +/** + * Factory function for creating LoggingDisableParams + */ +export const createLoggingDisableParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + // Persona uniqueId to disable logging for (e.g., 'helper', 'codereview') + persona: string; + // Specific category to disable. If not specified, disables all logging for the persona + category?: string; + } +): LoggingDisableParams => createPayload(context, sessionId, { + category: data.category ?? '', + ...data +}); + +/** + * Logging Disable Command Result + */ +export interface LoggingDisableResult extends CommandResult { + success: boolean; + // The persona that was disabled + persona: string; + // Whether any logging remains enabled for this persona + enabled: boolean; + // Categories still enabled (empty if all disabled) + categories: string[]; + // Human-readable status message + message: string; + error?: JTAGError; +} + +/** + * Factory function for creating LoggingDisableResult with defaults + */ +export const createLoggingDisableResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + // The persona that was disabled + persona?: string; + // Whether any logging remains enabled for this persona + enabled?: boolean; + // Categories still enabled (empty if all disabled) + categories?: string[]; + // Human-readable status message + message?: string; + error?: JTAGError; + } +): LoggingDisableResult => createPayload(context, sessionId, { + persona: data.persona ?? '', + enabled: data.enabled ?? false, + categories: data.categories ?? [], + message: data.message ?? '', + ...data +}); + +/** + * Smart Logging Disable-specific inheritance from params + * Auto-inherits context and sessionId from params + * Must provide all required result fields + */ +export const createLoggingDisableResultFromParams = ( + params: LoggingDisableParams, + differences: Omit +): LoggingDisableResult => transformPayload(params, differences); + +/** + * Logging Disable โ€” Type-safe command executor + * + * Usage: + * import { LoggingDisable } from '...shared/LoggingDisableTypes'; + * const result = await LoggingDisable.execute({ ... }); + */ +export const LoggingDisable = { + execute(params: CommandInput): Promise { + return Commands.execute('logging/disable', params as Partial); + }, + commandName: 'logging/disable' as const, +} as const; diff --git a/src/debug/jtag/commands/logging/disable/test/integration/LoggingDisableIntegration.test.ts b/src/debug/jtag/commands/logging/disable/test/integration/LoggingDisableIntegration.test.ts new file mode 100644 index 000000000..84cc3b2dc --- /dev/null +++ b/src/debug/jtag/commands/logging/disable/test/integration/LoggingDisableIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * LoggingDisable Command Integration Tests + * + * Tests Logging Disable 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/Logging Disable/test/integration/LoggingDisableIntegration.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('๐Ÿงช LoggingDisable 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 Logging Disable command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\nโšก Test 2: Executing Logging Disable command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Logging Disable']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' ๐Ÿ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Logging Disable returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Logging Disable 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['Logging Disable']({ + // // 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['Logging Disable']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Logging Disable']({ + // 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['Logging Disable']({ /* 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['Logging Disable']({ /* 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 runAllLoggingDisableIntegrationTests(): Promise { + console.log('๐Ÿš€ Starting LoggingDisable 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 LoggingDisable 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โŒ LoggingDisable 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 runAllLoggingDisableIntegrationTests(); +} else { + module.exports = { runAllLoggingDisableIntegrationTests }; +} diff --git a/src/debug/jtag/commands/logging/disable/test/unit/LoggingDisableCommand.test.ts b/src/debug/jtag/commands/logging/disable/test/unit/LoggingDisableCommand.test.ts new file mode 100644 index 000000000..8d7dddb00 --- /dev/null +++ b/src/debug/jtag/commands/logging/disable/test/unit/LoggingDisableCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * LoggingDisable Command Unit Tests + * + * Tests Logging Disable 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/Logging Disable/test/unit/LoggingDisableCommand.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 { LoggingDisableParams, LoggingDisableResult } from '../../shared/LoggingDisableTypes'; + +console.log('๐Ÿงช LoggingDisable 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 Logging Disable logic for testing + */ +async function mockLoggingDisableCommand(params: LoggingDisableParams): 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 'Logging Disable' or see the Logging Disable 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 LoggingDisableResult; +} + +/** + * Test 1: Command structure validation + */ +function testLoggingDisableCommandStructure(): void { + console.log('\n๐Ÿ“‹ Test 1: LoggingDisable command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Logging Disable command + const validParams: LoggingDisableParams = { + // 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 testMockLoggingDisableExecution(): Promise { + console.log('\nโšก Test 2: Mock Logging Disable command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: LoggingDisableParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockLoggingDisableCommand(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 testLoggingDisableRequiredParams(): 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 LoggingDisableParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as LoggingDisableParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockLoggingDisableCommand({ ...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 testLoggingDisableOptionalParams(): 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: LoggingDisableParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockLoggingDisableCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: LoggingDisableParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockLoggingDisableCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('โœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testLoggingDisablePerformance(): Promise { + console.log('\nโšก Test 5: LoggingDisable performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockLoggingDisableCommand({ + // TODO: Add your parameters + context, + sessionId + } as LoggingDisableParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `LoggingDisable completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testLoggingDisableResultStructure(): Promise { + console.log('\n๐Ÿ” Test 6: LoggingDisable result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockLoggingDisableCommand({ + // TODO: Add your parameters + context, + sessionId + } as LoggingDisableParams); + + 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 runAllLoggingDisableUnitTests(): Promise { + console.log('๐Ÿš€ Starting LoggingDisable Command Unit Tests\n'); + + try { + testLoggingDisableCommandStructure(); + await testMockLoggingDisableExecution(); + await testLoggingDisableRequiredParams(); + await testLoggingDisableOptionalParams(); + await testLoggingDisablePerformance(); + await testLoggingDisableResultStructure(); + + console.log('\n๐ŸŽ‰ ALL LoggingDisable 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โŒ LoggingDisable 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 runAllLoggingDisableUnitTests(); +} else { + module.exports = { runAllLoggingDisableUnitTests }; +} diff --git a/src/debug/jtag/commands/logging/enable/.npmignore b/src/debug/jtag/commands/logging/enable/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/logging/enable/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/logging/enable/README.md b/src/debug/jtag/commands/logging/enable/README.md new file mode 100644 index 000000000..1bc7591a4 --- /dev/null +++ b/src/debug/jtag/commands/logging/enable/README.md @@ -0,0 +1,165 @@ +# Logging Enable Command + +Enable logging for a persona. Persists to .continuum/logging.json + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag logging/enable --persona= +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('logging/enable', { + // your parameters here +}); +``` + +## Parameters + +- **persona** (required): `string` - Persona uniqueId to enable logging for (e.g., 'helper', 'codereview') +- **category** (optional): `string` - Specific category to enable (e.g., 'cognition', 'hippocampus'). If not specified, enables all categories + +## Result + +Returns `LoggingEnableResult` with: + +Returns CommandResult with: +- **persona**: `string` - The persona that was enabled +- **categories**: `string[]` - Categories now enabled for this persona +- **message**: `string` - Human-readable status message + +## Examples + +### Enable all logging for helper persona + +```bash +./jtag logging/enable --persona=helper +``` + +**Expected result:** +{ persona: 'helper', categories: ['*'], message: 'Enabled all logging for helper' } + +### Enable only cognition logs + +```bash +./jtag logging/enable --persona=helper --category=cognition +``` + +**Expected result:** +{ persona: 'helper', categories: ['cognition'], message: 'Enabled cognition logging for helper' } + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help logging/enable +``` + +**Tool:** +```typescript +// Use your help tool with command name 'logging/enable' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme logging/enable +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'logging/enable' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/Logging Enable/test/unit/LoggingEnableCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/Logging Enable/test/integration/LoggingEnableIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/LoggingEnableTypes.ts` +- **Browser**: Browser-specific implementation in `browser/LoggingEnableBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/LoggingEnableServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/LoggingEnableCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/LoggingEnableIntegration.test.ts` diff --git a/src/debug/jtag/commands/logging/enable/browser/LoggingEnableBrowserCommand.ts b/src/debug/jtag/commands/logging/enable/browser/LoggingEnableBrowserCommand.ts new file mode 100644 index 000000000..01668fd28 --- /dev/null +++ b/src/debug/jtag/commands/logging/enable/browser/LoggingEnableBrowserCommand.ts @@ -0,0 +1,21 @@ +/** + * Logging Enable Command - Browser Implementation + * + * Enable logging for a persona. Persists to .continuum/logging.json + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { LoggingEnableParams, LoggingEnableResult } from '../shared/LoggingEnableTypes'; + +export class LoggingEnableBrowserCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('logging/enable', context, subpath, commander); + } + + async execute(params: LoggingEnableParams): Promise { + console.log('๐ŸŒ BROWSER: Delegating Logging Enable to server'); + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/logging/enable/package.json b/src/debug/jtag/commands/logging/enable/package.json new file mode 100644 index 000000000..e2b2e21b8 --- /dev/null +++ b/src/debug/jtag/commands/logging/enable/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/logging/enable", + "version": "1.0.0", + "description": "Enable logging for a persona. Persists to .continuum/logging.json", + "main": "server/LoggingEnableServerCommand.ts", + "types": "shared/LoggingEnableTypes.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/LoggingEnableIntegration.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", + "logging/enable" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/logging/enable/server/LoggingEnableServerCommand.ts b/src/debug/jtag/commands/logging/enable/server/LoggingEnableServerCommand.ts new file mode 100644 index 000000000..46b921214 --- /dev/null +++ b/src/debug/jtag/commands/logging/enable/server/LoggingEnableServerCommand.ts @@ -0,0 +1,71 @@ +/** + * Logging Enable Command - Server Implementation + * + * Enable logging for a persona. Persists to .continuum/logging.json + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import { ValidationError } from '@system/core/types/ErrorTypes'; +import type { LoggingEnableParams, LoggingEnableResult } from '../shared/LoggingEnableTypes'; +import { createLoggingEnableResultFromParams } from '../shared/LoggingEnableTypes'; +import { LoggingConfig, LOGGING_CATEGORIES } from '@system/core/logging/LoggingConfig'; + +export class LoggingEnableServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('logging/enable', context, subpath, commander); + } + + async execute(params: LoggingEnableParams): Promise { + // Validate persona parameter + if (!params.persona || params.persona.trim() === '') { + throw new ValidationError( + 'persona', + `Missing required parameter 'persona'. ` + + `Use: ./jtag logging/enable --persona=helper [--category=cognition]` + ); + } + + const persona = params.persona.trim(); + const category = params.category?.trim(); + + // Enable logging + if (category) { + // Enable specific category + LoggingConfig.setEnabled(persona, category, true); + } else { + // Enable all categories for persona + LoggingConfig.setPersonaEnabled(persona, true); + } + + // Get current state after update + const config = LoggingConfig.getConfig(); + const personaConfig = config.personas[persona.toLowerCase().replace(/\s+ai$/i, '').replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')]; + + // Determine enabled categories + let categories: string[]; + if (personaConfig && personaConfig.enabled) { + if (!personaConfig.categories || personaConfig.categories.length === 0) { + categories = Object.values(LOGGING_CATEGORIES); + } else if (personaConfig.categories.includes('*')) { + categories = Object.values(LOGGING_CATEGORIES); + } else { + categories = personaConfig.categories; + } + } else { + categories = []; + } + + const message = category + ? `Enabled ${category} logging for ${persona}` + : `Enabled all logging for ${persona}`; + + return createLoggingEnableResultFromParams(params, { + success: true, + persona, + categories, + message, + }); + } +} diff --git a/src/debug/jtag/commands/logging/enable/shared/LoggingEnableTypes.ts b/src/debug/jtag/commands/logging/enable/shared/LoggingEnableTypes.ts new file mode 100644 index 000000000..f556cec34 --- /dev/null +++ b/src/debug/jtag/commands/logging/enable/shared/LoggingEnableTypes.ts @@ -0,0 +1,99 @@ +/** + * Logging Enable Command - Shared Types + * + * Enable logging for a persona. Persists to .continuum/logging.json + */ + +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'; + +/** + * Logging Enable Command Parameters + */ +export interface LoggingEnableParams extends CommandParams { + // Persona uniqueId to enable logging for (e.g., 'helper', 'codereview') + persona: string; + // Specific category to enable (e.g., 'cognition', 'hippocampus'). If not specified, enables all categories + category?: string; +} + +/** + * Factory function for creating LoggingEnableParams + */ +export const createLoggingEnableParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + // Persona uniqueId to enable logging for (e.g., 'helper', 'codereview') + persona: string; + // Specific category to enable (e.g., 'cognition', 'hippocampus'). If not specified, enables all categories + category?: string; + } +): LoggingEnableParams => createPayload(context, sessionId, { + category: data.category ?? '', + ...data +}); + +/** + * Logging Enable Command Result + */ +export interface LoggingEnableResult extends CommandResult { + success: boolean; + // The persona that was enabled + persona: string; + // Categories now enabled for this persona + categories: string[]; + // Human-readable status message + message: string; + error?: JTAGError; +} + +/** + * Factory function for creating LoggingEnableResult with defaults + */ +export const createLoggingEnableResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + // The persona that was enabled + persona?: string; + // Categories now enabled for this persona + categories?: string[]; + // Human-readable status message + message?: string; + error?: JTAGError; + } +): LoggingEnableResult => createPayload(context, sessionId, { + persona: data.persona ?? '', + categories: data.categories ?? [], + message: data.message ?? '', + ...data +}); + +/** + * Smart Logging Enable-specific inheritance from params + * Auto-inherits context and sessionId from params + * Must provide all required result fields + */ +export const createLoggingEnableResultFromParams = ( + params: LoggingEnableParams, + differences: Omit +): LoggingEnableResult => transformPayload(params, differences); + +/** + * Logging Enable โ€” Type-safe command executor + * + * Usage: + * import { LoggingEnable } from '...shared/LoggingEnableTypes'; + * const result = await LoggingEnable.execute({ ... }); + */ +export const LoggingEnable = { + execute(params: CommandInput): Promise { + return Commands.execute('logging/enable', params as Partial); + }, + commandName: 'logging/enable' as const, +} as const; diff --git a/src/debug/jtag/commands/logging/enable/test/integration/LoggingEnableIntegration.test.ts b/src/debug/jtag/commands/logging/enable/test/integration/LoggingEnableIntegration.test.ts new file mode 100644 index 000000000..df5c60adb --- /dev/null +++ b/src/debug/jtag/commands/logging/enable/test/integration/LoggingEnableIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * LoggingEnable Command Integration Tests + * + * Tests Logging Enable 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/Logging Enable/test/integration/LoggingEnableIntegration.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('๐Ÿงช LoggingEnable 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 Logging Enable command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\nโšก Test 2: Executing Logging Enable command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Logging Enable']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' ๐Ÿ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Logging Enable returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Logging Enable 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['Logging Enable']({ + // // 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['Logging Enable']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Logging Enable']({ + // 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['Logging Enable']({ /* 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['Logging Enable']({ /* 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 runAllLoggingEnableIntegrationTests(): Promise { + console.log('๐Ÿš€ Starting LoggingEnable 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 LoggingEnable 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โŒ LoggingEnable 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 runAllLoggingEnableIntegrationTests(); +} else { + module.exports = { runAllLoggingEnableIntegrationTests }; +} diff --git a/src/debug/jtag/commands/logging/enable/test/unit/LoggingEnableCommand.test.ts b/src/debug/jtag/commands/logging/enable/test/unit/LoggingEnableCommand.test.ts new file mode 100644 index 000000000..30c212572 --- /dev/null +++ b/src/debug/jtag/commands/logging/enable/test/unit/LoggingEnableCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * LoggingEnable Command Unit Tests + * + * Tests Logging Enable 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/Logging Enable/test/unit/LoggingEnableCommand.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 { LoggingEnableParams, LoggingEnableResult } from '../../shared/LoggingEnableTypes'; + +console.log('๐Ÿงช LoggingEnable 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 Logging Enable logic for testing + */ +async function mockLoggingEnableCommand(params: LoggingEnableParams): 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 'Logging Enable' or see the Logging Enable 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 LoggingEnableResult; +} + +/** + * Test 1: Command structure validation + */ +function testLoggingEnableCommandStructure(): void { + console.log('\n๐Ÿ“‹ Test 1: LoggingEnable command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Logging Enable command + const validParams: LoggingEnableParams = { + // 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 testMockLoggingEnableExecution(): Promise { + console.log('\nโšก Test 2: Mock Logging Enable command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: LoggingEnableParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockLoggingEnableCommand(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 testLoggingEnableRequiredParams(): 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 LoggingEnableParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as LoggingEnableParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockLoggingEnableCommand({ ...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 testLoggingEnableOptionalParams(): 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: LoggingEnableParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockLoggingEnableCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: LoggingEnableParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockLoggingEnableCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('โœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testLoggingEnablePerformance(): Promise { + console.log('\nโšก Test 5: LoggingEnable performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockLoggingEnableCommand({ + // TODO: Add your parameters + context, + sessionId + } as LoggingEnableParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `LoggingEnable completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testLoggingEnableResultStructure(): Promise { + console.log('\n๐Ÿ” Test 6: LoggingEnable result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockLoggingEnableCommand({ + // TODO: Add your parameters + context, + sessionId + } as LoggingEnableParams); + + 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 runAllLoggingEnableUnitTests(): Promise { + console.log('๐Ÿš€ Starting LoggingEnable Command Unit Tests\n'); + + try { + testLoggingEnableCommandStructure(); + await testMockLoggingEnableExecution(); + await testLoggingEnableRequiredParams(); + await testLoggingEnableOptionalParams(); + await testLoggingEnablePerformance(); + await testLoggingEnableResultStructure(); + + console.log('\n๐ŸŽ‰ ALL LoggingEnable 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โŒ LoggingEnable 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 runAllLoggingEnableUnitTests(); +} else { + module.exports = { runAllLoggingEnableUnitTests }; +} diff --git a/src/debug/jtag/commands/logging/status/.npmignore b/src/debug/jtag/commands/logging/status/.npmignore new file mode 100644 index 000000000..f74ad6b8a --- /dev/null +++ b/src/debug/jtag/commands/logging/status/.npmignore @@ -0,0 +1,20 @@ +# Development files +.eslintrc* +tsconfig*.json +vitest.config.ts + +# Build artifacts +*.js.map +*.d.ts.map + +# IDE +.vscode/ +.idea/ + +# Logs +*.log +npm-debug.log* + +# OS files +.DS_Store +Thumbs.db diff --git a/src/debug/jtag/commands/logging/status/README.md b/src/debug/jtag/commands/logging/status/README.md new file mode 100644 index 000000000..c4be8b7d6 --- /dev/null +++ b/src/debug/jtag/commands/logging/status/README.md @@ -0,0 +1,166 @@ +# Logging Status Command + +Show current logging configuration for all personas or a specific persona + +## Table of Contents + +- [Usage](#usage) + - [CLI Usage](#cli-usage) + - [Tool Usage](#tool-usage) +- [Parameters](#parameters) +- [Result](#result) +- [Examples](#examples) +- [Testing](#testing) + - [Unit Tests](#unit-tests) + - [Integration Tests](#integration-tests) +- [Getting Help](#getting-help) +- [Access Level](#access-level) +- [Implementation Notes](#implementation-notes) + +## Usage + +### CLI Usage + +From the command line using the jtag CLI: + +```bash +./jtag logging/status [options] +``` + +### Tool Usage + +From Persona tools or programmatic access using `Commands.execute()`: + +```typescript +import { Commands } from '@system/core/shared/Commands'; + +const result = await Commands.execute('logging/status', { + // your parameters here +}); +``` + +## Parameters + +- **persona** (optional): `string` - Specific persona to show status for. If not specified, shows all personas + +## Result + +Returns `LoggingStatusResult` with: + +Returns CommandResult with: +- **personas**: `object[]` - Array of persona logging statuses with { persona, enabled, categories } +- **systemEnabled**: `boolean` - Whether system logging is enabled +- **defaultEnabled**: `boolean` - Default enabled state for unconfigured personas +- **availableCategories**: `string[]` - List of valid category names +- **summary**: `string` - Human-readable summary of logging state + +## Examples + +### Show all logging status + +```bash +./jtag logging/status +``` + +**Expected result:** +{ personas: [...], systemEnabled: true, defaultEnabled: false, summary: '2/11 personas logging enabled' } + +### Show status for specific persona + +```bash +./jtag logging/status --persona=helper +``` + +**Expected result:** +{ personas: [{ persona: 'helper', enabled: true, categories: ['cognition'] }], summary: 'helper: ON (cognition)' } + +## Getting Help + +### Using the Help Tool + +Get detailed usage information for this command: + +**CLI:** +```bash +./jtag help logging/status +``` + +**Tool:** +```typescript +// Use your help tool with command name 'logging/status' +``` + +### Using the README Tool + +Access this README programmatically: + +**CLI:** +```bash +./jtag readme logging/status +``` + +**Tool:** +```typescript +// Use your readme tool with command name 'logging/status' +``` + +## Testing + +### Unit Tests + +Test command logic in isolation using mock dependencies: + +```bash +# Run unit tests (no server required) +npx tsx commands/Logging Status/test/unit/LoggingStatusCommand.test.ts +``` + +**What's tested:** +- Command structure and parameter validation +- Mock command execution patterns +- Required parameter validation (throws ValidationError) +- Optional parameter handling (sensible defaults) +- Performance requirements +- Assertion utility helpers + +**TDD Workflow:** +1. Write/modify unit test first (test-driven development) +2. Run test, see it fail +3. Implement feature +4. Run test, see it pass +5. Refactor if needed + +### Integration Tests + +Test command with real client connections and system integration: + +```bash +# Prerequisites: Server must be running +npm start # Wait 90+ seconds for deployment + +# Run integration tests +npx tsx commands/Logging Status/test/integration/LoggingStatusIntegration.test.ts +``` + +**What's tested:** +- Client connection to live system +- Real command execution via WebSocket +- ValidationError handling for missing params +- Optional parameter defaults +- Performance under load +- Various parameter combinations + +**Best Practice:** +Run unit tests frequently during development (fast feedback). Run integration tests before committing (verify system integration). + +## Access Level + +**ai-safe** - Safe for AI personas to call autonomously + +## Implementation Notes + +- **Shared Logic**: Core business logic in `shared/LoggingStatusTypes.ts` +- **Browser**: Browser-specific implementation in `browser/LoggingStatusBrowserCommand.ts` +- **Server**: Server-specific implementation in `server/LoggingStatusServerCommand.ts` +- **Unit Tests**: Isolated testing in `test/unit/LoggingStatusCommand.test.ts` +- **Integration Tests**: System testing in `test/integration/LoggingStatusIntegration.test.ts` diff --git a/src/debug/jtag/commands/logging/status/browser/LoggingStatusBrowserCommand.ts b/src/debug/jtag/commands/logging/status/browser/LoggingStatusBrowserCommand.ts new file mode 100644 index 000000000..3de5fce54 --- /dev/null +++ b/src/debug/jtag/commands/logging/status/browser/LoggingStatusBrowserCommand.ts @@ -0,0 +1,21 @@ +/** + * Logging Status Command - Browser Implementation + * + * Show current logging configuration for all personas or a specific persona + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { LoggingStatusParams, LoggingStatusResult } from '../shared/LoggingStatusTypes'; + +export class LoggingStatusBrowserCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('logging/status', context, subpath, commander); + } + + async execute(params: LoggingStatusParams): Promise { + console.log('๐ŸŒ BROWSER: Delegating Logging Status to server'); + return await this.remoteExecute(params); + } +} diff --git a/src/debug/jtag/commands/logging/status/package.json b/src/debug/jtag/commands/logging/status/package.json new file mode 100644 index 000000000..07f47aed8 --- /dev/null +++ b/src/debug/jtag/commands/logging/status/package.json @@ -0,0 +1,35 @@ +{ + "name": "@jtag-commands/logging/status", + "version": "1.0.0", + "description": "Show current logging configuration for all personas or a specific persona", + "main": "server/LoggingStatusServerCommand.ts", + "types": "shared/LoggingStatusTypes.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/LoggingStatusIntegration.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", + "logging/status" + ], + "license": "MIT", + "author": "", + "repository": { + "type": "git", + "url": "" + } +} diff --git a/src/debug/jtag/commands/logging/status/server/LoggingStatusServerCommand.ts b/src/debug/jtag/commands/logging/status/server/LoggingStatusServerCommand.ts new file mode 100644 index 000000000..b254db968 --- /dev/null +++ b/src/debug/jtag/commands/logging/status/server/LoggingStatusServerCommand.ts @@ -0,0 +1,87 @@ +/** + * Logging Status Command - Server Implementation + * + * Show current logging configuration for all personas or a specific persona + */ + +import { CommandBase, type ICommandDaemon } from '@daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext } from '@system/core/types/JTAGTypes'; +import type { LoggingStatusParams, LoggingStatusResult } from '../shared/LoggingStatusTypes'; +import { createLoggingStatusResultFromParams } from '../shared/LoggingStatusTypes'; +import { LoggingConfig, LOGGING_CATEGORIES } from '@system/core/logging/LoggingConfig'; + +interface PersonaStatus { + persona: string; + enabled: boolean; + categories: string[]; +} + +export class LoggingStatusServerCommand extends CommandBase { + + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('logging/status', context, subpath, commander); + } + + async execute(params: LoggingStatusParams): Promise { + const config = LoggingConfig.getConfig(); + const allCategories = Object.values(LOGGING_CATEGORIES); + const personaFilter = params.persona?.trim().toLowerCase().replace(/\s+ai$/i, '').replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); + + // Build persona status list + const personas: PersonaStatus[] = []; + + for (const [personaId, personaConfig] of Object.entries(config.personas)) { + // Skip if filtering for specific persona + if (personaFilter && personaId !== personaFilter) { + continue; + } + + let categories: string[]; + if (!personaConfig.enabled) { + categories = []; + } else if (!personaConfig.categories || personaConfig.categories.length === 0) { + categories = allCategories; + } else if (personaConfig.categories.includes('*')) { + categories = allCategories; + } else { + categories = personaConfig.categories; + } + + personas.push({ + persona: personaId, + enabled: personaConfig.enabled, + categories, + }); + } + + // System logging status + const systemEnabled = config.system?.enabled ?? false; + const defaultEnabled = config.defaults?.enabled ?? false; + + // Build summary + const enabledCount = personas.filter(p => p.enabled).length; + let summary: string; + + if (personaFilter) { + const found = personas.find(p => p.persona === personaFilter); + if (found) { + summary = found.enabled + ? `${found.persona}: logging ENABLED (${found.categories.length} categories)` + : `${found.persona}: logging DISABLED`; + } else { + summary = `${personaFilter}: using defaults (${defaultEnabled ? 'enabled' : 'disabled'})`; + } + } else { + summary = `${enabledCount} persona(s) with logging enabled. Default: ${defaultEnabled ? 'ON' : 'OFF'}. System: ${systemEnabled ? 'ON' : 'OFF'}`; + } + + return createLoggingStatusResultFromParams(params, { + success: true, + personas, + systemEnabled, + defaultEnabled, + availableCategories: allCategories, + summary, + }); + } +} diff --git a/src/debug/jtag/commands/logging/status/shared/LoggingStatusTypes.ts b/src/debug/jtag/commands/logging/status/shared/LoggingStatusTypes.ts new file mode 100644 index 000000000..2128a0095 --- /dev/null +++ b/src/debug/jtag/commands/logging/status/shared/LoggingStatusTypes.ts @@ -0,0 +1,105 @@ +/** + * Logging Status Command - Shared Types + * + * Show current logging configuration for all personas or a specific persona + */ + +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'; + +/** + * Logging Status Command Parameters + */ +export interface LoggingStatusParams extends CommandParams { + // Specific persona to show status for. If not specified, shows all personas + persona?: string; +} + +/** + * Factory function for creating LoggingStatusParams + */ +export const createLoggingStatusParams = ( + context: JTAGContext, + sessionId: UUID, + data: { + // Specific persona to show status for. If not specified, shows all personas + persona?: string; + } +): LoggingStatusParams => createPayload(context, sessionId, { + persona: data.persona ?? '', + ...data +}); + +/** + * Logging Status Command Result + */ +export interface LoggingStatusResult extends CommandResult { + success: boolean; + // Array of persona logging statuses with { persona, enabled, categories } + personas: object[]; + // Whether system logging is enabled + systemEnabled: boolean; + // Default enabled state for unconfigured personas + defaultEnabled: boolean; + // List of valid category names + availableCategories: string[]; + // Human-readable summary of logging state + summary: string; + error?: JTAGError; +} + +/** + * Factory function for creating LoggingStatusResult with defaults + */ +export const createLoggingStatusResult = ( + context: JTAGContext, + sessionId: UUID, + data: { + success: boolean; + // Array of persona logging statuses with { persona, enabled, categories } + personas?: object[]; + // Whether system logging is enabled + systemEnabled?: boolean; + // Default enabled state for unconfigured personas + defaultEnabled?: boolean; + // List of valid category names + availableCategories?: string[]; + // Human-readable summary of logging state + summary?: string; + error?: JTAGError; + } +): LoggingStatusResult => createPayload(context, sessionId, { + personas: data.personas ?? [], + systemEnabled: data.systemEnabled ?? false, + defaultEnabled: data.defaultEnabled ?? false, + availableCategories: data.availableCategories ?? [], + summary: data.summary ?? '', + ...data +}); + +/** + * Smart Logging Status-specific inheritance from params + * Auto-inherits context and sessionId from params + * Must provide all required result fields + */ +export const createLoggingStatusResultFromParams = ( + params: LoggingStatusParams, + differences: Omit +): LoggingStatusResult => transformPayload(params, differences); + +/** + * Logging Status โ€” Type-safe command executor + * + * Usage: + * import { LoggingStatus } from '...shared/LoggingStatusTypes'; + * const result = await LoggingStatus.execute({ ... }); + */ +export const LoggingStatus = { + execute(params: CommandInput): Promise { + return Commands.execute('logging/status', params as Partial); + }, + commandName: 'logging/status' as const, +} as const; diff --git a/src/debug/jtag/commands/logging/status/test/integration/LoggingStatusIntegration.test.ts b/src/debug/jtag/commands/logging/status/test/integration/LoggingStatusIntegration.test.ts new file mode 100644 index 000000000..60d52955e --- /dev/null +++ b/src/debug/jtag/commands/logging/status/test/integration/LoggingStatusIntegration.test.ts @@ -0,0 +1,196 @@ +#!/usr/bin/env tsx +/** + * LoggingStatus Command Integration Tests + * + * Tests Logging Status 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/Logging Status/test/integration/LoggingStatusIntegration.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('๐Ÿงช LoggingStatus 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 Logging Status command on live system + */ +async function testCommandExecution(client: Awaited>): Promise { + console.log('\nโšก Test 2: Executing Logging Status command'); + + // TODO: Replace with your actual command parameters + const result = await client.commands['Logging Status']({ + // Add your required parameters here + // Example: name: 'test-value' + }); + + console.log(' ๐Ÿ“Š Result:', JSON.stringify(result, null, 2)); + + assert(result !== null, 'Logging Status returned result'); + // TODO: Add assertions for your specific result fields + // assert(result.success === true, 'Logging Status 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['Logging Status']({ + // // 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['Logging Status']({ + // requiredParam: 'test', + // optionalParam: true + // }); + // + // const withoutOptional = await client.commands['Logging Status']({ + // 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['Logging Status']({ /* 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['Logging Status']({ /* 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 runAllLoggingStatusIntegrationTests(): Promise { + console.log('๐Ÿš€ Starting LoggingStatus 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 LoggingStatus 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โŒ LoggingStatus 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 runAllLoggingStatusIntegrationTests(); +} else { + module.exports = { runAllLoggingStatusIntegrationTests }; +} diff --git a/src/debug/jtag/commands/logging/status/test/unit/LoggingStatusCommand.test.ts b/src/debug/jtag/commands/logging/status/test/unit/LoggingStatusCommand.test.ts new file mode 100644 index 000000000..1143d8714 --- /dev/null +++ b/src/debug/jtag/commands/logging/status/test/unit/LoggingStatusCommand.test.ts @@ -0,0 +1,259 @@ +#!/usr/bin/env tsx +/** + * LoggingStatus Command Unit Tests + * + * Tests Logging Status 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/Logging Status/test/unit/LoggingStatusCommand.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 { LoggingStatusParams, LoggingStatusResult } from '../../shared/LoggingStatusTypes'; + +console.log('๐Ÿงช LoggingStatus 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 Logging Status logic for testing + */ +async function mockLoggingStatusCommand(params: LoggingStatusParams): 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 'Logging Status' or see the Logging Status 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 LoggingStatusResult; +} + +/** + * Test 1: Command structure validation + */ +function testLoggingStatusCommandStructure(): void { + console.log('\n๐Ÿ“‹ Test 1: LoggingStatus command structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Create valid params for Logging Status command + const validParams: LoggingStatusParams = { + // 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 testMockLoggingStatusExecution(): Promise { + console.log('\nโšก Test 2: Mock Logging Status command execution'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test mock execution + const params: LoggingStatusParams = { + // TODO: Add your parameters here + context, + sessionId + }; + + const result = await mockLoggingStatusCommand(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 testLoggingStatusRequiredParams(): 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 LoggingStatusParams, desc: 'Missing requiredParam' }, + // { params: { requiredParam: '' } as LoggingStatusParams, desc: 'Empty requiredParam' }, + // ]; + // + // for (const testCase of testCases) { + // try { + // await mockLoggingStatusCommand({ ...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 testLoggingStatusOptionalParams(): 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: LoggingStatusParams = { + // requiredParam: 'test', + // context, + // sessionId + // }; + // + // const resultWithoutOptional = await mockLoggingStatusCommand(paramsWithoutOptional); + // assert(resultWithoutOptional.success === true, 'Command succeeds without optional params'); + + // TODO: Test WITH optional param + // const paramsWithOptional: LoggingStatusParams = { + // requiredParam: 'test', + // optionalParam: true, + // context, + // sessionId + // }; + // + // const resultWithOptional = await mockLoggingStatusCommand(paramsWithOptional); + // assert(resultWithOptional.success === true, 'Command succeeds with optional params'); + + console.log('โœ… Optional parameter handling validated'); +} + +/** + * Test 5: Performance validation + */ +async function testLoggingStatusPerformance(): Promise { + console.log('\nโšก Test 5: LoggingStatus performance validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + const startTime = Date.now(); + + await mockLoggingStatusCommand({ + // TODO: Add your parameters + context, + sessionId + } as LoggingStatusParams); + + const executionTime = Date.now() - startTime; + + assert(executionTime < 100, `LoggingStatus completed in ${executionTime}ms (under 100ms limit)`); +} + +/** + * Test 6: Result structure validation + */ +async function testLoggingStatusResultStructure(): Promise { + console.log('\n๐Ÿ” Test 6: LoggingStatus result structure validation'); + + const context = { environment: 'server' as const }; + const sessionId = generateUUID(); + + // Test various scenarios + const basicResult = await mockLoggingStatusCommand({ + // TODO: Add your parameters + context, + sessionId + } as LoggingStatusParams); + + 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 runAllLoggingStatusUnitTests(): Promise { + console.log('๐Ÿš€ Starting LoggingStatus Command Unit Tests\n'); + + try { + testLoggingStatusCommandStructure(); + await testMockLoggingStatusExecution(); + await testLoggingStatusRequiredParams(); + await testLoggingStatusOptionalParams(); + await testLoggingStatusPerformance(); + await testLoggingStatusResultStructure(); + + console.log('\n๐ŸŽ‰ ALL LoggingStatus 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โŒ LoggingStatus 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 runAllLoggingStatusUnitTests(); +} else { + module.exports = { runAllLoggingStatusUnitTests }; +} diff --git a/src/debug/jtag/commands/sentinel/logs/list/server/SentinelLogsListServerCommand.ts b/src/debug/jtag/commands/sentinel/logs/list/server/SentinelLogsListServerCommand.ts new file mode 100644 index 000000000..2d4cbf869 --- /dev/null +++ b/src/debug/jtag/commands/sentinel/logs/list/server/SentinelLogsListServerCommand.ts @@ -0,0 +1,88 @@ +/** + * Sentinel Logs List Command - Server Implementation + * + * List available log streams for a sentinel. + * Uses async file operations - NEVER blocks. + */ + +import { CommandBase, type ICommandDaemon } from '../../../../../daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext, JTAGPayload } from '../../../../../system/core/types/JTAGTypes'; +import { transformPayload } from '../../../../../system/core/types/JTAGTypes'; +import type { SentinelLogsListParams, SentinelLogsListResult, LogStreamInfo } from '../shared/SentinelLogsListTypes'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const BASE_DIR = '.sentinel-workspaces'; + +export class SentinelLogsListServerCommand extends CommandBase { + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('sentinel/logs/list', context, subpath, commander); + } + + async execute(params: JTAGPayload): Promise { + const listParams = params as SentinelLogsListParams; + const { handle } = listParams; + + if (!handle) { + return transformPayload(params, { + success: false, + handle: '', + logsDir: '', + streams: [], + error: 'Missing required parameter: handle', + }); + } + + const logsDir = path.join(BASE_DIR, handle, 'logs'); + + try { + // Check if directory exists + await fs.access(logsDir); + + // List all .log files + const files = await fs.readdir(logsDir); + const logFiles = files.filter(f => f.endsWith('.log')); + + // Get info for each file + const streams: LogStreamInfo[] = await Promise.all( + logFiles.map(async (filename) => { + const filePath = path.join(logsDir, filename); + const stats = await fs.stat(filePath); + return { + name: filename.replace('.log', ''), + path: filePath, + size: stats.size, + modifiedAt: stats.mtime.toISOString(), + }; + }) + ); + + // Sort by modified time (most recent first) + streams.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime()); + + return transformPayload(params, { + success: true, + handle, + logsDir, + streams, + }); + } catch (error: any) { + if (error.code === 'ENOENT') { + return transformPayload(params, { + success: false, + handle, + logsDir, + streams: [], + error: `No logs found for sentinel: ${handle}`, + }); + } + return transformPayload(params, { + success: false, + handle, + logsDir, + streams: [], + error: error.message, + }); + } + } +} diff --git a/src/debug/jtag/commands/sentinel/logs/list/shared/SentinelLogsListTypes.ts b/src/debug/jtag/commands/sentinel/logs/list/shared/SentinelLogsListTypes.ts new file mode 100644 index 000000000..c4f9f61f0 --- /dev/null +++ b/src/debug/jtag/commands/sentinel/logs/list/shared/SentinelLogsListTypes.ts @@ -0,0 +1,43 @@ +/** + * Sentinel Logs List Command - Types + * + * List available log streams for a sentinel. + */ + +import type { CommandParams, CommandResult } from '../../../../../system/core/types/JTAGTypes'; + +/** + * List params + */ +export interface SentinelLogsListParams extends CommandParams { + /** Sentinel handle (short ID or full ID) */ + handle: string; +} + +/** + * Log stream info + */ +export interface LogStreamInfo { + /** Stream name (e.g., "execution", "build-1", "stderr") */ + name: string; + + /** File path */ + path: string; + + /** Size in bytes */ + size: number; + + /** Last modified timestamp */ + modifiedAt: string; +} + +/** + * List result + */ +export interface SentinelLogsListResult extends CommandResult { + success: boolean; + handle: string; + logsDir: string; + streams: LogStreamInfo[]; + error?: string; +} diff --git a/src/debug/jtag/commands/sentinel/logs/read/server/SentinelLogsReadServerCommand.ts b/src/debug/jtag/commands/sentinel/logs/read/server/SentinelLogsReadServerCommand.ts new file mode 100644 index 000000000..810c8e908 --- /dev/null +++ b/src/debug/jtag/commands/sentinel/logs/read/server/SentinelLogsReadServerCommand.ts @@ -0,0 +1,104 @@ +/** + * Sentinel Logs Read Command - Server Implementation + * + * Read a log stream for a sentinel with optional offset/limit. + * Uses async file operations - NEVER blocks. + */ + +import { CommandBase, type ICommandDaemon } from '../../../../../daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext, JTAGPayload } from '../../../../../system/core/types/JTAGTypes'; +import { transformPayload } from '../../../../../system/core/types/JTAGTypes'; +import type { SentinelLogsReadParams, SentinelLogsReadResult } from '../shared/SentinelLogsReadTypes'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const BASE_DIR = '.sentinel-workspaces'; + +export class SentinelLogsReadServerCommand extends CommandBase { + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('sentinel/logs/read', context, subpath, commander); + } + + async execute(params: JTAGPayload): Promise { + const readParams = params as SentinelLogsReadParams; + const { handle, stream, offset = 0, limit } = readParams; + + if (!handle) { + return transformPayload(params, { + success: false, + handle: '', + stream: '', + content: '', + lineCount: 0, + totalLines: 0, + truncated: false, + error: 'Missing required parameter: handle', + }); + } + + if (!stream) { + return transformPayload(params, { + success: false, + handle, + stream: '', + content: '', + lineCount: 0, + totalLines: 0, + truncated: false, + error: 'Missing required parameter: stream', + }); + } + + const logPath = path.join(BASE_DIR, handle, 'logs', `${stream}.log`); + + try { + const content = await fs.readFile(logPath, 'utf-8'); + const allLines = content.split('\n'); + const totalLines = allLines.length; + + // Apply offset and limit + let selectedLines = allLines.slice(offset); + let truncated = false; + + if (limit !== undefined && limit > 0) { + if (selectedLines.length > limit) { + selectedLines = selectedLines.slice(0, limit); + truncated = true; + } + } + + return transformPayload(params, { + success: true, + handle, + stream, + content: selectedLines.join('\n'), + lineCount: selectedLines.length, + totalLines, + truncated, + }); + } catch (error: any) { + if (error.code === 'ENOENT') { + return transformPayload(params, { + success: false, + handle, + stream, + content: '', + lineCount: 0, + totalLines: 0, + truncated: false, + error: `Log stream not found: ${stream}`, + }); + } + return transformPayload(params, { + success: false, + handle, + stream, + content: '', + lineCount: 0, + totalLines: 0, + truncated: false, + error: error.message, + }); + } + } +} diff --git a/src/debug/jtag/commands/sentinel/logs/read/shared/SentinelLogsReadTypes.ts b/src/debug/jtag/commands/sentinel/logs/read/shared/SentinelLogsReadTypes.ts new file mode 100644 index 000000000..224ff7eea --- /dev/null +++ b/src/debug/jtag/commands/sentinel/logs/read/shared/SentinelLogsReadTypes.ts @@ -0,0 +1,38 @@ +/** + * Sentinel Logs Read Command - Types + * + * Read a log stream for a sentinel. + */ + +import type { CommandParams, CommandResult } from '../../../../../system/core/types/JTAGTypes'; + +/** + * Read params + */ +export interface SentinelLogsReadParams extends CommandParams { + /** Sentinel handle (short ID or full ID) */ + handle: string; + + /** Stream name (e.g., "execution", "build-1", "stderr") */ + stream: string; + + /** Start from line number (0-indexed, default: 0) */ + offset?: number; + + /** Maximum lines to return (default: all) */ + limit?: number; +} + +/** + * Read result + */ +export interface SentinelLogsReadResult extends CommandResult { + success: boolean; + handle: string; + stream: string; + content: string; + lineCount: number; + totalLines: number; + truncated: boolean; + error?: string; +} diff --git a/src/debug/jtag/commands/sentinel/logs/tail/server/SentinelLogsTailServerCommand.ts b/src/debug/jtag/commands/sentinel/logs/tail/server/SentinelLogsTailServerCommand.ts new file mode 100644 index 000000000..0a6a6685a --- /dev/null +++ b/src/debug/jtag/commands/sentinel/logs/tail/server/SentinelLogsTailServerCommand.ts @@ -0,0 +1,86 @@ +/** + * Sentinel Logs Tail Command - Server Implementation + * + * Get the last N lines of a log stream (like Unix tail). + * Uses async file operations - NEVER blocks. + */ + +import { CommandBase, type ICommandDaemon } from '../../../../../daemons/command-daemon/shared/CommandBase'; +import type { JTAGContext, JTAGPayload } from '../../../../../system/core/types/JTAGTypes'; +import { transformPayload } from '../../../../../system/core/types/JTAGTypes'; +import type { SentinelLogsTailParams, SentinelLogsTailResult } from '../shared/SentinelLogsTailTypes'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +const BASE_DIR = '.sentinel-workspaces'; +const DEFAULT_LINES = 20; + +export class SentinelLogsTailServerCommand extends CommandBase { + constructor(context: JTAGContext, subpath: string, commander: ICommandDaemon) { + super('sentinel/logs/tail', context, subpath, commander); + } + + async execute(params: JTAGPayload): Promise { + const tailParams = params as SentinelLogsTailParams; + const { handle, stream, lines = DEFAULT_LINES } = tailParams; + + if (!handle) { + return transformPayload(params, { + success: false, + handle: '', + stream: '', + content: '', + lineCount: 0, + error: 'Missing required parameter: handle', + }); + } + + if (!stream) { + return transformPayload(params, { + success: false, + handle, + stream: '', + content: '', + lineCount: 0, + error: 'Missing required parameter: stream', + }); + } + + const logPath = path.join(BASE_DIR, handle, 'logs', `${stream}.log`); + + try { + const content = await fs.readFile(logPath, 'utf-8'); + const allLines = content.split('\n'); + + // Get last N lines + const tailLines = allLines.slice(-lines); + + return transformPayload(params, { + success: true, + handle, + stream, + content: tailLines.join('\n'), + lineCount: tailLines.length, + }); + } catch (error: any) { + if (error.code === 'ENOENT') { + return transformPayload(params, { + success: false, + handle, + stream, + content: '', + lineCount: 0, + error: `Log stream not found: ${stream}`, + }); + } + return transformPayload(params, { + success: false, + handle, + stream, + content: '', + lineCount: 0, + error: error.message, + }); + } + } +} diff --git a/src/debug/jtag/commands/sentinel/logs/tail/shared/SentinelLogsTailTypes.ts b/src/debug/jtag/commands/sentinel/logs/tail/shared/SentinelLogsTailTypes.ts new file mode 100644 index 000000000..73dabefea --- /dev/null +++ b/src/debug/jtag/commands/sentinel/logs/tail/shared/SentinelLogsTailTypes.ts @@ -0,0 +1,33 @@ +/** + * Sentinel Logs Tail Command - Types + * + * Get the last N lines of a log stream (like Unix tail). + */ + +import type { CommandParams, CommandResult } from '../../../../../system/core/types/JTAGTypes'; + +/** + * Tail params + */ +export interface SentinelLogsTailParams extends CommandParams { + /** Sentinel handle (short ID or full ID) */ + handle: string; + + /** Stream name (e.g., "execution", "build-1", "stderr") */ + stream: string; + + /** Number of lines from the end (default: 20) */ + lines?: number; +} + +/** + * Tail result + */ +export interface SentinelLogsTailResult extends CommandResult { + success: boolean; + handle: string; + stream: string; + content: string; + lineCount: number; + error?: string; +} diff --git a/src/debug/jtag/docs/OBSERVABILITY-ARCHITECTURE.md b/src/debug/jtag/docs/OBSERVABILITY-ARCHITECTURE.md new file mode 100644 index 000000000..c1c94321c --- /dev/null +++ b/src/debug/jtag/docs/OBSERVABILITY-ARCHITECTURE.md @@ -0,0 +1,724 @@ +# Observability Architecture + +## Vision + +**Complete observability** for a system where multiple AI personas think, act, and collaborate autonomously. Every thought, decision, tool call, and build output must be: + +1. **Captured** - Full logs, not truncated +2. **Segregated** - Per-persona, per-subsystem, per-sentinel +3. **Controllable** - Turn on/off via config, state, or command +4. **Streamable** - Real-time events for UI display +5. **Queryable** - Search, filter, correlate across logs + +**The ideal**: Watch Helper AI's prefrontal cortex think through a problem, see its tool calls execute, observe the build sentinel fix errors, and drill into any step - all with the ability to mute noisy subsystems. + +--- + +## Three Domains of Observability + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ OBSERVABILITY DOMAINS โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ SYSTEM LOGS โ”‚ โ”‚ COGNITION LOGS โ”‚ โ”‚ SENTINEL LOGS โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ Infrastructure โ”‚ โ”‚ AI Thinking โ”‚ โ”‚ Autonomous โ”‚ โ”‚ +โ”‚ โ”‚ - Daemons โ”‚ โ”‚ - Prefrontal โ”‚ โ”‚ Execution โ”‚ โ”‚ +โ”‚ โ”‚ - Adapters โ”‚ โ”‚ - Hippocampus โ”‚ โ”‚ - Build output โ”‚ โ”‚ +โ”‚ โ”‚ - SQL queries โ”‚ โ”‚ - Motor cortex โ”‚ โ”‚ - LLM queries โ”‚ โ”‚ +โ”‚ โ”‚ - Events โ”‚ โ”‚ - Limbic system โ”‚ โ”‚ - File changes โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ - Training โ”‚ โ”‚ - Evidence โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ โ”‚ +โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ UNIFIED CONTROLS โ”‚ โ”‚ +โ”‚ โ”‚ - LoggingConfig โ”‚ โ”‚ +โ”‚ โ”‚ - LogLevelRegistry โ”‚ โ”‚ +โ”‚ โ”‚ - CLI commands โ”‚ โ”‚ +โ”‚ โ”‚ - Event streaming โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +--- + +## 1. System Logs + +Infrastructure logging for daemons, adapters, and core services. + +### Directory Structure + +``` +.continuum/jtag/logs/system/ +โ”œโ”€โ”€ system.log # Global events (startup, shutdown) +โ”œโ”€โ”€ sql.log # Database operations +โ”œโ”€โ”€ adapters.log # AI provider adapter activity +โ”œโ”€โ”€ tools.log # System-level tool execution +โ”œโ”€โ”€ coordination.log # Coordination state +โ”œโ”€โ”€ ai-decisions-*.log # AI decision training data +โ””โ”€โ”€ daemons/ # Per-daemon logs (auto-routed) + โ”œโ”€โ”€ ArchiveDaemonServer.log + โ”œโ”€โ”€ SessionDaemonServer.log + โ””โ”€โ”€ ... +``` + +### Usage + +```typescript +import { Logger } from '@system/core/logging/Logger'; + +// Auto-routes based on component name suffix +const log = Logger.create('ArchiveDaemonServer'); // โ†’ daemons/ArchiveDaemonServer.log +const log = Logger.create('SqliteStorageAdapter'); // โ†’ adapters/SqliteStorageAdapter.log + +log.info('Starting operation'); +log.debug('Detailed context', { params }); +log.warn('Unusual condition'); +log.error('Operation failed', error); +``` + +### Controls + +**Environment variables:** +```bash +LOG_LEVEL=warn # error | warn | info | debug +LOG_TO_CONSOLE=0 # Disable console output +LOG_TO_FILES=1 # Enable file logging +LOG_FILE_MODE=clean # clean | append | archive +``` + +**Runtime level control:** +```typescript +import { LogLevelRegistry, LogLevel } from '@system/core/logging/LogLevelRegistry'; + +// Set global level +LogLevelRegistry.instance.globalLevel = LogLevel.WARN; + +// Set per-component level (suppress noisy components) +LogLevelRegistry.instance.configure({ + 'SessionDaemonServer': LogLevel.WARN, // Only warnings and errors + 'ArchiveDaemonServer': LogLevel.ERROR, // Only errors + 'PersonaUser': LogLevel.WARN, // Suppress autonomous loop spam +}); +``` + +--- + +## 2. Cognition Logs (Neuroanatomy-Inspired) + +Per-persona logging organized by cognitive subsystem. Each persona gets isolated log directories with brain-region-inspired categories. + +### Directory Structure + +``` +.continuum/personas/{uniqueId}/logs/ +โ”œโ”€โ”€ prefrontal.log # High-level cognition, planning, state tracking +โ”œโ”€โ”€ motor-cortex.log # Action execution, response generation +โ”œโ”€โ”€ limbic.log # Memory, learning, emotion, genome operations +โ”œโ”€โ”€ hippocampus.log # Memory consolidation, recall +โ”œโ”€โ”€ cns.log # Central nervous system coordination +โ”œโ”€โ”€ cognition.log # Decision-making, thought streams +โ”œโ”€โ”€ tools.log # Tool execution (per-persona) +โ”œโ”€โ”€ genome.log # LoRA adapter paging, eviction +โ”œโ”€โ”€ training.log # Fine-tuning data accumulation +โ””โ”€โ”€ user.log # User interaction events +``` + +### Usage + +```typescript +import { SubsystemLogger } from '@system/user/server/modules/being/logging/SubsystemLogger'; + +class PrefrontalCortex { + private logger: SubsystemLogger; + + constructor(persona: PersonaUser) { + this.logger = new SubsystemLogger('prefrontal', persona.id, persona.entity.uniqueId); + } + + async processThought(thought: string): Promise { + this.logger.info('Processing thought', { thought }); + // ... cognition ... + this.logger.debug('Decision made', { action: 'respond' }); + } +} +``` + +### The On/Off Control System + +**LoggingConfig** (`system/core/logging/LoggingConfig.ts`) provides per-persona, per-category control. + +**Config file:** `.continuum/logging.json` + +```json +{ + "version": 1, + "defaults": { + "enabled": false, // OFF by default - opt-in logging + "categories": [] + }, + "personas": { + "*": { "enabled": false }, // All personas off by default + "helper": { + "enabled": true, + "categories": ["cognition", "hippocampus"] // Only these categories + }, + "codereview": { + "enabled": true, + "categories": ["*"] // All categories + } + }, + "system": { + "enabled": true, + "categories": [] + } +} +``` + +**Programmatic control:** + +```typescript +import { LoggingConfig } from '@system/core/logging/LoggingConfig'; + +// Check if enabled +LoggingConfig.isEnabled('helper', 'cognition'); // true +LoggingConfig.isEnabled('helper', 'training'); // false + +// Enable/disable +LoggingConfig.setEnabled('helper', 'training', true); +LoggingConfig.setPersonaEnabled('teacher', false); // Disable all for persona + +// Hot reload (if edited externally) +LoggingConfig.reload(); +``` + +**CLI commands (planned):** + +```bash +# Enable specific persona + category +./jtag logging/enable --persona=helper --category=cognition + +# Disable all logging for a persona +./jtag logging/disable --persona=teacher + +# Show current config +./jtag logging/config + +# Set to verbose (all personas, all categories) +./jtag logging/config --preset=verbose + +# Set to minimal (errors only) +./jtag logging/config --preset=minimal +``` + +### Known Logging Categories + +```typescript +export const LOGGING_CATEGORIES = { + // Persona categories (cognition subsystems) + COGNITION: 'cognition', // Task processing, decisions + HIPPOCAMPUS: 'hippocampus', // Memory consolidation + TRAINING: 'training', // Fine-tuning data + GENOME: 'genome', // LoRA adapter paging + USER: 'user', // Persona lifecycle + ADAPTERS: 'adapters', // AI provider activity + + // System categories + SERVER: 'server', + BROWSER: 'browser', + COMMANDS: 'commands', + EVENTS: 'events' +} as const; +``` + +--- + +## 3. Sentinel Logs + +Execution logs for autonomous agents (BuildSentinel, TaskSentinel, etc.). Unlike cognition logs which track thinking, sentinel logs track **doing**. + +### Current State + +Sentinels currently use `SentinelExecutionLog` for structured action tracking: + +```typescript +// SentinelExecutionLog captures: +interface SentinelAction { + timestamp: string; + type: 'build' | 'analyze' | 'fix' | 'llm_query' | 'file_edit' | 'escalate'; + intent: string; + result: 'success' | 'failure' | 'skipped'; + evidence?: { + output?: string; // TRUNCATED to ~200 chars + outputFile?: string; // Reference to full log + }; +} +``` + +**Problem**: Build output is captured but truncated. Massive compilation logs are lost. + +### Planned Architecture + +**Per-sentinel log directories** inside workspace: + +``` +.sentinel-workspaces/{handle}/ +โ””โ”€โ”€ logs/ + โ”œโ”€โ”€ sentinel.log # High-level actions + โ”œโ”€โ”€ build-1.log # FULL output from build attempt 1 + โ”œโ”€โ”€ build-2.log # FULL output from build attempt 2 + โ”œโ”€โ”€ llm-requests.log # LLM queries and responses + โ””โ”€โ”€ evidence/ # Structured evidence files +``` + +**Real-time streaming via Events:** + +```typescript +// Emit as build runs +Events.emit(`sentinel:${handle}:log`, { + stream: 'build-1', + chunk: 'Compiling src/index.ts...\n', + sourceType: 'stdout' +}); + +// Subscribe for real-time UI +Events.subscribe(`sentinel:${handle}:log`, (event) => { + appendToUI(event.payload.chunk); +}); +``` + +**CLI commands:** + +```bash +# List logs for a sentinel run +./jtag sentinel/logs/list --handle=abc123 + +# Read full build output (not truncated!) +./jtag sentinel/logs/read --handle=abc123 --stream=build-1 + +# Tail in real-time +./jtag sentinel/logs/tail --handle=abc123 --stream=build-1 +``` + +--- + +## Unified Control Architecture + +### Configuration Hierarchy + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ CONFIGURATION SOURCES โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ โ”‚ +โ”‚ 1. Environment Variables (highest priority for system) โ”‚ +โ”‚ LOG_LEVEL=warn LOG_TO_CONSOLE=0 โ”‚ +โ”‚ โ”‚ +โ”‚ 2. LogLevelRegistry (runtime per-component) โ”‚ +โ”‚ LogLevelRegistry.instance.configure({...}) โ”‚ +โ”‚ โ”‚ +โ”‚ 3. LoggingConfig (per-persona, per-category) โ”‚ +โ”‚ .continuum/logging.json โ”‚ +โ”‚ โ”‚ +โ”‚ 4. CLI Commands (runtime adjustment) โ”‚ +โ”‚ ./jtag logging/enable --persona=helper โ”‚ +โ”‚ โ”‚ +โ”‚ 5. State System (persistent preferences) โ”‚ +โ”‚ ./jtag state/set --key=logging:helper:enabled --value=true โ”‚ +โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### Flow: How Logging Decisions Are Made + +```typescript +// In SubsystemLogger.ts +private get _enabled(): boolean { + return LoggingConfig.isEnabled(this.uniqueId, this.subsystem); +} + +info(message: string, ...args: unknown[]): void { + if (!this._enabled) return; // Early exit if disabled + this.logger.info(message, ...args); +} +``` + +```typescript +// In LoggingConfig.ts +static isEnabled(personaId: string, category: string): boolean { + // 1. Check persona-specific config + let config = this.config.personas[personaId]; + + // 2. Fall back to wildcard "*" + if (!config) config = this.config.personas['*']; + + // 3. Fall back to defaults + if (!config) return this.config.defaults.enabled; + + // 4. Check category filter + if (config.categories?.includes('*')) return true; + if (config.categories?.length > 0) { + return config.categories.includes(category); + } + + return config.enabled; +} +``` + +### Presets for Quick Configuration + +```typescript +const LOGGING_PRESETS = { + // Everything off except errors + minimal: { + defaults: { enabled: false }, + personas: { '*': { enabled: false } }, + system: { enabled: true, categories: ['error'] } + }, + + // System on, personas off + system_only: { + defaults: { enabled: false }, + personas: { '*': { enabled: false } }, + system: { enabled: true } + }, + + // Focus on one persona + focus_helper: { + defaults: { enabled: false }, + personas: { + '*': { enabled: false }, + 'helper': { enabled: true, categories: ['*'] } + } + }, + + // Everything on (verbose debugging) + verbose: { + defaults: { enabled: true, categories: ['*'] }, + personas: { '*': { enabled: true, categories: ['*'] } }, + system: { enabled: true, categories: ['*'] } + } +}; +``` + +--- + +## Event-Based Observability + +### Event Types + +| Event Pattern | Domain | Description | +|---------------|--------|-------------| +| `log:{component}:{level}` | System | System log entry | +| `persona:{id}:cognition:{subsystem}` | Cognition | Persona thought/decision | +| `sentinel:{handle}:log` | Sentinel | Real-time execution output | +| `sentinel:{handle}:action` | Sentinel | Action taken (build, fix, etc.) | +| `sentinel:{handle}:status` | Sentinel | Status change (running, completed) | +| `tool:{name}:start` | Tools | Tool execution started | +| `tool:{name}:result` | Tools | Tool execution completed | + +### Subscribing to Observability Events + +```typescript +import { Events } from '@system/core/shared/Events'; + +// Watch a specific persona's cognition +Events.subscribe('persona:helper:cognition:prefrontal', (event) => { + console.log('Helper thinking:', event.thought); +}); + +// Watch all sentinel activity +Events.subscribe('sentinel:*:action', (event) => { + console.log(`[${event.handle}] ${event.type}: ${event.intent}`); +}); + +// Watch tool execution across all personas +Events.subscribe('tool:*:result', (event) => { + console.log(`Tool ${event.name} returned:`, event.result); +}); +``` + +### Real-Time UI Integration + +```typescript +// In chat widget, show inline logs for tool calls +class ChatMessage { + render() { + if (this.message.toolCall) { + return html` +
+ ${this.message.toolCall.name} + ${this.message.toolCall.sentinelHandle ? html` + + ` : ''} +
+ `; + } + } +} +``` + +--- + +## CLI Commands Reference + +### Discovery + +```bash +# List all logs with metadata +./jtag logs/list +./jtag logs/list --category=persona +./jtag logs/list --personaUniqueId=helper + +# Get statistics +./jtag logs/stats +./jtag logs/stats --category=persona --personaUniqueId=helper +``` + +### Reading + +```bash +# Read log content +./jtag logs/read --log="helper/cognition" --tail=50 +./jtag logs/read --log="system/adapters" --startLine=1 --endLine=100 + +# Search across logs +./jtag logs/search --pattern="ERROR" +./jtag logs/search --pattern="timeout" --contextBefore=5 + +# Sentinel-specific +./jtag sentinel/logs/list --handle=abc123 +./jtag sentinel/logs/read --handle=abc123 --stream=build-1 +``` + +### Configuration + +```bash +# Current config +./jtag logging/config + +# Enable/disable +./jtag logging/enable --persona=helper --category=cognition +./jtag logging/disable --persona=teacher +./jtag logging/disable --all # Emergency shutoff + +# Presets +./jtag logging/config --preset=minimal +./jtag logging/config --preset=verbose +./jtag logging/config --preset=focus_helper +``` + +### Maintenance + +```bash +# Cleanup old logs +./jtag logs/cleanup --olderThan=7d +./jtag logs/cleanup --category=session --olderThan=1d + +# Export for analysis +./jtag logs/export --persona=helper --output=/tmp/helper-logs.tar.gz +``` + +--- + +## Implementation Status + +### Completed + +- [x] Logger.ts core with async write queues +- [x] Per-component auto-routing (daemons/, adapters/, etc.) +- [x] SubsystemLogger for neuroanatomy-inspired persona logs +- [x] LoggingConfig for per-persona/category control +- [x] LogLevelRegistry for runtime level adjustment +- [x] logs/list, logs/read, logs/search, logs/stats commands +- [x] SentinelExecutionLog with streaming events +- [x] SentinelWorkspace for isolated execution + +### In Progress + +- [ ] Sentinel per-run log directories +- [ ] Full build output capture (not truncated) +- [ ] sentinel/logs/* commands + +### Completed (Recent) + +- [x] logging/enable command +- [x] logging/disable command +- [x] logging/status command + +### Planned + +- [ ] Real-time log streaming UI widget +- [ ] Inline chat log display for tool calls +- [ ] Log retention policies +- [ ] Log compression/archival +- [ ] AI-queryable structured logs (natural language queries) +- [ ] Probes - targeted, conditional observation points (requires concurrent architecture) + +--- + +## Performance Considerations + +### Current Bottlenecks + +1. **254+ open file descriptors** - Close to macOS default limit (256) +2. **Memory pressure** - Each queue holds buffered writes +3. **No compression** - Plain text files grow large +4. **No rotation** - CLEAN truncates, APPEND grows unbounded + +### Mitigation Strategies + +1. **Aggressive filtering** - Use LoggingConfig to disable unused categories +2. **Level suppression** - Set noisy components to WARN/ERROR only +3. **Retention policies** - Auto-delete logs older than N days +4. **Rust backend** - LoggerModule in continuum-core handles high-throughput + +### Rust Logger Integration + +The Logger class can route to Rust for high-performance logging: + +```typescript +// Logger.ts +if (this.useRustLogger && this.workerClient?.connected) { + // Send to Rust LoggerModule via Unix socket + await this.workerClient.writeLog(logFile, message, level); +} else { + // Fallback to TypeScript file I/O + this.queueMessage(logFile, message); +} +``` + +--- + +## Quick Reference: "I Want To..." + +| Goal | Solution | +|------|----------| +| See what a persona is thinking | `./jtag logs/read --log="helper/prefrontal" --tail=50` | +| Watch build output in real-time | `./jtag sentinel/logs/tail --handle=X --stream=build-1` | +| Disable noisy persona logs | `./jtag logging/disable --persona=helper` | +| Enable only cognition logs | `./jtag logging/enable --persona=helper --category=cognition` | +| Find all errors | `./jtag logs/search --pattern="ERROR"` | +| Reduce logging overhead | `./jtag logging/config --preset=minimal` | +| Debug one specific persona | `./jtag logging/config --preset=focus_helper` | +| Clean up old logs | `./jtag logs/cleanup --olderThan=7d` | +| Check current config | `./jtag logging/config` | +| Edit config manually | `vim .continuum/logging.json` (hot reloads) | + +--- + +## 4. Probes (Future) + +Probes are **targeted, conditional, parameterized observation points** - more surgical than logging. While logging is "turn on the firehose for category X", probes answer specific questions. + +### Why Probes, Not Breakpoints + +Traditional debuggers use breakpoints that **stop** execution. This doesn't work for async distributed systems: + +- Pausing one persona breaks timing-dependent coordination +- Deadlocks if a persona pauses mid-conversation +- Only works if architecture is truly concurrent (isolated actors, message passing) + +Probes **observe without stopping** - they're instrumentation, not interruption. + +### Probe Concept + +```typescript +// Define a probe - "next time Helper's gating decision has low confidence, capture it" +probe/create --name="gating-low-conf" \ + --target="persona:helper:cognition:gating" \ + --condition="confidence < 0.3" \ + --mode="once" + +// Modes: +// - once: Capture next occurrence, then disable +// - count:N: Capture N occurrences, then disable +// - duration:Xs: Capture for X seconds, then disable +// - accumulate: Collect until manually disabled, then dump summary + +// Check what it captured +probe/results --name="gating-low-conf" + +// Disable +probe/disable --name="gating-low-conf" +``` + +### Implementation Requirements + +Probes need: + +1. **Probe registry** - What probes exist, their conditions, their state +2. **Instrumentation points** - Code locations that check for active probes +3. **Condition evaluation** - Fast check: `if (probeActive && conditionMet)` +4. **Result storage** - Captured data with the parameterized limits +5. **Concurrent-safe architecture** - Probes must not interfere with execution + +### Probe vs Logging + +| Aspect | Logging | Probes | +|--------|---------|--------| +| Scope | Category-wide (cognition, hippocampus) | Single point + condition | +| Duration | Persistent until disabled | Parameterized (once, N times, duration) | +| Output | Stream to file | Captured results, queryable | +| Overhead | Always on when enabled | Only when condition checked | +| Use case | "What's happening?" | "Why did X happen?" | + +### Example Use Cases + +```bash +# "Why did Helper stay silent on that message?" +probe/create --name="helper-silent" \ + --target="persona:helper:cognition:gating" \ + --condition="shouldRespond == false" \ + --mode="once" \ + --capture="reason,confidence,ragContextSummary" + +# "Are we hitting rate limits?" +probe/create --name="rate-limit-hits" \ + --target="persona:*:ratelimiter:blocked" \ + --mode="accumulate" \ + --capture="personaId,roomId,waitTimeSeconds" + +# "What's the inference slot denial rate?" +probe/create --name="slot-denials" \ + --target="coordination:inference:denied" \ + --mode="duration:60s" \ + --capture="personaId,reason" +``` + +### Status + +**Not implemented** - Requires proper concurrent architecture underneath. The logging system handles the "firehose when needed" case. Probes are for surgical debugging of specific questions. + +--- + +## Architecture Principles + +1. **Segregation by identity** - Each persona's logs are isolated +2. **Segregation by concern** - Subsystems have dedicated log files +3. **Opt-in by default** - Logging is OFF until explicitly enabled +4. **Hot reload** - Config changes take effect without restart +5. **Graceful degradation** - Log failures never crash the system +6. **Event-driven** - Real-time streaming for UI integration +7. **Single source of truth** - LoggingConfig is the authority + +--- + +## Related Documents + +- [LOGGING.md](LOGGING.md) - Detailed logging implementation guide +- [LOGGING-PATHS-DESIGN.md](LOGGING-PATHS-DESIGN.md) - Path resolution design +- [SENTINEL-ARCHITECTURE.md](SENTINEL-ARCHITECTURE.md) - Sentinel system design +- [PERSONA-BEING-ARCHITECTURE.md](PERSONA-BEING-ARCHITECTURE.md) - Cognition subsystems + +--- + +**Document version**: 1.0 (February 2026) +**Covers**: System logs, Cognition logs, Sentinel logs, Unified controls diff --git a/src/debug/jtag/docs/SENTINEL-LOGGING-PLAN.md b/src/debug/jtag/docs/SENTINEL-LOGGING-PLAN.md new file mode 100644 index 000000000..d270e5458 --- /dev/null +++ b/src/debug/jtag/docs/SENTINEL-LOGGING-PLAN.md @@ -0,0 +1,704 @@ +# Sentinel Logging & Observability Plan + +> **Note**: This document focuses on sentinel-specific logging. For the unified observability architecture covering system logs, cognition logs, and controls, see [OBSERVABILITY-ARCHITECTURE.md](OBSERVABILITY-ARCHITECTURE.md). + +## Vision + +Sentinels are autonomous agents that do real work (compile, test, deploy, fix). They need **complete observability** - every action, every output, every decision must be: + +1. **Captured** - Full logs, not truncated +2. **Accessible** - Via command, event, or UI +3. **Contextual** - Tied to the specific sentinel run +4. **Controllable** - Can dial up/down verbosity + +**The ideal**: When a sentinel runs `npm run build`, you can see EVERY line of compilation output in real-time, click on any error to jump to the file, and review the complete log later. + +--- + +## Current State Analysis + +### What Exists + +| Component | Status | Gap | +|-----------|--------|-----| +| `SentinelExecutionLog` | Streaming events via `sentinel:{handle}:{type}` | Evidence output truncated to ~200 chars | +| `SentinelWorkspace` | Creates `.sentinel-workspaces/{handle}` dirs | No logging infrastructure in workspace | +| `BuildSentinel` | Captures stdout/stderr | Only stores `error.stdout + error.stderr` truncated | +| `Logger` | Per-component routing, Rust backend | No per-sentinel logging support | +| `LogLevelRegistry` | Runtime level control | No sentinel-aware controls | +| `logs/*` commands | list, read, search, config, stats | System logs only, not sentinel logs | + +### The Core Problem + +Sentinels run compilations that produce **massive output** (thousands of lines). Currently: + +```typescript +// BuildSentinel.ts:201-213 +try { + output = execSync(this.config.command, { ... }); +} catch (error: any) { + output = error.stdout?.toString() || ''; + output += error.stderr?.toString() || ''; // This could be HUGE +} + +// Later, in SentinelExecutionLog.ts:530 +if (action.evidence.output && action.evidence.output.length < 200) { + // Show inline +} else if (action.evidence.output) { + const firstLines = action.evidence.output.split('\n').slice(0, 3)... + // TRUNCATED - full output LOST +} +``` + +**The output is captured but then truncated.** We need to: +1. Save full output to files +2. Stream it in real-time +3. Reference it from execution logs + +--- + +## Architecture Design + +### Per-Sentinel Log Directory + +Each sentinel gets a dedicated logging directory inside its workspace: + +``` +.sentinel-workspaces/ +โ””โ”€โ”€ {handle}/ # e.g., abc12345 + โ”œโ”€โ”€ .git/ # Worktree checkout + โ”œโ”€โ”€ logs/ # NEW: Sentinel-specific logs + โ”‚ โ”œโ”€โ”€ sentinel.log # High-level sentinel actions + โ”‚ โ”œโ”€โ”€ build-1.log # Full output from build attempt 1 + โ”‚ โ”œโ”€โ”€ build-2.log # Full output from build attempt 2 + โ”‚ โ”œโ”€โ”€ build-3.log # Full output from build attempt 3 + โ”‚ โ”œโ”€โ”€ llm-requests.log # LLM queries and responses + โ”‚ โ””โ”€โ”€ evidence/ # Structured evidence files + โ”‚ โ”œโ”€โ”€ error-parse-1.json + โ”‚ โ””โ”€โ”€ fix-applied-2.json + โ””โ”€โ”€ src/... # Working directory +``` + +**Key insight**: Logs live WITH the sentinel's workspace. When the workspace is cleaned up, logs can optionally be archived or deleted. + +### Log Streaming Architecture + +``` +BuildSentinel + โ”‚ + โ”œโ”€โ”€ spawn('npm run build') + โ”‚ โ”‚ + โ”‚ โ”œโ”€โ”€ stdout.on('data') โ”€โ”€โ–บ SentinelLogWriter.write(handle, 'build-1', chunk) + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ”œโ”€โ”€ Append to logs/build-1.log + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ””โ”€โ”€ Events.emit('sentinel:{handle}:stdout', chunk) + โ”‚ โ”‚ โ”‚ + โ”‚ โ”‚ โ””โ”€โ”€ UI subscribes for real-time display + โ”‚ โ”‚ + โ”‚ โ””โ”€โ”€ stderr.on('data') โ”€โ”€โ–บ (same path) + โ”‚ + โ””โ”€โ”€ on('close') โ”€โ”€โ–บ SentinelLogWriter.finalize(handle, 'build-1', exitCode) +``` + +### New Components + +#### 1. `SentinelLogWriter` - Per-Sentinel Logging + +```typescript +// system/sentinel/SentinelLogWriter.ts + +export class SentinelLogWriter { + private handle: string; + private workspaceDir: string; + private logDir: string; + private streams: Map; + private eventEmitter?: SentinelEventEmitter; + + constructor(handle: string, workspaceDir: string, eventEmitter?: SentinelEventEmitter) { + this.handle = handle; + this.workspaceDir = workspaceDir; + this.logDir = path.join(workspaceDir, 'logs'); + fs.mkdirSync(this.logDir, { recursive: true }); + this.streams = new Map(); + this.eventEmitter = eventEmitter; + } + + /** + * Write a chunk to a named log stream (e.g., 'build-1', 'llm-requests') + * Streams to file AND emits event for real-time UI + */ + write(streamName: string, chunk: string | Buffer): void { + // Get or create stream + let stream = this.streams.get(streamName); + if (!stream) { + const logPath = path.join(this.logDir, `${streamName}.log`); + stream = fs.createWriteStream(logPath, { flags: 'a' }); + this.streams.set(streamName, stream); + } + + // Write to file + stream.write(chunk); + + // Emit for real-time streaming + if (this.eventEmitter) { + this.eventEmitter({ + type: 'log', + handle: this.handle, + timestamp: new Date().toISOString(), + payload: { + stream: streamName, + chunk: chunk.toString(), + cumulative: false // This is a delta, not full content + } + }); + } + } + + /** + * Get full content of a log stream + */ + async read(streamName: string): Promise { + const logPath = path.join(this.logDir, `${streamName}.log`); + if (!fs.existsSync(logPath)) return null; + return fs.readFileSync(logPath, 'utf-8'); + } + + /** + * List all log streams for this sentinel + */ + list(): string[] { + if (!fs.existsSync(this.logDir)) return []; + return fs.readdirSync(this.logDir) + .filter(f => f.endsWith('.log')) + .map(f => f.replace('.log', '')); + } + + /** + * Get log directory path for external access + */ + get logsPath(): string { + return this.logDir; + } + + /** + * Close all streams + */ + close(): void { + for (const stream of this.streams.values()) { + stream.end(); + } + this.streams.clear(); + } +} +``` + +#### 2. Enhanced `SentinelExecutionLog` - Evidence File References + +```typescript +// Enhance SentinelAction.evidence to reference full logs +export interface SentinelAction { + // ... existing fields ... + evidence?: { + output?: string; // Short preview (~200 chars) + outputFile?: string; // NEW: Full path to complete output file + outputStream?: string; // NEW: Stream name for log retrieval + // ... other fields ... + }; +} +``` + +#### 3. New Events for Log Streaming + +```typescript +// sentinel:{handle}:log - Real-time log chunks +Events.subscribe('sentinel:abc123:log', (event) => { + // event.payload = { stream: 'build-1', chunk: '...', cumulative: false } + appendToUI(event.payload.chunk); +}); + +// sentinel:{handle}:log-complete - Stream finished +Events.subscribe('sentinel:abc123:log-complete', (event) => { + // event.payload = { stream: 'build-1', exitCode: 0, totalLines: 1523 } + showCompletionStatus(event.payload); +}); +``` + +#### 4. New Commands + +| Command | Purpose | +|---------|---------| +| `sentinel/logs/list` | List log streams for a sentinel handle | +| `sentinel/logs/read` | Read full content of a log stream | +| `sentinel/logs/tail` | Stream real-time output (subscribe to events) | +| `sentinel/logs/search` | Search within sentinel logs | + +--- + +## Implementation Phases + +### Phase 1: Per-Sentinel Log Directory (Foundation) + +**Goal**: Every sentinel run creates a dedicated log directory with full output capture. + +**Files to modify**: +- `system/sentinel/SentinelWorkspace.ts` - Create logs/ directory on workspace init +- `system/sentinel/BuildSentinel.ts` - Use `spawn()` instead of `execSync()` for streaming +- `system/sentinel/SentinelExecutionLog.ts` - Add `outputFile` to evidence + +**Changes**: + +```typescript +// SentinelWorkspace.ts - Add logs directory creation +private async initialize(): Promise { + // ... existing code ... + + // Create logs directory + const logsDir = path.join(this.info.workingDir, 'logs'); + fs.mkdirSync(logsDir, { recursive: true }); + this.info.logsDir = logsDir; // NEW field +} +``` + +```typescript +// BuildSentinel.ts - Use spawn for streaming +private async build(attemptNumber: number): Promise { + const startTime = Date.now(); + const logWriter = new SentinelLogWriter(this.handle, this.workingDir); + const streamName = `build-${attemptNumber}`; + + return new Promise((resolve) => { + const proc = spawn('sh', ['-c', this.config.command], { + cwd: this.config.workingDir, + stdio: ['pipe', 'pipe', 'pipe'] + }); + + proc.stdout.on('data', (chunk) => { + logWriter.write(streamName, chunk); + }); + + proc.stderr.on('data', (chunk) => { + logWriter.write(streamName, chunk); + }); + + proc.on('close', (exitCode) => { + const fullOutput = await logWriter.read(streamName); + const errors = exitCode === 0 ? [] : this.parseErrors(fullOutput || ''); + + resolve({ + attemptNumber, + command: this.config.command, + success: exitCode === 0, + errors, + durationMs: Date.now() - startTime, + // NEW: Reference to full output + outputFile: path.join(logWriter.logsPath, `${streamName}.log`) + }); + }); + }); +} +``` + +### Phase 2: Real-Time Event Streaming + +**Goal**: UI can subscribe to sentinel log events and display real-time compilation output. + +**Files to create/modify**: +- `system/sentinel/SentinelLogWriter.ts` - New class +- `system/sentinel/SentinelExecutionLog.ts` - Add 'log' event type +- `commands/sentinel/run/server/SentinelRunServerCommand.ts` - Wire up events + +**New events**: +``` +sentinel:{handle}:log - Real-time log chunks +sentinel:{handle}:log-complete - Stream finished +``` + +### Phase 3: Log Commands + +**Goal**: CLI access to sentinel logs. + +**New commands**: +- `commands/sentinel/logs/list/` - List log streams +- `commands/sentinel/logs/read/` - Read full log content +- `commands/sentinel/logs/tail/` - Stream real-time (returns subscription handle) + +**Examples**: +```bash +# List logs for a sentinel +./jtag sentinel/logs/list --handle=abc123 +# Returns: ["build-1", "build-2", "llm-requests", "sentinel"] + +# Read full build output +./jtag sentinel/logs/read --handle=abc123 --stream=build-1 +# Returns: Full compilation output (could be thousands of lines) + +# Tail in real-time (for running sentinels) +./jtag sentinel/logs/tail --handle=abc123 --stream=build-1 +# Streams output until sentinel completes +``` + +### Phase 4: Chat Integration (Inline Clickable Logs) + +**Goal**: When a sentinel is invoked from chat, show inline log references that expand/click to full logs. + +**Concept**: +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Helper AI 2:34pm โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Running build sentinel to fix the type errors... โ”‚ +โ”‚ โ”‚ +โ”‚ โ”Œโ”€ sentinel:abc123 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ +โ”‚ โ”‚ โ–ธ Build attempt 1/3 [FAILED] ๐Ÿ“‹ view logs โ”‚ โ”‚ +โ”‚ โ”‚ error TS2345: Argument of type 'string' is not... โ”‚ โ”‚ +โ”‚ โ”‚ โ–ธ Applying fix... โ”‚ โ”‚ +โ”‚ โ”‚ โ–ธ Build attempt 2/3 [SUCCESS] ๐Ÿ“‹ view logs โ”‚ โ”‚ +โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ +โ”‚ โ”‚ +โ”‚ Fixed! The build now passes. Ready for review. โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +**Implementation**: +- Tool calls from PersonaUser emit `tool:result` events +- If result contains `sentinelHandle`, render as expandable log widget +- Clicking "view logs" opens log-viewer widget with sentinel logs + +### Phase 5: Verbosity Controls + +**Goal**: Ability to dramatically reduce logging workload when not needed. + +**Approaches**: + +1. **Global sentinel log level**: +```bash +./jtag sentinel/config --logLevel=minimal # Only errors and summaries +./jtag sentinel/config --logLevel=normal # Default (streaming, but not persisted in full) +./jtag sentinel/config --logLevel=verbose # Full capture + persistence +./jtag sentinel/config --logLevel=off # No logging (fastest) +``` + +2. **Per-sentinel override**: +```bash +./jtag sentinel/run --type=build --logLevel=verbose # Override for this run +``` + +3. **Retention controls**: +```bash +./jtag sentinel/logs/cleanup --olderThan=7d # Delete logs older than 7 days +./jtag sentinel/logs/cleanup --handle=abc123 # Delete logs for specific run +./jtag sentinel/config --autoCleanup=24h # Auto-delete after 24 hours +``` + +4. **Output filtering**: +```bash +./jtag sentinel/run --type=build --logFilter=errors # Only capture error lines +``` + +--- + +## Data Structures + +### SentinelLogConfig (New) + +```typescript +export interface SentinelLogConfig { + /** Global log level for sentinel execution */ + level: 'off' | 'minimal' | 'normal' | 'verbose'; + + /** How long to retain logs (ms, 0 = forever) */ + retentionMs: number; + + /** Max log file size before rotation (bytes, 0 = unlimited) */ + maxFileSizeBytes: number; + + /** Stream logs via events (for real-time UI) */ + streamEvents: boolean; + + /** Persist full output to files */ + persistToFiles: boolean; + + /** Filter patterns (only log lines matching these) */ + includePatterns?: RegExp[]; + + /** Filter patterns (exclude lines matching these) */ + excludePatterns?: RegExp[]; +} + +// Defaults +const DEFAULT_LOG_CONFIG: SentinelLogConfig = { + level: 'normal', + retentionMs: 7 * 24 * 60 * 60 * 1000, // 7 days + maxFileSizeBytes: 10 * 1024 * 1024, // 10MB + streamEvents: true, + persistToFiles: true, +}; +``` + +### Enhanced WorkspaceInfo + +```typescript +export interface WorkspaceInfo { + workingDir: string; + branch: string; + originalBranch: string; + isWorktree: boolean; + worktreePath?: string; + logsDir: string; // NEW: Path to logs directory + logWriter?: SentinelLogWriter; // NEW: Active log writer +} +``` + +### Enhanced BuildAttempt + +```typescript +export interface BuildAttempt { + attemptNumber: number; + command: string; + success: boolean; + errors: BuildError[]; + fixApplied?: string; + durationMs: number; + + // NEW: Log references + outputFile?: string; // Path to full output log + outputStream?: string; // Stream name for retrieval + outputLines?: number; // Total line count + outputBytes?: number; // Total byte size +} +``` + +--- + +## Event Specifications + +### sentinel:{handle}:log + +Emitted for each chunk of output from sentinel processes. + +```typescript +{ + type: 'log', + handle: 'abc123', + timestamp: '2024-02-09T10:30:15.123Z', + payload: { + stream: 'build-1', // Which log stream + chunk: 'Compiling...\n', // The actual content + cumulative: false, // This is a delta + sourceType: 'stdout' | 'stderr', + lineNumber?: number, // Starting line number of this chunk + } +} +``` + +### sentinel:{handle}:log-complete + +Emitted when a log stream is finalized. + +```typescript +{ + type: 'log-complete', + handle: 'abc123', + timestamp: '2024-02-09T10:30:45.456Z', + payload: { + stream: 'build-1', + exitCode: 1, + totalLines: 1523, + totalBytes: 89234, + durationMs: 30333, + logPath: '/path/to/logs/build-1.log' + } +} +``` + +--- + +## Integration Points + +### 1. PersonaUser Tool Execution + +When a persona calls a tool that spawns a sentinel: + +```typescript +// PersonaToolExecutor.ts +async executeTool(tool: string, params: any): Promise { + if (tool === 'sentinel/run') { + // Add event listener for real-time updates + const handle = params.handle || generateHandle(); + const unsubscribe = await subscribeSentinelEvents(handle, (event) => { + if (event.type === 'log') { + // Forward to chat as streaming message update + this.emitToolProgress(tool, event); + } + }); + + try { + const result = await Commands.execute(tool, { ...params, handle }); + return result; + } finally { + unsubscribe(); + } + } + // ... other tools +} +``` + +### 2. Chat Widget Log Display + +New component for inline log display: + +```typescript +// widgets/sentinel-log-inline/SentinelLogInlineWidget.ts +export class SentinelLogInlineWidget extends LitElement { + @property() handle: string; + @property() stream: string; + @property() collapsed: boolean = true; + @property() lines: string[] = []; + + private unsubscribe?: () => void; + + connectedCallback() { + super.connectedCallback(); + // Subscribe to real-time log events + this.subscribeToLogs(); + } + + private async subscribeToLogs() { + this.unsubscribe = await Events.subscribe( + `sentinel:${this.handle}:log`, + (event) => { + if (event.payload.stream === this.stream) { + this.lines = [...this.lines, ...event.payload.chunk.split('\n')]; + this.requestUpdate(); + } + } + ); + } + + render() { + return html` +
+
+ ${this.collapsed ? 'โ–ธ' : 'โ–พ'} + ${this.stream} + ${this.lines.length} lines + +
+ ${!this.collapsed ? html` +
${this.lines.slice(-50).join('\n')}
+ ` : ''} +
+ `; + } +} +``` + +### 3. Log Viewer Widget Enhancement + +Enhance existing log-viewer to support sentinel logs: + +```typescript +// widgets/log-viewer/LogViewerWidget.ts +// Add mode for sentinel logs +@property() mode: 'system' | 'sentinel' = 'system'; +@property() sentinelHandle?: string; +@property() sentinelStream?: string; + +// When mode='sentinel', load from sentinel workspace +private async loadSentinelLogs() { + const result = await Commands.execute('sentinel/logs/read', { + handle: this.sentinelHandle, + stream: this.sentinelStream + }); + this.content = result.content; +} +``` + +--- + +## Testing Strategy + +### Unit Tests + +```typescript +// tests/unit/SentinelLogWriter.test.ts +describe('SentinelLogWriter', () => { + it('creates log directory on construction'); + it('writes chunks to named streams'); + it('emits events for real-time streaming'); + it('reads full content from streams'); + it('lists all log streams'); + it('handles concurrent writes'); +}); +``` + +### Integration Tests + +```typescript +// tests/integration/sentinel-logging.test.ts +describe('Sentinel Logging Integration', () => { + it('captures full build output to file'); + it('streams output via events in real-time'); + it('execution log references output files'); + it('logs survive sentinel completion'); + it('logs are cleaned up with workspace'); +}); +``` + +### E2E Tests + +```bash +# Run a build sentinel and verify logs +./jtag sentinel/run --type=build --command="npm run build:ts" --async=false + +# Check logs were created +./jtag sentinel/logs/list --handle=$HANDLE +# Expected: ["build-1", "sentinel"] + +# Read full output +./jtag sentinel/logs/read --handle=$HANDLE --stream=build-1 | wc -l +# Expected: Thousands of lines (not truncated) +``` + +--- + +## Migration Path + +1. **Phase 1**: Implement SentinelLogWriter + workspace logging (no breaking changes) +2. **Phase 2**: Add event streaming (additive, no breaking changes) +3. **Phase 3**: Add log commands (additive) +4. **Phase 4**: Chat integration (UI enhancement) +5. **Phase 5**: Verbosity controls (configuration) + +Each phase is independently deployable and testable. + +--- + +## Open Questions + +1. **Log retention**: How long should sentinel logs be kept by default? + - Proposal: 7 days, configurable via `sentinel/config` + +2. **Log size limits**: Should we cap individual log files? + - Proposal: 10MB per stream, rotate if exceeded + +3. **Archive integration**: Should completed sentinel logs be archived? + - Proposal: Optional archive command, not automatic + +4. **Persona log separation**: Should each persona's sentinel runs have separate log directories? + - Proposal: Yes, under persona home dir: `~/.continuum/personas/{id}/sentinel-logs/` + +--- + +## Summary + +This plan enables complete observability for sentinel execution: + +- **Full capture**: No more truncated build output +- **Real-time streaming**: UI shows compilation as it happens +- **Accessible**: CLI commands + event subscriptions +- **Contextual**: Logs tied to specific sentinel runs +- **Controllable**: Verbosity and retention settings + +The architecture builds on existing infrastructure (SentinelExecutionLog, SentinelWorkspace, Logger) while adding the missing pieces for complete observability. diff --git a/src/debug/jtag/generated-command-schemas.json b/src/debug/jtag/generated-command-schemas.json index 283a41d46..a5accd50a 100644 --- a/src/debug/jtag/generated-command-schemas.json +++ b/src/debug/jtag/generated-command-schemas.json @@ -1,5 +1,5 @@ { - "generated": "2026-02-13T05:10:50.368Z", + "generated": "2026-02-13T17:53:34.018Z", "version": "1.0.0", "commands": [ { @@ -1805,6 +1805,64 @@ } } }, + { + "name": "sentinel/logs/tail", + "description": "Sentinel Logs Tail Command - Types\n *\n * Get the last N lines of a log stream (like Unix tail).", + "params": { + "handle": { + "type": "string", + "required": true, + "description": "handle parameter" + }, + "stream": { + "type": "string", + "required": true, + "description": "stream parameter" + }, + "lines": { + "type": "number", + "required": false, + "description": "lines parameter" + } + } + }, + { + "name": "sentinel/logs/read", + "description": "Sentinel Logs Read Command - Types\n *\n * Read a log stream for a sentinel.", + "params": { + "handle": { + "type": "string", + "required": true, + "description": "handle parameter" + }, + "stream": { + "type": "string", + "required": true, + "description": "stream parameter" + }, + "offset": { + "type": "number", + "required": false, + "description": "offset parameter" + }, + "limit": { + "type": "number", + "required": false, + "description": "limit parameter" + } + } + }, + { + "name": "sentinel/logs/list", + "description": "Sentinel Logs List Command - Types\n *\n * List available log streams for a sentinel.", + "params": { + "handle": { + "type": "string", + "required": true, + "description": "handle parameter" + } + } + }, { "name": "sentinel/load", "description": "Sentinel Load Command - Types\n *\n * Load and optionally run saved sentinel definitions from database.", @@ -2853,6 +2911,49 @@ } } }, + { + "name": "logging/status", + "description": "Logging Status Command - Shared Types\n *\n * Show current logging configuration for all personas or a specific persona", + "params": { + "persona": { + "type": "string", + "required": false, + "description": "persona parameter" + } + } + }, + { + "name": "logging/enable", + "description": "Logging Enable Command - Shared Types\n *\n * Enable logging for a persona. Persists to .continuum/logging.json", + "params": { + "persona": { + "type": "string", + "required": true, + "description": "persona parameter" + }, + "category": { + "type": "string", + "required": false, + "description": "category parameter" + } + } + }, + { + "name": "logging/disable", + "description": "Logging Disable Command - Shared Types\n *\n * Disable logging for a persona. Persists to .continuum/logging.json", + "params": { + "persona": { + "type": "string", + "required": true, + "description": "persona parameter" + }, + "category": { + "type": "string", + "required": false, + "description": "category parameter" + } + } + }, { "name": "list", "description": "List Command Types - Command Discovery Interface\n * \n * Provides strongly-typed interface for discovering available commands from the system.\n * Essential command that all JTAG systems must implement for client discovery.", diff --git a/src/debug/jtag/generator/specs/logging-disable.json b/src/debug/jtag/generator/specs/logging-disable.json new file mode 100644 index 000000000..d6d44fc87 --- /dev/null +++ b/src/debug/jtag/generator/specs/logging-disable.json @@ -0,0 +1,53 @@ +{ + "name": "logging/disable", + "description": "Disable logging for a persona. Persists to .continuum/logging.json", + "params": [ + { + "name": "persona", + "type": "string", + "optional": false, + "description": "Persona uniqueId to disable logging for (e.g., 'helper', 'codereview')" + }, + { + "name": "category", + "type": "string", + "optional": true, + "description": "Specific category to disable. If not specified, disables all logging for the persona" + } + ], + "results": [ + { + "name": "persona", + "type": "string", + "description": "The persona that was disabled" + }, + { + "name": "enabled", + "type": "boolean", + "description": "Whether any logging remains enabled for this persona" + }, + { + "name": "categories", + "type": "string[]", + "description": "Categories still enabled (empty if all disabled)" + }, + { + "name": "message", + "type": "string", + "description": "Human-readable status message" + } + ], + "examples": [ + { + "description": "Disable all logging for helper persona", + "command": "./jtag logging/disable --persona=helper", + "expectedResult": "{ persona: 'helper', enabled: false, categories: [], message: 'Disabled all logging for helper' }" + }, + { + "description": "Disable only training logs", + "command": "./jtag logging/disable --persona=helper --category=training", + "expectedResult": "{ persona: 'helper', enabled: true, categories: ['cognition'], message: 'Disabled training logging for helper' }" + } + ], + "accessLevel": "ai-safe" +} diff --git a/src/debug/jtag/generator/specs/logging-enable.json b/src/debug/jtag/generator/specs/logging-enable.json new file mode 100644 index 000000000..fc987646c --- /dev/null +++ b/src/debug/jtag/generator/specs/logging-enable.json @@ -0,0 +1,48 @@ +{ + "name": "logging/enable", + "description": "Enable logging for a persona. Persists to .continuum/logging.json", + "params": [ + { + "name": "persona", + "type": "string", + "optional": false, + "description": "Persona uniqueId to enable logging for (e.g., 'helper', 'codereview')" + }, + { + "name": "category", + "type": "string", + "optional": true, + "description": "Specific category to enable (e.g., 'cognition', 'hippocampus'). If not specified, enables all categories" + } + ], + "results": [ + { + "name": "persona", + "type": "string", + "description": "The persona that was enabled" + }, + { + "name": "categories", + "type": "string[]", + "description": "Categories now enabled for this persona" + }, + { + "name": "message", + "type": "string", + "description": "Human-readable status message" + } + ], + "examples": [ + { + "description": "Enable all logging for helper persona", + "command": "./jtag logging/enable --persona=helper", + "expectedResult": "{ persona: 'helper', categories: ['*'], message: 'Enabled all logging for helper' }" + }, + { + "description": "Enable only cognition logs", + "command": "./jtag logging/enable --persona=helper --category=cognition", + "expectedResult": "{ persona: 'helper', categories: ['cognition'], message: 'Enabled cognition logging for helper' }" + } + ], + "accessLevel": "ai-safe" +} diff --git a/src/debug/jtag/generator/specs/logging-status.json b/src/debug/jtag/generator/specs/logging-status.json new file mode 100644 index 000000000..16b22abcb --- /dev/null +++ b/src/debug/jtag/generator/specs/logging-status.json @@ -0,0 +1,52 @@ +{ + "name": "logging/status", + "description": "Show current logging configuration for all personas or a specific persona", + "params": [ + { + "name": "persona", + "type": "string", + "optional": true, + "description": "Specific persona to show status for. If not specified, shows all personas" + } + ], + "results": [ + { + "name": "personas", + "type": "object[]", + "description": "Array of persona logging statuses with { persona, enabled, categories }" + }, + { + "name": "systemEnabled", + "type": "boolean", + "description": "Whether system logging is enabled" + }, + { + "name": "defaultEnabled", + "type": "boolean", + "description": "Default enabled state for unconfigured personas" + }, + { + "name": "availableCategories", + "type": "string[]", + "description": "List of valid category names" + }, + { + "name": "summary", + "type": "string", + "description": "Human-readable summary of logging state" + } + ], + "examples": [ + { + "description": "Show all logging status", + "command": "./jtag logging/status", + "expectedResult": "{ personas: [...], systemEnabled: true, defaultEnabled: false, summary: '2/11 personas logging enabled' }" + }, + { + "description": "Show status for specific persona", + "command": "./jtag logging/status --persona=helper", + "expectedResult": "{ personas: [{ persona: 'helper', enabled: true, categories: ['cognition'] }], summary: 'helper: ON (cognition)' }" + } + ], + "accessLevel": "ai-safe" +} diff --git a/src/debug/jtag/package-lock.json b/src/debug/jtag/package-lock.json index 482f5bf98..f4331a579 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.7858", + "version": "1.0.7861", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@continuum/jtag", - "version": "1.0.7858", + "version": "1.0.7861", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/debug/jtag/package.json b/src/debug/jtag/package.json index 016b15efb..687125f9f 100644 --- a/src/debug/jtag/package.json +++ b/src/debug/jtag/package.json @@ -1,6 +1,6 @@ { "name": "@continuum/jtag", - "version": "1.0.7858", + "version": "1.0.7861", "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 0a89e0774..4b416a26e 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 17 daemons and 237 commands and 3 adapters. + * Contains 17 daemons and 243 commands and 3 adapters. * Generated by scripts/generate-structure.ts - DO NOT EDIT MANUALLY */ @@ -177,6 +177,9 @@ import { WebSearchServerCommand } from './../commands/interface/web/search/serve import { InterfaceWebmcpCallServerCommand } from './../commands/interface/webmcp/call/server/InterfaceWebmcpCallServerCommand'; import { InterfaceWebmcpDiscoverServerCommand } from './../commands/interface/webmcp/discover/server/InterfaceWebmcpDiscoverServerCommand'; import { ListServerCommand } from './../commands/list/server/ListServerCommand'; +import { LoggingDisableServerCommand } from './../commands/logging/disable/server/LoggingDisableServerCommand'; +import { LoggingEnableServerCommand } from './../commands/logging/enable/server/LoggingEnableServerCommand'; +import { LoggingStatusServerCommand } from './../commands/logging/status/server/LoggingStatusServerCommand'; import { LogsConfigServerCommand } from './../commands/logs/config/server/LogsConfigServerCommand'; import { LogsListServerCommand } from './../commands/logs/list/server/LogsListServerCommand'; import { LogsReadServerCommand } from './../commands/logs/read/server/LogsReadServerCommand'; @@ -204,6 +207,9 @@ import { SearchVectorServerCommand } from './../commands/search/vector/server/Se import { SecuritySetupServerCommand } from './../commands/security/setup/server/SecuritySetupServerCommand'; import { SentinelListServerCommand } from './../commands/sentinel/list/server/SentinelListServerCommand'; import { SentinelLoadServerCommand } from './../commands/sentinel/load/server/SentinelLoadServerCommand'; +import { SentinelLogsListServerCommand } from './../commands/sentinel/logs/list/server/SentinelLogsListServerCommand'; +import { SentinelLogsReadServerCommand } from './../commands/sentinel/logs/read/server/SentinelLogsReadServerCommand'; +import { SentinelLogsTailServerCommand } from './../commands/sentinel/logs/tail/server/SentinelLogsTailServerCommand'; import { SentinelRunServerCommand } from './../commands/sentinel/run/server/SentinelRunServerCommand'; import { SentinelSaveServerCommand } from './../commands/sentinel/save/server/SentinelSaveServerCommand'; import { SentinelStatusServerCommand } from './../commands/sentinel/status/server/SentinelStatusServerCommand'; @@ -1125,6 +1131,21 @@ export const SERVER_COMMANDS: CommandEntry[] = [ className: 'ListServerCommand', commandClass: ListServerCommand }, +{ + name: 'logging/disable', + className: 'LoggingDisableServerCommand', + commandClass: LoggingDisableServerCommand + }, +{ + name: 'logging/enable', + className: 'LoggingEnableServerCommand', + commandClass: LoggingEnableServerCommand + }, +{ + name: 'logging/status', + className: 'LoggingStatusServerCommand', + commandClass: LoggingStatusServerCommand + }, { name: 'logs/config', className: 'LogsConfigServerCommand', @@ -1260,6 +1281,21 @@ export const SERVER_COMMANDS: CommandEntry[] = [ className: 'SentinelLoadServerCommand', commandClass: SentinelLoadServerCommand }, +{ + name: 'sentinel/logs/list', + className: 'SentinelLogsListServerCommand', + commandClass: SentinelLogsListServerCommand + }, +{ + name: 'sentinel/logs/read', + className: 'SentinelLogsReadServerCommand', + commandClass: SentinelLogsReadServerCommand + }, +{ + name: 'sentinel/logs/tail', + className: 'SentinelLogsTailServerCommand', + commandClass: SentinelLogsTailServerCommand + }, { name: 'sentinel/run', className: 'SentinelRunServerCommand', diff --git a/src/debug/jtag/shared/generated-command-constants.ts b/src/debug/jtag/shared/generated-command-constants.ts index 55f613298..762a80b0c 100644 --- a/src/debug/jtag/shared/generated-command-constants.ts +++ b/src/debug/jtag/shared/generated-command-constants.ts @@ -178,6 +178,9 @@ export const COMMANDS = { INTERFACE_WEBMCP_CALL: 'interface/webmcp/call', INTERFACE_WEBMCP_DISCOVER: 'interface/webmcp/discover', LIST: 'list', + LOGGING_DISABLE: 'logging/disable', + LOGGING_ENABLE: 'logging/enable', + LOGGING_STATUS: 'logging/status', LOGS_CONFIG: 'logs/config', LOGS_LIST: 'logs/list', LOGS_READ: 'logs/read', @@ -205,6 +208,9 @@ export const COMMANDS = { SECURITY_SETUP: 'security/setup', SENTINEL_LIST: 'sentinel/list', SENTINEL_LOAD: 'sentinel/load', + SENTINEL_LOGS_LIST: 'sentinel/logs/list', + SENTINEL_LOGS_READ: 'sentinel/logs/read', + SENTINEL_LOGS_TAIL: 'sentinel/logs/tail', SENTINEL_RUN: 'sentinel/run', SENTINEL_SAVE: 'sentinel/save', SENTINEL_STATUS: 'sentinel/status', diff --git a/src/debug/jtag/shared/version.ts b/src/debug/jtag/shared/version.ts index 38eecc9cf..33645affd 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.7858'; +export const VERSION = '1.0.7861'; export const PACKAGE_NAME = '@continuum/jtag'; diff --git a/src/debug/jtag/system/sentinel/SentinelLogWriter.ts b/src/debug/jtag/system/sentinel/SentinelLogWriter.ts new file mode 100644 index 000000000..0339e32b7 --- /dev/null +++ b/src/debug/jtag/system/sentinel/SentinelLogWriter.ts @@ -0,0 +1,203 @@ +/** + * SentinelLogWriter - Non-blocking log writer for sentinel execution + * + * Writes FULL output (not truncated) to per-sentinel log directories. + * Uses async file operations - NEVER blocks the main thread. + * + * Directory structure: + * .sentinel-workspaces/{handle}/logs/ + * โ”œโ”€โ”€ execution.log # High-level actions + * โ”œโ”€โ”€ build-1.log # Full output from build attempt 1 + * โ”œโ”€โ”€ build-2.log # Full output from build attempt 2 + * โ”œโ”€โ”€ llm-requests.log # LLM queries and responses + * โ””โ”€โ”€ stderr.log # All stderr output + * + * Streaming: Emits events as logs are written for real-time UI. + * sentinel:{handle}:log - Each log chunk + */ + +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { Events } from '@system/core/shared/Events'; + +export interface LogWriterConfig { + /** Unique sentinel handle */ + handle: string; + + /** Base directory for sentinel workspaces */ + baseDir?: string; + + /** Whether to emit events for streaming */ + emitEvents?: boolean; +} + +export interface LogChunk { + stream: string; + chunk: string; + timestamp: string; + sourceType: 'stdout' | 'stderr' | 'info' | 'error'; +} + +/** + * Non-blocking log writer for sentinel execution. + * All writes are async - NEVER blocks caller. + */ +export class SentinelLogWriter { + private handle: string; + private logsDir: string; + private emitEvents: boolean; + private writeQueue: Map> = new Map(); + private buildCounter: number = 0; + + private constructor(config: LogWriterConfig) { + this.handle = config.handle; + const baseDir = config.baseDir ?? '.sentinel-workspaces'; + this.logsDir = path.join(baseDir, config.handle, 'logs'); + this.emitEvents = config.emitEvents ?? true; + } + + /** + * Create and initialize a log writer. + * Creates the logs directory if it doesn't exist. + */ + static async create(config: LogWriterConfig): Promise { + const writer = new SentinelLogWriter(config); + await fs.mkdir(writer.logsDir, { recursive: true }); + return writer; + } + + /** + * Get the logs directory path. + */ + get logDirectory(): string { + return this.logsDir; + } + + /** + * Write to the execution log (high-level actions). + * NON-BLOCKING - returns immediately. + */ + async writeExecution(message: string): Promise { + await this.appendToStream('execution', message, 'info'); + } + + /** + * Start a new build log and return its stream name. + * Each build attempt gets a separate log file. + */ + startBuildLog(): string { + this.buildCounter++; + return `build-${this.buildCounter}`; + } + + /** + * Write build output (stdout/stderr) to the current build log. + * NON-BLOCKING - returns immediately. + */ + async writeBuildOutput(stream: string, output: string, sourceType: 'stdout' | 'stderr' = 'stdout'): Promise { + await this.appendToStream(stream, output, sourceType); + } + + /** + * Write LLM request/response to the LLM log. + * NON-BLOCKING - returns immediately. + */ + async writeLlmRequest(prompt: string, response: string): Promise { + const timestamp = new Date().toISOString(); + const entry = `\n${'='.repeat(80)}\n[${timestamp}] LLM REQUEST\n${'='.repeat(80)}\n${prompt}\n\n${'='.repeat(80)}\n[${timestamp}] LLM RESPONSE\n${'='.repeat(80)}\n${response}\n`; + await this.appendToStream('llm-requests', entry, 'info'); + } + + /** + * Write error output to stderr log. + * NON-BLOCKING - returns immediately. + */ + async writeError(error: string): Promise { + await this.appendToStream('stderr', error, 'error'); + } + + /** + * Get the full path to a log file. + */ + getLogPath(stream: string): string { + return path.join(this.logsDir, `${stream}.log`); + } + + /** + * Read a log file's contents. + */ + async readLog(stream: string): Promise { + const logPath = this.getLogPath(stream); + try { + return await fs.readFile(logPath, 'utf-8'); + } catch { + return ''; + } + } + + /** + * List all log files for this sentinel. + */ + async listLogs(): Promise { + try { + const files = await fs.readdir(this.logsDir); + return files.filter(f => f.endsWith('.log')).map(f => f.replace('.log', '')); + } catch { + return []; + } + } + + /** + * Core append method - NON-BLOCKING via async queue. + * Ensures writes to the same file are serialized (no interleaving). + */ + private async appendToStream(stream: string, content: string, sourceType: 'stdout' | 'stderr' | 'info' | 'error'): Promise { + const logPath = this.getLogPath(stream); + const timestamp = new Date().toISOString(); + + // Emit event for real-time streaming (non-blocking) + if (this.emitEvents) { + const event: LogChunk = { + stream, + chunk: content, + timestamp, + sourceType, + }; + // Fire-and-forget - don't await event emission + Events.emit(`sentinel:${this.handle}:log`, event).catch(() => {}); + } + + // Queue writes to same file to prevent interleaving + const existingWrite = this.writeQueue.get(stream) ?? Promise.resolve(); + const newWrite = existingWrite.then(async () => { + try { + await fs.appendFile(logPath, content); + } catch (e) { + // Log write failure but don't crash + console.error(`[SentinelLogWriter] Failed to write to ${logPath}:`, e); + } + }); + + this.writeQueue.set(stream, newWrite); + + // Don't await - return immediately for non-blocking behavior + // The write will complete in the background + } + + /** + * Wait for all pending writes to complete. + * Call this before sentinel completion to ensure all logs are flushed. + */ + async flush(): Promise { + const pending = Array.from(this.writeQueue.values()); + await Promise.all(pending); + this.writeQueue.clear(); + } +} + +/** + * Factory function for creating log writers. + */ +export async function createSentinelLogWriter(handle: string): Promise { + return SentinelLogWriter.create({ handle }); +} diff --git a/src/debug/jtag/workers/continuum-core/src/ai/adapter.rs b/src/debug/jtag/workers/continuum-core/src/ai/adapter.rs index 76a471adc..ef4810846 100644 --- a/src/debug/jtag/workers/continuum-core/src/ai/adapter.rs +++ b/src/debug/jtag/workers/continuum-core/src/ai/adapter.rs @@ -14,6 +14,7 @@ //! - Google (Gemini) //! - Local (Candle, llama.cpp) +use crate::clog_warn; use async_trait::async_trait; use super::types::{ @@ -369,7 +370,7 @@ impl AdapterRegistry { for id in ids { if let Some(adapter) = self.adapters.get_mut(&id) { if let Err(e) = adapter.initialize().await { - eprintln!("โš ๏ธ Failed to initialize {} adapter: {}", id, e); + clog_warn!("Failed to initialize {} adapter: {}", id, e); // Don't fail entirely - other adapters may work } } @@ -381,7 +382,7 @@ impl AdapterRegistry { pub async fn shutdown_all(&mut self) -> Result<(), String> { for (id, adapter) in self.adapters.iter_mut() { if let Err(e) = adapter.shutdown().await { - eprintln!("โš ๏ธ Failed to shutdown {} adapter: {}", id, e); + clog_warn!("Failed to shutdown {} adapter: {}", id, e); } } Ok(()) diff --git a/src/debug/jtag/workers/continuum-core/src/concurrent/message_processor.rs b/src/debug/jtag/workers/continuum-core/src/concurrent/message_processor.rs index cedcb1e5b..180c2877c 100644 --- a/src/debug/jtag/workers/continuum-core/src/concurrent/message_processor.rs +++ b/src/debug/jtag/workers/continuum-core/src/concurrent/message_processor.rs @@ -1,3 +1,4 @@ +use crate::clog_error; use tokio::sync::mpsc; use std::sync::Arc; @@ -46,7 +47,7 @@ impl ConcurrentProcessor

{ tokio::spawn(async move { if let Err(e) = processor.on_start().await { - eprintln!("Worker {worker_id}: start error: {e}"); + clog_error!("Worker {}: start error: {}", worker_id, e); return; } @@ -59,7 +60,7 @@ impl ConcurrentProcessor

{ match message { Some(msg) => { if let Err(e) = processor.process(msg).await { - eprintln!("Worker {worker_id}: process error: {e}"); + clog_error!("Worker {}: process error: {}", worker_id, e); } } None => break, // Channel closed diff --git a/src/debug/jtag/workers/continuum-core/src/lib.rs b/src/debug/jtag/workers/continuum-core/src/lib.rs index 404d9445a..7a54988e5 100644 --- a/src/debug/jtag/workers/continuum-core/src/lib.rs +++ b/src/debug/jtag/workers/continuum-core/src/lib.rs @@ -38,7 +38,9 @@ pub use persona::{ QueueItem, SenderType, Modality, Mood, }; pub use concurrent::*; -pub use logging::{init_logger, logger, LogLevel}; +// Easy logging macros - auto-route to proper log files based on module_path!() +// Usage: clog_info!("Session started"); clog_warn!("Warning"); etc. +pub use logging::{init_logger, logger, LogLevel, module_path_to_category, extract_component}; pub use ipc::start_server; pub use voice::call_server::CallManager; pub use rag::{RagEngine, RagContext, RagOptions, LlmMessage, MessageRole}; diff --git a/src/debug/jtag/workers/continuum-core/src/logging/mod.rs b/src/debug/jtag/workers/continuum-core/src/logging/mod.rs index c0e132e0b..65213e0b3 100644 --- a/src/debug/jtag/workers/continuum-core/src/logging/mod.rs +++ b/src/debug/jtag/workers/continuum-core/src/logging/mod.rs @@ -2,6 +2,27 @@ /// /// Integrates with the existing logger worker via Unix socket. /// Provides macros for performance timing and structured logging. +/// +/// # Easy Logging (Zero Setup) +/// +/// Auto-routing macros that derive category from module_path!(): +/// +/// ```rust +/// use crate::clog_info; +/// +/// // Auto-routes to modules/voice.log based on module path +/// clog_info!("Session started for user {}", user_id); +/// clog_warn!("Rate limit approaching"); +/// clog_error!("Connection failed: {}", err); +/// ``` +/// +/// # Explicit Logging (Full Control) +/// +/// ```rust +/// use crate::log_info; +/// +/// log_info!("personas/helper/cognition", "DecisionEngine", "Made decision: {}", decision); +/// ``` pub mod timing; pub mod client; @@ -114,3 +135,215 @@ macro_rules! log_error { } }; } + +// ============================================================================ +// Easy Auto-Routing Macros โ€” Zero Setup Required +// ============================================================================ +// +// These macros derive the category and component from module_path!(). +// No need to specify category/component - just log! +// +// Category routing examples: +// - continuum_core::voice::orchestrator โ†’ modules/voice +// - continuum_core::modules::data โ†’ modules/data +// - continuum_core::orm::sqlite โ†’ modules/orm +// - continuum_core::ipc::* โ†’ system/ipc + +/// Convert module_path!() to a log category. +/// +/// Maps Rust module paths to concern-based log categories: +/// - `continuum_core::voice::*` โ†’ `modules/voice` +/// - `continuum_core::modules::data` โ†’ `modules/data` +/// - `continuum_core::orm::*` โ†’ `modules/orm` +/// - `continuum_core::ipc::*` โ†’ `system/ipc` +pub fn module_path_to_category(module_path: &str) -> &'static str { + // Strip the crate prefix + let path = module_path + .strip_prefix("continuum_core::") + .unwrap_or(module_path); + + // Match first segment to determine category + if path.starts_with("modules::data") { + "modules/data" + } else if path.starts_with("modules::embedding") { + "modules/embedding" + } else if path.starts_with("modules::search") { + "modules/search" + } else if path.starts_with("modules::logger") { + "modules/logger" + } else if path.starts_with("modules::voice") { + "modules/voice" + } else if path.starts_with("modules::memory") { + "modules/memory" + } else if path.starts_with("modules::code") { + "modules/code" + } else if path.starts_with("modules::rag") { + "modules/rag" + } else if path.starts_with("modules::cognition") { + "modules/cognition" + } else if path.starts_with("modules::channel") { + "modules/channel" + } else if path.starts_with("modules::health") { + "modules/health" + } else if path.starts_with("modules::models") { + "modules/models" + } else if path.starts_with("voice::") { + "modules/voice" + } else if path.starts_with("orm::") { + "modules/orm" + } else if path.starts_with("ai::") || path.starts_with("inference::") { + "modules/inference" + } else if path.starts_with("memory::") { + "modules/memory" + } else if path.starts_with("rag::") { + "modules/rag" + } else if path.starts_with("code::") { + "modules/code" + } else if path.starts_with("ipc::") { + "system/ipc" + } else if path.starts_with("concurrent::") { + "system/concurrent" + } else if path.starts_with("ffi::") { + "system/ffi" + } else if path.starts_with("runtime::") { + "system/runtime" + } else if path.starts_with("persona::") { + "modules/persona" + } else { + "system/core" + } +} + +/// Extract component name from module path (last segment). +pub fn extract_component(module_path: &str) -> &str { + module_path.rsplit("::").next().unwrap_or(module_path) +} + +// ============================================================================ +// Non-Blocking Logging โ€” Uses LoggerModule's Optimized Writer +// ============================================================================ +// +// clog_* macros route to LoggerModule's queue_log() which: +// - Uses bounded sync_channel with try_send (GUARANTEED non-blocking) +// - Background writer with batching (250ms or 200 messages) +// - Per-category rate limiting (100 msg/sec) +// - File handle caching +// - Proper directory routing +// +// If LoggerModule not initialized, messages silently dropped. +// If channel full, messages silently dropped (NEVER blocks). + +use crate::modules::logger::{queue_log, LogLevel as ModuleLogLevel}; + +/// Queue log entry via LoggerModule (NON-BLOCKING) +#[inline] +pub fn write_log_direct(category: &str, level: &str, component: &str, message: &str) { + let log_level = match level { + "DEBUG" => ModuleLogLevel::Debug, + "INFO" => ModuleLogLevel::Info, + "WARN" => ModuleLogLevel::Warn, + "ERROR" => ModuleLogLevel::Error, + _ => ModuleLogLevel::Info, + }; + queue_log(category, log_level, component, message); +} + +/// Easy info log โ€” auto-routes by module_path!(), writes directly to files +#[macro_export] +macro_rules! clog_info { + ($($arg:tt)*) => {{ + let category = $crate::logging::module_path_to_category(module_path!()); + let component = $crate::logging::extract_component(module_path!()); + let message = format!($($arg)*); + $crate::logging::write_log_direct(category, "INFO", component, &message); + }}; +} + +/// Easy warn log โ€” auto-routes by module_path!(), writes directly to files +#[macro_export] +macro_rules! clog_warn { + ($($arg:tt)*) => {{ + let category = $crate::logging::module_path_to_category(module_path!()); + let component = $crate::logging::extract_component(module_path!()); + let message = format!($($arg)*); + $crate::logging::write_log_direct(category, "WARN", component, &message); + }}; +} + +/// Easy error log โ€” auto-routes by module_path!(), writes directly to files +#[macro_export] +macro_rules! clog_error { + ($($arg:tt)*) => {{ + let category = $crate::logging::module_path_to_category(module_path!()); + let component = $crate::logging::extract_component(module_path!()); + let message = format!($($arg)*); + $crate::logging::write_log_direct(category, "ERROR", component, &message); + }}; +} + +/// Easy debug log โ€” auto-routes by module_path!(), writes directly to files +#[macro_export] +macro_rules! clog_debug { + ($($arg:tt)*) => {{ + let category = $crate::logging::module_path_to_category(module_path!()); + let component = $crate::logging::extract_component(module_path!()); + let message = format!($($arg)*); + $crate::logging::write_log_direct(category, "DEBUG", component, &message); + }}; +} + +/// Log to explicit category (for cross-cutting concerns like personas, sentinels) +#[macro_export] +macro_rules! clog_to { + ($category:expr, info, $($arg:tt)*) => {{ + let component = $crate::logging::extract_component(module_path!()); + let message = format!($($arg)*); + $crate::logging::write_log_direct($category, "INFO", component, &message); + }}; + ($category:expr, warn, $($arg:tt)*) => {{ + let component = $crate::logging::extract_component(module_path!()); + let message = format!($($arg)*); + $crate::logging::write_log_direct($category, "WARN", component, &message); + }}; + ($category:expr, error, $($arg:tt)*) => {{ + let component = $crate::logging::extract_component(module_path!()); + let message = format!($($arg)*); + $crate::logging::write_log_direct($category, "ERROR", component, &message); + }}; + ($category:expr, debug, $($arg:tt)*) => {{ + let component = $crate::logging::extract_component(module_path!()); + let message = format!($($arg)*); + $crate::logging::write_log_direct($category, "DEBUG", component, &message); + }}; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_module_path_conversion() { + assert_eq!( + module_path_to_category("continuum_core::voice::orchestrator"), + "modules/voice" + ); + assert_eq!( + module_path_to_category("continuum_core::modules::data"), + "modules/data" + ); + assert_eq!( + module_path_to_category("continuum_core::orm::sqlite"), + "modules/orm" + ); + assert_eq!( + module_path_to_category("continuum_core::ipc::handler"), + "system/ipc" + ); + } + + #[test] + fn test_extract_component() { + assert_eq!(extract_component("continuum_core::voice::orchestrator"), "orchestrator"); + assert_eq!(extract_component("my_module"), "my_module"); + } +} diff --git a/src/debug/jtag/workers/continuum-core/src/modules/logger.rs b/src/debug/jtag/workers/continuum-core/src/modules/logger.rs index f7c945f63..87be80aa9 100644 --- a/src/debug/jtag/workers/continuum-core/src/modules/logger.rs +++ b/src/debug/jtag/workers/continuum-core/src/modules/logger.rs @@ -6,11 +6,18 @@ //! - File handle caching (files stay open) //! - Auto-recovery if log files deleted //! - Per-file locking (no global contention) +//! - Global sender for clog_* macros (non-blocking) //! //! Commands: //! - log/write: Write log entry to file //! - log/ping: Health check with stats //! +//! Usage from Rust code: +//! ```rust +//! use crate::clog_info; +//! clog_info!("Session started"); // Non-blocking, routes to modules/voice.log +//! ``` +//! //! Migration from: workers/logger (222 lines main.rs + 4 modules) use crate::runtime::{CommandResult, ModuleConfig, ModuleContext, ModulePriority, ServiceModule}; @@ -24,11 +31,42 @@ use std::fs::{self, File, OpenOptions}; use std::io::Write; use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{mpsc, Arc, Mutex}; +use std::sync::{mpsc, Arc, Mutex, OnceLock}; use std::thread; use std::time::{Duration, Instant}; use ts_rs::TS; +// ============================================================================ +// Global Logger Sender โ€” For clog_* Macros +// ============================================================================ + +/// Global sender for clog_* macros. Set by LoggerModule::new(). +/// Uses SyncSender with try_send() for GUARANTEED non-blocking. +static GLOBAL_LOG_SENDER: OnceLock> = OnceLock::new(); + +/// Channel capacity - if full, new messages dropped (NEVER blocks) +const CLOG_CHANNEL_CAPACITY: usize = 4096; + +/// Queue a log entry for async writing (called by clog_* macros). +/// GUARANTEED NON-BLOCKING: Uses try_send(), drops if channel full. +/// If LoggerModule not yet initialized, message is dropped. +#[inline] +pub fn queue_log(category: &str, level: LogLevel, component: &str, message: &str) { + if let Some(sender) = GLOBAL_LOG_SENDER.get() { + let payload = WriteLogPayload { + category: category.to_string(), + level, + component: component.to_string(), + message: message.to_string(), + args: None, + }; + // GUARANTEED NON-BLOCKING: try_send returns immediately + // If channel full, message dropped - NEVER blocks caller + let _ = sender.try_send(payload); + } + // If GLOBAL_LOG_SENDER not set, silently drop (LoggerModule not initialized yet) +} + // ============================================================================ // Types (matches legacy worker's messages.rs) // ============================================================================ @@ -174,11 +212,53 @@ type LockedFile = Arc>; type FileCache = Arc>>; type HeaderTracker = Arc>>; +/// Resolve category to proper log path based on concern hierarchy. +/// +/// Categories follow a structured naming convention: +/// - `system/{component}` โ†’ .continuum/jtag/logs/system/{component}.log +/// - `modules/{module}` โ†’ .continuum/jtag/logs/modules/{module}.log +/// - `personas/{uniqueId}/{subsystem}` โ†’ .continuum/personas/{uniqueId}/logs/{subsystem}.log +/// - `sentinels/{handle}/{stream}` โ†’ .sentinel-workspaces/{handle}/logs/{stream}.log +/// - `daemons/{name}` โ†’ .continuum/jtag/logs/system/daemons/{name}.log +/// - Anything else โ†’ .continuum/jtag/logs/system/{category}.log (legacy fallback) fn resolve_log_path(category: &str, log_dir: &str) -> PathBuf { - if category.starts_with("personas/") { - PathBuf::from(format!(".continuum/{category}.log")) - } else { - PathBuf::from(log_dir).join(format!("{category}.log")) + let parts: Vec<&str> = category.split('/').collect(); + + match parts.as_slice() { + // personas/{uniqueId}/{subsystem} โ†’ .continuum/personas/{uniqueId}/logs/{subsystem}.log + ["personas", unique_id, subsystem] => { + PathBuf::from(format!(".continuum/personas/{unique_id}/logs/{subsystem}.log")) + } + // personas/{uniqueId} โ†’ .continuum/personas/{uniqueId}/logs/general.log + ["personas", unique_id] => { + PathBuf::from(format!(".continuum/personas/{unique_id}/logs/general.log")) + } + // sentinels/{handle}/{stream} โ†’ .sentinel-workspaces/{handle}/logs/{stream}.log + ["sentinels", handle, stream] => { + PathBuf::from(format!(".sentinel-workspaces/{handle}/logs/{stream}.log")) + } + // sentinels/{handle} โ†’ .sentinel-workspaces/{handle}/logs/execution.log + ["sentinels", handle] => { + PathBuf::from(format!(".sentinel-workspaces/{handle}/logs/execution.log")) + } + // modules/{module} โ†’ {log_dir}/modules/{module}.log + ["modules", module] => { + PathBuf::from(log_dir).join(format!("modules/{module}.log")) + } + // daemons/{name} โ†’ {log_dir}/daemons/{name}.log + ["daemons", name] => { + PathBuf::from(log_dir).join(format!("daemons/{name}.log")) + } + // system/{component} โ†’ {log_dir}/{component}.log + ["system", component] => { + PathBuf::from(log_dir).join(format!("{component}.log")) + } + // Legacy/fallback: put in system dir with category as filename + _ => { + // Replace slashes with underscores for legacy categories + let safe_name = category.replace('/', "_"); + PathBuf::from(log_dir).join(format!("{safe_name}.log")) + } } } @@ -347,7 +427,7 @@ pub struct LoggerModule { file_cache: FileCache, #[allow(dead_code)] // Used by writer thread, but compiler doesn't see through thread::spawn headers_written: HeaderTracker, - log_tx: mpsc::Sender, + log_tx: mpsc::SyncSender, started_at: Instant, requests_processed: AtomicU64, pending_writes: Arc, @@ -362,8 +442,12 @@ impl LoggerModule { let headers_written = Arc::new(Mutex::new(HashSet::new())); let pending_writes = Arc::new(AtomicU64::new(0)); - // Create channel for background writer - let (log_tx, log_rx) = mpsc::channel::(); + // Create BOUNDED sync_channel for GUARANTEED non-blocking + // try_send() returns immediately - if full, message dropped (NEVER blocks) + let (log_tx, log_rx) = mpsc::sync_channel::(CLOG_CHANNEL_CAPACITY); + + // Set global sender for clog_* macros (if not already set) + let _ = GLOBAL_LOG_SENDER.set(log_tx.clone()); // Spawn dedicated writer thread (same architecture as legacy worker) let writer_file_cache = file_cache.clone(); diff --git a/src/debug/jtag/workers/continuum-core/src/orm/sqlite.rs b/src/debug/jtag/workers/continuum-core/src/orm/sqlite.rs index 25da97b94..4f5db59ba 100644 --- a/src/debug/jtag/workers/continuum-core/src/orm/sqlite.rs +++ b/src/debug/jtag/workers/continuum-core/src/orm/sqlite.rs @@ -4,6 +4,7 @@ //! Uses a dedicated thread for SQLite operations since rusqlite::Connection //! is not Send+Sync. +use crate::{clog_info, clog_error, clog_warn}; use async_trait::async_trait; use rusqlite::{params, Connection, OpenFlags}; use serde_json::{json, Value}; @@ -103,7 +104,7 @@ impl Default for SqliteAdapter { /// Worker thread that owns the SQLite connection fn sqlite_worker(path: String, mut receiver: mpsc::Receiver) { - eprintln!("[sqlite_worker] Starting worker for path: {}", path); + clog_info!("Starting worker for path: {}", path); // Open connection let conn = match Connection::open_with_flags( @@ -114,15 +115,15 @@ fn sqlite_worker(path: String, mut receiver: mpsc::Receiver) { ) { Ok(c) => c, Err(e) => { - eprintln!("[sqlite_worker] ERROR: SQLite open failed: {}", e); + clog_error!("SQLite open failed: {}", e); return; } }; - eprintln!("[sqlite_worker] Connection opened successfully"); + clog_info!("Connection opened successfully"); // Enable WAL mode for better concurrency if let Err(e) = conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;") { - eprintln!("[sqlite_worker] PRAGMA error: {}", e); + clog_error!("PRAGMA error: {}", e); } let mut query_count = 0u64; @@ -159,7 +160,7 @@ fn sqlite_worker(path: String, mut receiver: mpsc::Receiver) { let collection = query.collection.clone(); let result = do_query(&conn, query); if start.elapsed().as_millis() > 100 { - eprintln!("[sqlite_worker] SLOW query #{} on {}: {}ms", query_count, collection, start.elapsed().as_millis()); + clog_warn!("SLOW query #{} on {}: {}ms", query_count, collection, start.elapsed().as_millis()); } let _ = reply.send(result); } @@ -248,7 +249,7 @@ fn ensure_table_exists(conn: &Connection, table: &str, data: &Value) { ); if let Err(e) = conn.execute(&sql, []) { - eprintln!("SQLite table creation error for '{}': {}", table, e); + clog_error!("SQLite table creation error for '{}': {}", table, e); } } diff --git a/src/debug/jtag/workers/continuum-core/src/voice/orchestrator.rs b/src/debug/jtag/workers/continuum-core/src/voice/orchestrator.rs index 4656a5ee8..c743a7959 100644 --- a/src/debug/jtag/workers/continuum-core/src/voice/orchestrator.rs +++ b/src/debug/jtag/workers/continuum-core/src/voice/orchestrator.rs @@ -1,4 +1,5 @@ use super::types::*; +use crate::clog_info; use std::collections::HashMap; use std::sync::{Arc, Mutex}; use uuid::Uuid; @@ -33,7 +34,7 @@ impl VoiceOrchestrator { let mut contexts = self.session_contexts.lock().unwrap(); contexts.insert(session_id, ConversationContext::new(session_id, room_id)); } - println!("๐ŸŽ™๏ธ VoiceOrchestrator: Registered session {} with {} participants", + clog_info!("Registered session {} with {} participants", &session_id.to_string()[..8], participants.len()); } @@ -41,13 +42,13 @@ impl VoiceOrchestrator { self.session_participants.lock().unwrap().remove(&session_id); self.session_contexts.lock().unwrap().remove(&session_id); self.voice_responders.lock().unwrap().remove(&session_id); - println!("๐ŸŽ™๏ธ VoiceOrchestrator: Unregistered session {}", &session_id.to_string()[..8]); + clog_info!("Unregistered session {}", &session_id.to_string()[..8]); } /// Process utterance and return ALL AI participant IDs (broadcast model) /// Each AI will decide if they want to respond via their own logic pub fn on_utterance(&self, event: UtteranceEvent) -> Vec { - println!("๐ŸŽ™๏ธ VoiceOrchestrator: Utterance from {}: \"{}...\"", + clog_info!("Utterance from {}: \"{}...\"", event.speaker_name, crate::voice::tts::truncate_str(&event.transcript, 50)); // Get context @@ -55,7 +56,7 @@ impl VoiceOrchestrator { let context = match contexts.get_mut(&event.session_id) { Some(ctx) => ctx, None => { - println!("๐ŸŽ™๏ธ VoiceOrchestrator: No context for session {}", crate::voice::tts::truncate_str(&event.session_id.to_string(), 8)); + clog_info!("No context for session {}", crate::voice::tts::truncate_str(&event.session_id.to_string(), 8)); return Vec::new(); } }; @@ -68,7 +69,7 @@ impl VoiceOrchestrator { let session_participants = match participants.get(&event.session_id) { Some(p) => p, None => { - println!("๐ŸŽ™๏ธ VoiceOrchestrator: No participants for session {}", &event.session_id.to_string()[..8]); + clog_info!("No participants for session {}", &event.session_id.to_string()[..8]); return Vec::new(); } }; @@ -80,13 +81,13 @@ impl VoiceOrchestrator { .collect(); if ai_participants.is_empty() { - println!("๐ŸŽ™๏ธ VoiceOrchestrator: No AI participants to respond"); + clog_info!("No AI participants to respond"); return Vec::new(); } // NO ARBITER - broadcast to ALL AI participants, let THEM decide if they want to respond // Their PersonaUser.shouldRespond() logic handles engagement decisions - println!("๐ŸŽ™๏ธ VoiceOrchestrator: Broadcasting to {} AIs (no filtering)", ai_participants.len()); + clog_info!("Broadcasting to {} AIs (no filtering)", ai_participants.len()); ai_participants.iter().map(|p| p.user_id).collect() } diff --git a/src/debug/jtag/workers/continuum-core/src/voice/tts/phonemizer.rs b/src/debug/jtag/workers/continuum-core/src/voice/tts/phonemizer.rs index e3885e615..65fee9b78 100644 --- a/src/debug/jtag/workers/continuum-core/src/voice/tts/phonemizer.rs +++ b/src/debug/jtag/workers/continuum-core/src/voice/tts/phonemizer.rs @@ -1,6 +1,7 @@ //! Phonemizer using espeak-ng for text-to-phoneme conversion //! Piper TTS models require espeak-ng IPA phonemes +use crate::{clog_error, clog_warn}; use std::collections::HashMap; use std::process::Command; @@ -66,7 +67,7 @@ impl Phonemizer { let phonemes = match self.call_espeak(text) { Ok(p) => p, Err(e) => { - eprintln!("Phonemizer error: {e}"); + clog_error!("Phonemizer error: {}", e); // Return minimal valid sequence on error return vec![1, 59, 2]; // ^, ษ™, $ } @@ -93,14 +94,14 @@ impl Phonemizer { unknown_count += 1; if unknown_count <= 5 { // Only log first 5 to avoid spam let ch_code = ch as u32; - eprintln!("Unknown phoneme '{ch}' (U+{ch_code:04X}), skipping"); + clog_warn!("Unknown phoneme '{}' (U+{:04X}), skipping", ch, ch_code); } } } if unknown_count > 5 { let remaining = unknown_count - 5; - eprintln!("... and {remaining} more unknown phonemes"); + clog_warn!("... and {} more unknown phonemes", remaining); } // If we got no valid phonemes, return minimal sequence @@ -121,7 +122,7 @@ impl Default for Phonemizer { // Load from default model config Self::load_from_config("../models/piper/en_US-libritts_r-medium.onnx.json") .unwrap_or_else(|e| { - eprintln!("Failed to load phoneme map from config: {e}"); + clog_error!("Failed to load phoneme map from config: {}", e); Self { phoneme_to_id: HashMap::new() } }) } From 070a4e346ad54955f7e88728b47475e65809dcec Mon Sep 17 00:00:00 2001 From: joelteply Date: Fri, 13 Feb 2026 14:32:09 -0600 Subject: [PATCH 2/2] Candle token limiting: prevent NaN in quantized models - Add SAFE_INPUT_TOKENS (800) limit for quantized model input - Token-based truncation using tokenizer (not character-based) - Preserves 30% start (system prompt) + 60% end (recent messages) - Logs TOKEN LIMIT warning when truncation occurs Also: - Fix ts-rs binding generator to ignore harmless serde warnings - Add threshold test for future NaN threshold validation - Add INCIDENT_CAPTURE logging for reproducibility Verified: Helper AI and Teacher AI now produce coherent responses instead of garbage output from NaN/Inf in logits. --- src/debug/jtag/generated-command-schemas.json | 2 +- .../jtag/generator/generate-rust-bindings.ts | 10 ++ src/debug/jtag/package-lock.json | 4 +- src/debug/jtag/package.json | 2 +- src/debug/jtag/shared/version.ts | 2 +- .../src/inference/candle_adapter.rs | 49 +++++- .../continuum-core/src/inference/quantized.rs | 153 ++++++++++++++++++ 7 files changed, 215 insertions(+), 7 deletions(-) diff --git a/src/debug/jtag/generated-command-schemas.json b/src/debug/jtag/generated-command-schemas.json index a5accd50a..197472df4 100644 --- a/src/debug/jtag/generated-command-schemas.json +++ b/src/debug/jtag/generated-command-schemas.json @@ -1,5 +1,5 @@ { - "generated": "2026-02-13T17:53:34.018Z", + "generated": "2026-02-13T20:12:30.837Z", "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 index 0cb0c18bb..928b10d8b 100644 --- a/src/debug/jtag/generator/generate-rust-bindings.ts +++ b/src/debug/jtag/generator/generate-rust-bindings.ts @@ -84,6 +84,16 @@ function generateBindings(pkg: string, description: string): boolean { return true; } + // ts-rs emits harmless warnings about serde attributes it can't parse + // These should not fail the build + const isOnlyTsRsWarnings = stderr.includes('ts-rs failed to parse this attribute') && + !stderr.includes('error[') && !stderr.includes('error:') && + !stderr.includes('could not compile'); + if (isOnlyTsRsWarnings) { + console.log(` โš ๏ธ ts-rs warnings (ignored)`); + return true; + } + console.error(` โŒ Failed: ${stderr.slice(0, 200)}`); return false; } diff --git a/src/debug/jtag/package-lock.json b/src/debug/jtag/package-lock.json index f4331a579..9c1a93179 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.7861", + "version": "1.0.7873", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@continuum/jtag", - "version": "1.0.7861", + "version": "1.0.7873", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/src/debug/jtag/package.json b/src/debug/jtag/package.json index 687125f9f..5e90706ae 100644 --- a/src/debug/jtag/package.json +++ b/src/debug/jtag/package.json @@ -1,6 +1,6 @@ { "name": "@continuum/jtag", - "version": "1.0.7861", + "version": "1.0.7873", "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 33645affd..fe6f2b4c7 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.7861'; +export const VERSION = '1.0.7873'; export const PACKAGE_NAME = '@continuum/jtag'; diff --git a/src/debug/jtag/workers/continuum-core/src/inference/candle_adapter.rs b/src/debug/jtag/workers/continuum-core/src/inference/candle_adapter.rs index 460ef375b..0ffb39a6f 100644 --- a/src/debug/jtag/workers/continuum-core/src/inference/candle_adapter.rs +++ b/src/debug/jtag/workers/continuum-core/src/inference/candle_adapter.rs @@ -431,12 +431,57 @@ impl AIProviderAdapter for CandleAdapter { "Model not loaded".to_string() })?; + // For quantized models, limit input tokens to prevent NaN + // Based on testing: NaN occurs at ~1400+ tokens, safe threshold is ~800 + // This token-based limiting is more accurate than char-based EMERGENCY_MAX_CHARS + let final_prompt = match model { + ModelVariant::Quantized(state) => { + const SAFE_INPUT_TOKENS: usize = 800; + let tokens = state.tokenizer.encode(prompt.as_str(), true) + .map_err(|e| format!("Tokenization failed: {e}"))?; + let token_count = tokens.len(); + + if token_count > SAFE_INPUT_TOKENS { + // Truncate by keeping first ~30% and last ~60% of tokens + // (system prompt at start, recent messages at end) + let keep_start = SAFE_INPUT_TOKENS * 30 / 100; // 240 tokens + let keep_end = SAFE_INPUT_TOKENS * 60 / 100; // 480 tokens + + let token_ids = tokens.get_ids(); + let mut truncated_ids: Vec = Vec::with_capacity(SAFE_INPUT_TOKENS + 10); + + // Keep first N tokens (system prompt) + truncated_ids.extend_from_slice(&token_ids[..keep_start]); + + // Add truncation marker (just use newlines, don't add extra tokens) + // The model will understand context was cut + + // Keep last N tokens (recent messages) + let end_start = token_ids.len().saturating_sub(keep_end); + truncated_ids.extend_from_slice(&token_ids[end_start..]); + + let truncated_prompt = state.tokenizer.decode(&truncated_ids, true) + .map_err(|e| format!("Decode failed: {e}"))?; + + log.warn(&format!( + "TOKEN LIMIT: {} -> {} tokens (RAG sent too much context for quantized model)", + token_count, truncated_ids.len() + )); + + truncated_prompt + } else { + prompt.clone() + } + } + ModelVariant::Regular(_) => prompt.clone(), + }; + let (output_text, completion_tokens) = match model { ModelVariant::Regular(state) => { - generate_text(state, &prompt, max_tokens, temperature)? + generate_text(state, &final_prompt, max_tokens, temperature)? } ModelVariant::Quantized(state) => { - generate_text_quantized(state, &prompt, max_tokens, temperature)? + generate_text_quantized(state, &final_prompt, max_tokens, temperature)? } }; diff --git a/src/debug/jtag/workers/continuum-core/src/inference/quantized.rs b/src/debug/jtag/workers/continuum-core/src/inference/quantized.rs index f2084c6c8..3f232c5f9 100644 --- a/src/debug/jtag/workers/continuum-core/src/inference/quantized.rs +++ b/src/debug/jtag/workers/continuum-core/src/inference/quantized.rs @@ -223,6 +223,22 @@ pub fn generate_text_quantized( log.debug(&format!("Quantized generation: {} tokens from {} char prompt", prompt_len, prompt.len())); + // INCIDENT CAPTURE: Log prompt hash and first/last chars for reproducibility + // When NaN occurs, we can find this prompt in logs and recreate in tests + let prompt_hash = { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + let mut hasher = DefaultHasher::new(); + prompt.hash(&mut hasher); + hasher.finish() + }; + log.debug(&format!( + "INCIDENT_CAPTURE: prompt_hash={:016x} tokens={} first_100={}", + prompt_hash, + prompt_len, + &prompt.chars().take(100).collect::().replace('\n', "\\n") + )); + // Setup logits processor let seed = rand::thread_rng().gen::(); let mut logits_processor = LogitsProcessor::new(seed, Some(temperature), None); @@ -475,4 +491,141 @@ mod tests { || output_lower.contains("greet"); assert!(has_greeting, "Output should contain a greeting: {}", output); } + + /// Test to find the NaN threshold for quantized model + /// + /// This test sends progressively longer prompts to identify the exact + /// token count where NaN starts occurring. Used to set safe limits. + /// + /// Known from production logs: + /// - 149 tokens: works fine + /// - 1451 tokens: NaN detected + /// - 1622 tokens: NaN abort + /// + /// Run with: cargo test --release test_find_nan_threshold -- --ignored --nocapture + #[test] + #[ignore] // Requires model download, takes several minutes + fn test_find_nan_threshold() { + let mut state = load_default_quantized() + .expect("Failed to load quantized model"); + + println!("Finding NaN threshold for model: {}", state.model_id); + println!("============================================"); + + // Test at different token counts + // We'll generate prompts of various sizes and see where NaN appears + let test_sizes: Vec = vec![ + 100, // Should work + 200, // Should work + 400, // Should work + 600, // Likely works + 800, // May start having issues + 1000, // Threshold area based on docs + 1200, // Above documented threshold + 1400, // Near observed failure point + ]; + + // Create a repeatable filler that tokenizes consistently + // "The quick brown fox jumps. " is ~7 tokens + let filler = "The quick brown fox jumps over the lazy dog. "; + + for target_tokens in test_sizes { + // Build prompt with approximately target_tokens + // Header is ~20 tokens, so subtract that + let content_tokens = target_tokens.saturating_sub(20); + let repetitions = content_tokens / 10; // ~10 tokens per filler repetition + + let mut content = String::new(); + for _ in 0..repetitions { + content.push_str(filler); + } + + let prompt = format!( + "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n{}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n", + content + ); + + // Count actual tokens + let tokens_encoded = state.tokenizer.encode(prompt.as_str(), true) + .expect("Tokenization failed"); + let actual_tokens = tokens_encoded.len(); + + print!("Testing {} tokens (target {})... ", actual_tokens, target_tokens); + + // Reload model to clear KV cache before each test + state.reload_model().expect("Reload failed"); + + match generate_text_quantized(&mut state, &prompt, 20, 0.3) { + Ok((output, gen_tokens)) => { + // Check for garbage output (indicates NaN recovery produced junk) + let has_garbage = output.chars().any(|c| { + c == '\u{FFFD}' || // replacement char + (c as u32 > 0x1F000) || // emoji/symbol range often = garbage + output.contains("zeroes") || // Known garbage pattern + output.contains("valueOf") // Known garbage pattern + }); + + if has_garbage { + println!("โš ๏ธ {} tokens generated but GARBAGE detected: {}", + gen_tokens, &output.chars().take(50).collect::()); + } else { + println!("โœ“ {} tokens, output: {}", + gen_tokens, &output.chars().take(30).collect::()); + } + } + Err(e) => { + println!("โœ— FAILED: {}", e); + println!("\n==> NaN threshold appears to be around {} input tokens", actual_tokens); + break; + } + } + } + + println!("\n============================================"); + println!("Test complete. Use results to set safe input token limits."); + } + + /// Test that prompts at the safe threshold work reliably + /// + /// Run with: cargo test --release test_safe_threshold -- --ignored --nocapture + #[test] + #[ignore] + fn test_safe_threshold() { + let mut state = load_default_quantized() + .expect("Failed to load quantized model"); + + // Test at safe threshold (800 tokens based on analysis) + const SAFE_INPUT_TOKENS: usize = 800; + + let filler = "The quick brown fox jumps over the lazy dog. "; + let repetitions = (SAFE_INPUT_TOKENS - 20) / 10; + + let mut content = String::new(); + for _ in 0..repetitions { + content.push_str(filler); + } + + let prompt = format!( + "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n{}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n", + content + ); + + let tokens = state.tokenizer.encode(prompt.as_str(), true) + .expect("Tokenization failed").len(); + + println!("Testing safe threshold with {} tokens", tokens); + + // Run 5 times to ensure reliability + for i in 0..5 { + state.reload_model().expect("Reload failed"); + let (output, gen_tokens) = generate_text_quantized(&mut state, &prompt, 20, 0.3) + .expect(&format!("Generation {} failed", i + 1)); + + assert!(!output.contains('\u{FFFD}'), "Output {} contains garbage", i + 1); + assert!(!output.contains("zeroes"), "Output {} contains garbage pattern", i + 1); + println!("Run {}: {} tokens, OK", i + 1, gen_tokens); + } + + println!("โœ“ Safe threshold of {} tokens verified reliable", tokens); + } }