titan-agent 5.5.9 → 5.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -91,7 +91,7 @@ async function captureBugReport(err, context = {}) {
|
|
|
91
91
|
}
|
|
92
92
|
try {
|
|
93
93
|
const { trackEvent } = await import("./featureTracker.js");
|
|
94
|
-
await trackEvent("
|
|
94
|
+
await trackEvent("error", {
|
|
95
95
|
bug_id: report.id,
|
|
96
96
|
error_name: report.error.name,
|
|
97
97
|
error_message: report.error.message,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/analytics/bugReports.ts"],"sourcesContent":["/**\n * TITAN — Bug Report Capture\n *\n * Captures runtime errors with rich agent context so the human team\n * (Tony) plus the agent collaborators (Claude, Kimi) can review and\n * fix them. Telemetry-gated like everything else: no data leaves the\n * machine unless `telemetry.enabled === true`.\n *\n * Each report is:\n * • appended to `~/.titan/bug-reports.jsonl` (always, when capture is wired)\n * • forwarded to PostHog as a `bug_report` event when telemetry is on\n * • exposed at `GET /api/bug-reports` for review\n *\n * Stack traces are scrubbed by the existing `outboundSanitizer`\n * before they leave the machine. The local file is the un-scrubbed\n * source of truth for the operator.\n */\nimport { existsSync, mkdirSync, readFileSync, appendFileSync, statSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'BugReports';\n\nconst BUG_REPORTS_DIR = TITAN_HOME;\nconst BUG_REPORTS_PATH = join(BUG_REPORTS_DIR, 'bug-reports.jsonl');\nconst BUG_REPORTS_PREVIOUS = join(BUG_REPORTS_DIR, 'bug-reports.previous.jsonl');\nconst MAX_FILE_BYTES = 5 * 1024 * 1024; // 5 MB before rotation\nconst MAX_REPORTS_RETURNED = 200;\n\nexport interface BugReportContext {\n /** Active session id, if any */\n sessionId?: string;\n /** Channel where the work originated (webchat, voice, telegram, etc.) */\n channel?: string;\n /** Resolved model id at time of failure */\n model?: string;\n /** Last user message, truncated to 240 chars */\n lastUserMessage?: string;\n /** Last assistant content preview, truncated to 240 chars */\n lastAssistantPreview?: string;\n /** Up to last 5 tool names called this turn */\n toolsUsed?: string[];\n /** System prompt token estimate at failure */\n promptLength?: number;\n /** 1-indexed turn within the session */\n turnNumber?: number;\n /** Free-form tag for callers (e.g. 'agent.processMessage', 'gateway.api') */\n origin?: string;\n}\n\nexport interface BugReport {\n /** Stable id for cross-system reference (`bug_<ts>_<rand>`) */\n id: string;\n /** ISO timestamp when capture fired */\n ts: string;\n /** TITAN runtime version */\n version: string;\n /** Anonymous install id from mesh identity */\n installId: string;\n /** Error name, message, stack — stack truncated to 4 KB */\n error: {\n name: string;\n message: string;\n stack: string;\n };\n /** Caller-provided agent/turn context */\n context: BugReportContext;\n /** Bucketed system info — never the exact CPU/GPU model */\n system: {\n os: string;\n arch: string;\n nodeMajor: number;\n ramGB?: number;\n gpuVramGB?: number;\n };\n}\n\nlet lastBugReportTs = 0;\nconst MIN_INTERVAL_MS = 250; // burst guard\n\nfunction ensureDir(): void {\n if (!existsSync(BUG_REPORTS_DIR)) {\n mkdirSync(BUG_REPORTS_DIR, { recursive: true });\n }\n}\n\nfunction rotateIfNeeded(): void {\n try {\n if (!existsSync(BUG_REPORTS_PATH)) return;\n const size = statSync(BUG_REPORTS_PATH).size;\n if (size < MAX_FILE_BYTES) return;\n renameSync(BUG_REPORTS_PATH, BUG_REPORTS_PREVIOUS);\n } catch {\n /* non-fatal */\n }\n}\n\nfunction safeStack(err: Error): string {\n const stack = err.stack || `${err.name}: ${err.message}`;\n return stack.length > 4096 ? stack.slice(0, 4096) + '\\n…(truncated)' : stack;\n}\n\nfunction bucketGB(mb?: number, step = 4): number | undefined {\n if (typeof mb !== 'number' || !Number.isFinite(mb) || mb <= 0) return undefined;\n const gb = mb / 1024;\n return Math.round(gb / step) * step;\n}\n\nasync function buildSystemSummary(): Promise<BugReport['system']> {\n const { platform, arch, totalmem } = await import('os');\n let gpuVramMB: number | undefined;\n try {\n const { detectHardware } = await import('../hardware/autoConfig.js');\n const hw = await detectHardware();\n gpuVramMB = hw.gpuVramMB;\n } catch { /* ignore */ }\n const nodeMajor = parseInt((process.version.match(/^v(\\d+)/) || ['', '0'])[1] || '0', 10);\n return {\n os: platform(),\n arch: arch(),\n nodeMajor,\n ramGB: bucketGB(totalmem() / (1024 * 1024)),\n gpuVramGB: bucketGB(gpuVramMB),\n };\n}\n\nfunction newReportId(): string {\n return `bug_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n}\n\n/**\n * Capture an error with surrounding agent context. Best-effort: never\n * throws back to the caller — if persistence or telemetry fails, the\n * original error path stays intact.\n */\nexport async function captureBugReport(err: unknown, context: BugReportContext = {}): Promise<BugReport | null> {\n try {\n // Burst guard: drop reports that fire within 250ms of each other\n // to avoid overwhelming disk + PostHog when an error loops.\n const now = Date.now();\n if (now - lastBugReportTs < MIN_INTERVAL_MS) return null;\n lastBugReportTs = now;\n\n const error: Error = err instanceof Error ? err : new Error(String(err ?? 'unknown error'));\n const { TITAN_VERSION } = await import('../utils/constants.js');\n const { getOrCreateNodeId } = await import('../mesh/identity.js');\n\n // Trim PII-prone fields defensively. Operators see the full thing\n // in the local file but PostHog never gets a long user message.\n const ctx: BugReportContext = {\n ...context,\n lastUserMessage: context.lastUserMessage?.slice(0, 240),\n lastAssistantPreview: context.lastAssistantPreview?.slice(0, 240),\n toolsUsed: context.toolsUsed?.slice(0, 5),\n };\n\n const report: BugReport = {\n id: newReportId(),\n ts: new Date().toISOString(),\n version: TITAN_VERSION,\n installId: getOrCreateNodeId(),\n error: {\n name: error.name,\n message: error.message,\n stack: safeStack(error),\n },\n context: ctx,\n system: await buildSystemSummary(),\n };\n\n // Persist locally first — operator-readable record of truth.\n try {\n ensureDir();\n rotateIfNeeded();\n appendFileSync(BUG_REPORTS_PATH, JSON.stringify(report) + '\\n');\n } catch (writeErr) {\n logger.warn(COMPONENT, `Local persist failed: ${(writeErr as Error).message}`);\n }\n\n // Forward to PostHog if telemetry is on. featureTracker handles\n // the opt-in gate, sanitizer, and best-effort fetch.\n try {\n const { trackEvent } = await import('./featureTracker.js');\n await trackEvent('bug_report', {\n bug_id: report.id,\n error_name: report.error.name,\n error_message: report.error.message,\n origin: report.context.origin,\n model: report.context.model,\n channel: report.context.channel,\n tools_used: report.context.toolsUsed,\n prompt_length: report.context.promptLength,\n turn_number: report.context.turnNumber,\n os: report.system.os,\n arch: report.system.arch,\n node_major: report.system.nodeMajor,\n ram_gb: report.system.ramGB,\n gpu_vram_gb: report.system.gpuVramGB,\n titan_version: report.version,\n stack_preview: report.error.stack.slice(0, 800),\n });\n } catch (sendErr) {\n logger.debug(COMPONENT, `Remote send skipped: ${(sendErr as Error).message}`);\n }\n\n return report;\n } catch (selfErr) {\n // Never let bug-report capture itself become a bug.\n try {\n logger.warn(COMPONENT, `captureBugReport itself failed: ${(selfErr as Error).message}`);\n } catch { /* deeply broken */ }\n return null;\n }\n}\n\n/**\n * List recent bug reports for review. Reads the local jsonl file —\n * never PostHog — so the operator can review even when telemetry is\n * off. Returns most-recent first.\n */\nexport function listRecentBugReports(limit = 50): BugReport[] {\n try {\n if (!existsSync(BUG_REPORTS_PATH)) return [];\n const raw = readFileSync(BUG_REPORTS_PATH, 'utf-8');\n const lines = raw.split('\\n').filter(Boolean);\n const cap = Math.min(Math.max(1, limit), MAX_REPORTS_RETURNED);\n const tail = lines.slice(-cap).reverse();\n const reports: BugReport[] = [];\n for (const line of tail) {\n try {\n reports.push(JSON.parse(line));\n } catch { /* skip malformed */ }\n }\n return reports;\n } catch (err) {\n logger.warn(COMPONENT, `listRecentBugReports failed: ${(err as Error).message}`);\n return [];\n }\n}\n\n/** Lookup one report by id from the local file. */\nexport function getBugReport(id: string): BugReport | null {\n try {\n if (!existsSync(BUG_REPORTS_PATH)) return null;\n const raw = readFileSync(BUG_REPORTS_PATH, 'utf-8');\n for (const line of raw.split('\\n')) {\n if (!line) continue;\n try {\n const r = JSON.parse(line) as BugReport;\n if (r.id === id) return r;\n } catch { /* skip */ }\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** File path so /api/bug-reports/raw can stream the operator file. */\nexport function getBugReportsPath(): string {\n return BUG_REPORTS_PATH;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/analytics/bugReports.ts"],"sourcesContent":["/**\n * TITAN — Bug Report Capture\n *\n * Captures runtime errors with rich agent context so the human team\n * (Tony) plus the agent collaborators (Claude, Kimi) can review and\n * fix them. Telemetry-gated like everything else: no data leaves the\n * machine unless `telemetry.enabled === true`.\n *\n * Each report is:\n * • appended to `~/.titan/bug-reports.jsonl` (always, when capture is wired)\n * • forwarded to PostHog as a `bug_report` event when telemetry is on\n * • exposed at `GET /api/bug-reports` for review\n *\n * Stack traces are scrubbed by the existing `outboundSanitizer`\n * before they leave the machine. The local file is the un-scrubbed\n * source of truth for the operator.\n */\nimport { existsSync, mkdirSync, readFileSync, appendFileSync, statSync, renameSync } from 'fs';\nimport { join } from 'path';\nimport { TITAN_HOME } from '../utils/constants.js';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'BugReports';\n\nconst BUG_REPORTS_DIR = TITAN_HOME;\nconst BUG_REPORTS_PATH = join(BUG_REPORTS_DIR, 'bug-reports.jsonl');\nconst BUG_REPORTS_PREVIOUS = join(BUG_REPORTS_DIR, 'bug-reports.previous.jsonl');\nconst MAX_FILE_BYTES = 5 * 1024 * 1024; // 5 MB before rotation\nconst MAX_REPORTS_RETURNED = 200;\n\nexport interface BugReportContext {\n /** Active session id, if any */\n sessionId?: string;\n /** Channel where the work originated (webchat, voice, telegram, etc.) */\n channel?: string;\n /** Resolved model id at time of failure */\n model?: string;\n /** Last user message, truncated to 240 chars */\n lastUserMessage?: string;\n /** Last assistant content preview, truncated to 240 chars */\n lastAssistantPreview?: string;\n /** Up to last 5 tool names called this turn */\n toolsUsed?: string[];\n /** System prompt token estimate at failure */\n promptLength?: number;\n /** 1-indexed turn within the session */\n turnNumber?: number;\n /** Free-form tag for callers (e.g. 'agent.processMessage', 'gateway.api') */\n origin?: string;\n}\n\nexport interface BugReport {\n /** Stable id for cross-system reference (`bug_<ts>_<rand>`) */\n id: string;\n /** ISO timestamp when capture fired */\n ts: string;\n /** TITAN runtime version */\n version: string;\n /** Anonymous install id from mesh identity */\n installId: string;\n /** Error name, message, stack — stack truncated to 4 KB */\n error: {\n name: string;\n message: string;\n stack: string;\n };\n /** Caller-provided agent/turn context */\n context: BugReportContext;\n /** Bucketed system info — never the exact CPU/GPU model */\n system: {\n os: string;\n arch: string;\n nodeMajor: number;\n ramGB?: number;\n gpuVramGB?: number;\n };\n}\n\nlet lastBugReportTs = 0;\nconst MIN_INTERVAL_MS = 250; // burst guard\n\nfunction ensureDir(): void {\n if (!existsSync(BUG_REPORTS_DIR)) {\n mkdirSync(BUG_REPORTS_DIR, { recursive: true });\n }\n}\n\nfunction rotateIfNeeded(): void {\n try {\n if (!existsSync(BUG_REPORTS_PATH)) return;\n const size = statSync(BUG_REPORTS_PATH).size;\n if (size < MAX_FILE_BYTES) return;\n renameSync(BUG_REPORTS_PATH, BUG_REPORTS_PREVIOUS);\n } catch {\n /* non-fatal */\n }\n}\n\nfunction safeStack(err: Error): string {\n const stack = err.stack || `${err.name}: ${err.message}`;\n return stack.length > 4096 ? stack.slice(0, 4096) + '\\n…(truncated)' : stack;\n}\n\nfunction bucketGB(mb?: number, step = 4): number | undefined {\n if (typeof mb !== 'number' || !Number.isFinite(mb) || mb <= 0) return undefined;\n const gb = mb / 1024;\n return Math.round(gb / step) * step;\n}\n\nasync function buildSystemSummary(): Promise<BugReport['system']> {\n const { platform, arch, totalmem } = await import('os');\n let gpuVramMB: number | undefined;\n try {\n const { detectHardware } = await import('../hardware/autoConfig.js');\n const hw = await detectHardware();\n gpuVramMB = hw.gpuVramMB;\n } catch { /* ignore */ }\n const nodeMajor = parseInt((process.version.match(/^v(\\d+)/) || ['', '0'])[1] || '0', 10);\n return {\n os: platform(),\n arch: arch(),\n nodeMajor,\n ramGB: bucketGB(totalmem() / (1024 * 1024)),\n gpuVramGB: bucketGB(gpuVramMB),\n };\n}\n\nfunction newReportId(): string {\n return `bug_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n}\n\n/**\n * Capture an error with surrounding agent context. Best-effort: never\n * throws back to the caller — if persistence or telemetry fails, the\n * original error path stays intact.\n */\nexport async function captureBugReport(err: unknown, context: BugReportContext = {}): Promise<BugReport | null> {\n try {\n // Burst guard: drop reports that fire within 250ms of each other\n // to avoid overwhelming disk + PostHog when an error loops.\n const now = Date.now();\n if (now - lastBugReportTs < MIN_INTERVAL_MS) return null;\n lastBugReportTs = now;\n\n const error: Error = err instanceof Error ? err : new Error(String(err ?? 'unknown error'));\n const { TITAN_VERSION } = await import('../utils/constants.js');\n const { getOrCreateNodeId } = await import('../mesh/identity.js');\n\n // Trim PII-prone fields defensively. Operators see the full thing\n // in the local file but PostHog never gets a long user message.\n const ctx: BugReportContext = {\n ...context,\n lastUserMessage: context.lastUserMessage?.slice(0, 240),\n lastAssistantPreview: context.lastAssistantPreview?.slice(0, 240),\n toolsUsed: context.toolsUsed?.slice(0, 5),\n };\n\n const report: BugReport = {\n id: newReportId(),\n ts: new Date().toISOString(),\n version: TITAN_VERSION,\n installId: getOrCreateNodeId(),\n error: {\n name: error.name,\n message: error.message,\n stack: safeStack(error),\n },\n context: ctx,\n system: await buildSystemSummary(),\n };\n\n // Persist locally first — operator-readable record of truth.\n try {\n ensureDir();\n rotateIfNeeded();\n appendFileSync(BUG_REPORTS_PATH, JSON.stringify(report) + '\\n');\n } catch (writeErr) {\n logger.warn(COMPONENT, `Local persist failed: ${(writeErr as Error).message}`);\n }\n\n // Forward to PostHog if telemetry is on. featureTracker handles\n // the opt-in gate, sanitizer, and best-effort fetch.\n // v5.5.10: event renamed from 'bug_report' to 'error' so it lands\n // under the obvious name in PostHog Events. Operator (Tony) was\n // searching for 'error' events and finding nothing because they\n // were all under 'bug_report'.\n try {\n const { trackEvent } = await import('./featureTracker.js');\n await trackEvent('error', {\n bug_id: report.id,\n error_name: report.error.name,\n error_message: report.error.message,\n origin: report.context.origin,\n model: report.context.model,\n channel: report.context.channel,\n tools_used: report.context.toolsUsed,\n prompt_length: report.context.promptLength,\n turn_number: report.context.turnNumber,\n os: report.system.os,\n arch: report.system.arch,\n node_major: report.system.nodeMajor,\n ram_gb: report.system.ramGB,\n gpu_vram_gb: report.system.gpuVramGB,\n titan_version: report.version,\n stack_preview: report.error.stack.slice(0, 800),\n });\n } catch (sendErr) {\n logger.debug(COMPONENT, `Remote send skipped: ${(sendErr as Error).message}`);\n }\n\n return report;\n } catch (selfErr) {\n // Never let bug-report capture itself become a bug.\n try {\n logger.warn(COMPONENT, `captureBugReport itself failed: ${(selfErr as Error).message}`);\n } catch { /* deeply broken */ }\n return null;\n }\n}\n\n/**\n * List recent bug reports for review. Reads the local jsonl file —\n * never PostHog — so the operator can review even when telemetry is\n * off. Returns most-recent first.\n */\nexport function listRecentBugReports(limit = 50): BugReport[] {\n try {\n if (!existsSync(BUG_REPORTS_PATH)) return [];\n const raw = readFileSync(BUG_REPORTS_PATH, 'utf-8');\n const lines = raw.split('\\n').filter(Boolean);\n const cap = Math.min(Math.max(1, limit), MAX_REPORTS_RETURNED);\n const tail = lines.slice(-cap).reverse();\n const reports: BugReport[] = [];\n for (const line of tail) {\n try {\n reports.push(JSON.parse(line));\n } catch { /* skip malformed */ }\n }\n return reports;\n } catch (err) {\n logger.warn(COMPONENT, `listRecentBugReports failed: ${(err as Error).message}`);\n return [];\n }\n}\n\n/** Lookup one report by id from the local file. */\nexport function getBugReport(id: string): BugReport | null {\n try {\n if (!existsSync(BUG_REPORTS_PATH)) return null;\n const raw = readFileSync(BUG_REPORTS_PATH, 'utf-8');\n for (const line of raw.split('\\n')) {\n if (!line) continue;\n try {\n const r = JSON.parse(line) as BugReport;\n if (r.id === id) return r;\n } catch { /* skip */ }\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/** File path so /api/bug-reports/raw can stream the operator file. */\nexport function getBugReportsPath(): string {\n return BUG_REPORTS_PATH;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -130,13 +130,21 @@ function trackSelfModPR(action, drive) {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
function trackBugReport(properties) {
|
|
133
|
-
trackEvent("
|
|
133
|
+
trackEvent("error", properties).catch(() => {
|
|
134
134
|
});
|
|
135
135
|
}
|
|
136
|
+
async function trackError(err, origin, context = {}) {
|
|
137
|
+
try {
|
|
138
|
+
const { captureBugReport } = await import("./bugReports.js");
|
|
139
|
+
await captureBugReport(err, { origin, ...context });
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
136
143
|
export {
|
|
137
144
|
endToolSession,
|
|
138
145
|
trackBugReport,
|
|
139
146
|
trackChannelChange,
|
|
147
|
+
trackError,
|
|
140
148
|
trackEvent,
|
|
141
149
|
trackFeatureToggle,
|
|
142
150
|
trackModelUsage,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/analytics/featureTracker.ts"],"sourcesContent":["/**\n * TITAN — Feature Usage Tracker\n * Fire one-off analytics events for product insights.\n * All events are anonymized, opt-in gated, and best-effort.\n */\nimport { getOrCreateNodeId } from '../mesh/identity.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { sendRemoteAnalytics } from './collector.js';\n\nconst COMPONENT = 'FeatureTracker';\n\n/** Fire a custom analytics event if telemetry is enabled. */\nexport async function trackEvent(\n eventName: string,\n properties: Record<string, unknown> = {}\n): Promise<void> {\n try {\n await sendRemoteAnalytics({\n type: eventName,\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n timestamp: new Date().toISOString(),\n ...properties,\n });\n } catch {\n // Best-effort — never block user flow for analytics\n }\n}\n\n/** Track which model was used for a completion. */\nexport function trackModelUsage(model: string, provider?: string, success = true, latencyMs?: number): void {\n trackEvent('model_usage', {\n model,\n provider: provider || model.split('/')[0] || 'unknown',\n success,\n latency_ms: latencyMs,\n }).catch(() => {});\n}\n\n// ── Per-session tool accumulator (cuts event volume 10-100x) ────────────────\ninterface ToolStats {\n count: number;\n totalLatencyMs: number;\n errorCount: number;\n}\n\nconst sessionToolAccumulators = new Map<string, Map<string, ToolStats>>();\n\n/** Accumulate a tool call for later per-session batch emit. */\nexport function trackToolCall(tool: string, success: boolean, latencyMs?: number, errorType?: string, sessionId?: string): void {\n const sid = sessionId || '__global__';\n let acc = sessionToolAccumulators.get(sid);\n if (!acc) {\n acc = new Map<string, ToolStats>();\n sessionToolAccumulators.set(sid, acc);\n }\n const existing = acc.get(tool);\n if (existing) {\n existing.count += 1;\n existing.totalLatencyMs += latencyMs ?? 0;\n if (!success) existing.errorCount += 1;\n } else {\n acc.set(tool, {\n count: 1,\n totalLatencyMs: latencyMs ?? 0,\n errorCount: success ? 0 : 1,\n });\n }\n}\n\n/** Emit accumulated tool_use_summary for a session and clear the buffer. */\nexport function endToolSession(sessionId: string): void {\n const acc = sessionToolAccumulators.get(sessionId);\n if (!acc || acc.size === 0) return;\n\n const tools: Record<string, { count: number; total_latency_ms: number; error_count: number }> = {};\n let totalCalls = 0;\n for (const [tool, stats] of acc) {\n tools[tool] = {\n count: stats.count,\n total_latency_ms: stats.totalLatencyMs,\n error_count: stats.errorCount,\n };\n totalCalls += stats.count;\n }\n\n trackEvent('tool_use_summary', {\n session_id: sessionId,\n tool_count: acc.size,\n total_calls: totalCalls,\n tools,\n }).catch(() => {});\n\n sessionToolAccumulators.delete(sessionId);\n}\n\n/** Track feature toggle changes. */\nexport function trackFeatureToggle(feature: string, enabled: boolean): void {\n trackEvent('feature_toggle', {\n feature,\n enabled,\n }).catch(() => {});\n}\n\n/** Track session end metrics. Flushes any accumulated tool stats. */\nexport function trackSessionEnd(sessionId: string, messageCount: number, toolRounds: number, errorCount: number, durationMs?: number): void {\n endToolSession(sessionId);\n trackEvent('session_end', {\n session_id: sessionId,\n message_count: messageCount,\n tool_rounds: toolRounds,\n error_count: errorCount,\n duration_ms: durationMs,\n }).catch(() => {});\n}\n\n/** Track provider configuration changes (no keys, just counts). */\nexport function trackProviderChange(provider: string, action: 'added' | 'removed' | 'rotated'): void {\n trackEvent('provider_change', {\n provider,\n action,\n }).catch(() => {});\n}\n\n/** Track channel adapter enable/disable. */\nexport function trackChannelChange(channel: string, enabled: boolean): void {\n trackEvent('channel_change', {\n channel,\n enabled,\n }).catch(() => {});\n}\n\n/** Track update check results. */\nexport function trackUpdateCheck(currentVersion: string, latestVersion: string, isBehind: boolean): void {\n trackEvent('update_check', {\n current_version: currentVersion,\n latest_version: latestVersion,\n is_behind: isBehind,\n }).catch(() => {});\n}\n\n/** Track when a user completes onboarding. */\nexport function trackOnboardingComplete(steps: number, durationMs?: number): void {\n trackEvent('onboarding_complete', {\n steps,\n duration_ms: durationMs,\n }).catch(() => {});\n}\n\n/** Track when Soma fires a proposal. */\nexport function trackSomaProposal(drive: string, pressure: number, approved: boolean): void {\n trackEvent('soma_proposal', {\n drive,\n pressure,\n approved,\n }).catch(() => {});\n}\n\n/** Track self-modification PR lifecycle. */\nexport function trackSelfModPR(action: 'created' | 'approved' | 'merged' | 'rejected', drive?: string): void {\n trackEvent('self_mod_pr', {\n action,\n drive: drive || 'unknown',\n }).catch(() => {});\n}\n\n/** Track a bug report with rich context. */\nexport function trackBugReport(properties: {\n bug_id: string;\n error_name: string;\n error_message: string;\n origin?: string;\n model?: string;\n channel?: string;\n tools_used?: string[];\n prompt_length?: number;\n turn_number?: number;\n os?: string;\n arch?: string;\n node_major?: number;\n ram_gb?: number;\n gpu_vram_gb?: number;\n stack_preview?: string;\n}): void {\n trackEvent('
|
|
1
|
+
{"version":3,"sources":["../../src/analytics/featureTracker.ts"],"sourcesContent":["/**\n * TITAN — Feature Usage Tracker\n * Fire one-off analytics events for product insights.\n * All events are anonymized, opt-in gated, and best-effort.\n */\nimport { getOrCreateNodeId } from '../mesh/identity.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { sendRemoteAnalytics } from './collector.js';\n\nconst COMPONENT = 'FeatureTracker';\n\n/** Fire a custom analytics event if telemetry is enabled. */\nexport async function trackEvent(\n eventName: string,\n properties: Record<string, unknown> = {}\n): Promise<void> {\n try {\n await sendRemoteAnalytics({\n type: eventName,\n installId: getOrCreateNodeId(),\n version: TITAN_VERSION,\n timestamp: new Date().toISOString(),\n ...properties,\n });\n } catch {\n // Best-effort — never block user flow for analytics\n }\n}\n\n/** Track which model was used for a completion. */\nexport function trackModelUsage(model: string, provider?: string, success = true, latencyMs?: number): void {\n trackEvent('model_usage', {\n model,\n provider: provider || model.split('/')[0] || 'unknown',\n success,\n latency_ms: latencyMs,\n }).catch(() => {});\n}\n\n// ── Per-session tool accumulator (cuts event volume 10-100x) ────────────────\ninterface ToolStats {\n count: number;\n totalLatencyMs: number;\n errorCount: number;\n}\n\nconst sessionToolAccumulators = new Map<string, Map<string, ToolStats>>();\n\n/** Accumulate a tool call for later per-session batch emit. */\nexport function trackToolCall(tool: string, success: boolean, latencyMs?: number, errorType?: string, sessionId?: string): void {\n const sid = sessionId || '__global__';\n let acc = sessionToolAccumulators.get(sid);\n if (!acc) {\n acc = new Map<string, ToolStats>();\n sessionToolAccumulators.set(sid, acc);\n }\n const existing = acc.get(tool);\n if (existing) {\n existing.count += 1;\n existing.totalLatencyMs += latencyMs ?? 0;\n if (!success) existing.errorCount += 1;\n } else {\n acc.set(tool, {\n count: 1,\n totalLatencyMs: latencyMs ?? 0,\n errorCount: success ? 0 : 1,\n });\n }\n}\n\n/** Emit accumulated tool_use_summary for a session and clear the buffer. */\nexport function endToolSession(sessionId: string): void {\n const acc = sessionToolAccumulators.get(sessionId);\n if (!acc || acc.size === 0) return;\n\n const tools: Record<string, { count: number; total_latency_ms: number; error_count: number }> = {};\n let totalCalls = 0;\n for (const [tool, stats] of acc) {\n tools[tool] = {\n count: stats.count,\n total_latency_ms: stats.totalLatencyMs,\n error_count: stats.errorCount,\n };\n totalCalls += stats.count;\n }\n\n trackEvent('tool_use_summary', {\n session_id: sessionId,\n tool_count: acc.size,\n total_calls: totalCalls,\n tools,\n }).catch(() => {});\n\n sessionToolAccumulators.delete(sessionId);\n}\n\n/** Track feature toggle changes. */\nexport function trackFeatureToggle(feature: string, enabled: boolean): void {\n trackEvent('feature_toggle', {\n feature,\n enabled,\n }).catch(() => {});\n}\n\n/** Track session end metrics. Flushes any accumulated tool stats. */\nexport function trackSessionEnd(sessionId: string, messageCount: number, toolRounds: number, errorCount: number, durationMs?: number): void {\n endToolSession(sessionId);\n trackEvent('session_end', {\n session_id: sessionId,\n message_count: messageCount,\n tool_rounds: toolRounds,\n error_count: errorCount,\n duration_ms: durationMs,\n }).catch(() => {});\n}\n\n/** Track provider configuration changes (no keys, just counts). */\nexport function trackProviderChange(provider: string, action: 'added' | 'removed' | 'rotated'): void {\n trackEvent('provider_change', {\n provider,\n action,\n }).catch(() => {});\n}\n\n/** Track channel adapter enable/disable. */\nexport function trackChannelChange(channel: string, enabled: boolean): void {\n trackEvent('channel_change', {\n channel,\n enabled,\n }).catch(() => {});\n}\n\n/** Track update check results. */\nexport function trackUpdateCheck(currentVersion: string, latestVersion: string, isBehind: boolean): void {\n trackEvent('update_check', {\n current_version: currentVersion,\n latest_version: latestVersion,\n is_behind: isBehind,\n }).catch(() => {});\n}\n\n/** Track when a user completes onboarding. */\nexport function trackOnboardingComplete(steps: number, durationMs?: number): void {\n trackEvent('onboarding_complete', {\n steps,\n duration_ms: durationMs,\n }).catch(() => {});\n}\n\n/** Track when Soma fires a proposal. */\nexport function trackSomaProposal(drive: string, pressure: number, approved: boolean): void {\n trackEvent('soma_proposal', {\n drive,\n pressure,\n approved,\n }).catch(() => {});\n}\n\n/** Track self-modification PR lifecycle. */\nexport function trackSelfModPR(action: 'created' | 'approved' | 'merged' | 'rejected', drive?: string): void {\n trackEvent('self_mod_pr', {\n action,\n drive: drive || 'unknown',\n }).catch(() => {});\n}\n\n/** Track a bug report with rich context.\n * v5.5.10: now fires as 'error' event so it surfaces under the obvious name\n * in PostHog. */\nexport function trackBugReport(properties: {\n bug_id: string;\n error_name: string;\n error_message: string;\n origin?: string;\n model?: string;\n channel?: string;\n tools_used?: string[];\n prompt_length?: number;\n turn_number?: number;\n os?: string;\n arch?: string;\n node_major?: number;\n ram_gb?: number;\n gpu_vram_gb?: number;\n stack_preview?: string;\n}): void {\n trackEvent('error', properties).catch(() => {});\n}\n\n/**\n * v5.5.10: Convenience wrapper that captures an error to bug-reports.jsonl\n * AND fires the 'error' event to PostHog. Use this instead of bare\n * `logger.error(err)` at error sites the operator wants visibility into.\n *\n * Best-effort: never throws back to caller. Burst-guarded by captureBugReport\n * itself (250ms).\n *\n * Example:\n * try { await riskyThing(); }\n * catch (err) {\n * trackError(err, 'router.chat', { model: activeModel });\n * throw err; // or fallback path\n * }\n */\nexport async function trackError(\n err: unknown,\n origin: string,\n context: Record<string, unknown> = {},\n): Promise<void> {\n try {\n const { captureBugReport } = await import('./bugReports.js');\n await captureBugReport(err, { origin, ...context });\n } catch {\n // Never let error tracking become a new error path\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/utils/constants.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { homedir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
const TITAN_VERSION = "5.5.
|
|
4
|
+
const TITAN_VERSION = "5.5.10";
|
|
5
5
|
const TITAN_CODENAME = "Spacewalk";
|
|
6
6
|
const TITAN_NAME = "TITAN";
|
|
7
7
|
const TITAN_FULL_NAME = "The Intelligent Task Automation Network";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '5.5.
|
|
1
|
+
{"version":3,"sources":["../../src/utils/constants.ts"],"sourcesContent":["/**\n * TITAN Constants\n */\nimport { homedir } from 'os';\nimport { join } from 'path';\n\nexport const TITAN_VERSION = '5.5.10';\nexport const TITAN_CODENAME = 'Spacewalk';\nexport const TITAN_NAME = 'TITAN';\nexport const TITAN_FULL_NAME = 'The Intelligent Task Automation Network';\nexport const TITAN_ASCII_LOGO = `\n╔══════════════════════════════════════════════════════╗\n║ ║\n║ ████████╗██╗████████╗ █████╗ ███╗ ██╗ ║\n║ ██║ ██║ ██║ ██╔══██╗████╗ ██║ ║\n║ ██║ ██║ ██║ ███████║██╔██╗ ██║ ║\n║ ██║ ██║ ██║ ██╔══██║██║╚██╗██║ ║\n║ ██║ ██║ ██║ ██║ ██║██║ ╚████║ ║\n║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝ ║\n║ ║\n║ The Intelligent Task Automation Network ║\n║ v${TITAN_VERSION} • by Tony Elliott ║\n╚══════════════════════════════════════════════════════╝`;\n\n// Paths\n// Hunt Finding #03 (2026-04-14): honor TITAN_HOME env var if set.\n// Previously this was hardcoded to `~/.titan`, which meant:\n// - Docker containers couldn't override the config path\n// - Shared machines couldn't isolate per-user state\n// - Test fixtures couldn't run against an isolated home\n// - The systemd unit's `Environment=TITAN_HOME=...` was silently ignored\n// The env var is read once at module load (constants are resolved at import time).\n// If TITAN_HOME starts with `~/`, expand it to the user's home dir.\nfunction resolveTitanHome(): string {\n const envHome = process.env.TITAN_HOME;\n if (envHome && envHome.trim().length > 0) {\n const trimmed = envHome.trim();\n if (trimmed.startsWith('~/')) {\n return join(homedir(), trimmed.slice(2));\n }\n if (trimmed === '~') {\n return homedir();\n }\n return trimmed;\n }\n return join(homedir(), '.titan');\n}\nexport const TITAN_HOME = resolveTitanHome();\nexport const TITAN_CONFIG_PATH = join(TITAN_HOME, 'titan.json');\nexport const TITAN_DB_PATH = join(TITAN_HOME, 'titan.db');\nexport const TITAN_WORKSPACE = join(TITAN_HOME, 'workspace');\nexport const TITAN_SKILLS_DIR = join(TITAN_WORKSPACE, 'skills');\nexport const TITAN_LOGS_DIR = join(TITAN_HOME, 'logs');\nexport const TITAN_MEMORY_DIR = join(TITAN_HOME, 'memory');\n\n// Workspace prompt files (injected into agent context)\nexport const AGENTS_MD = join(TITAN_WORKSPACE, 'AGENTS.md');\nexport const SOUL_MD = join(TITAN_WORKSPACE, 'SOUL.md');\nexport const TOOLS_MD = join(TITAN_WORKSPACE, 'TOOLS.md');\nexport const TITAN_MD_FILENAME = 'TITAN.md';\nexport const AUTOPILOT_MD = join(TITAN_HOME, 'AUTOPILOT.md');\nexport const AUTOPILOT_RUNS_PATH = join(TITAN_HOME, 'autopilot-runs.jsonl');\nexport const TITAN_CREDENTIALS_DIR = join(TITAN_HOME, 'credentials');\n\n// Income & lead tracking\nexport const INCOME_LEDGER_PATH = join(TITAN_HOME, 'income-ledger.jsonl');\nexport const FREELANCE_LEADS_PATH = join(TITAN_HOME, 'freelance-leads.jsonl');\nexport const FREELANCE_PROFILE_PATH = join(TITAN_HOME, 'freelance-profile.json');\nexport const LEADS_PATH = join(TITAN_HOME, 'leads.jsonl');\nexport const TELEMETRY_EVENTS_PATH = join(TITAN_HOME, 'telemetry-events.jsonl');\nexport const SOMADRIVE_STATE_PATH = join(TITAN_HOME, 'soma-drive-state.json');\nexport const ACTIVITY_LOG_PATH = join(TITAN_HOME, 'activity-log.jsonl');\n\n// Gateway defaults\nexport const DEFAULT_GATEWAY_HOST = '0.0.0.0';\nexport const DEFAULT_GATEWAY_PORT = 48420;\nexport const DEFAULT_WEB_PORT = 48421;\n\n// Agent defaults\nexport const DEFAULT_MODEL = 'anthropic/claude-sonnet-4-20250514';\n/** v5.4.1: User-preference ceiling. Providers clamp per-model via\n * clampMaxTokens() so this can be high without causing 400s on\n * capped endpoints (e.g. Claude Sonnet 4 8K, Cohere 4K). */\nexport const DEFAULT_MAX_TOKENS = 200000;\nexport const DEFAULT_TEMPERATURE = 0.7;\nexport const MAX_CONTEXT_MESSAGES = 50;\nexport const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes\n\n// Security\nexport const DEFAULT_SANDBOX_MODE = 'host';\n/** Default allowed tools. Empty = allow ALL registered tools.\n * Use security.deniedTools to block specific tools instead. */\nexport const ALLOWED_TOOLS_DEFAULT: string[] = [];\nexport const DENIED_TOOLS_DEFAULT: string[] = [];\n"],"mappings":";AAGA,SAAS,eAAe;AACxB,SAAS,YAAY;AAEd,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,aAAa;AACnB,MAAM,kBAAkB;AACxB,MAAM,mBAAmB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,WAW1B,aAAa;AAAA;AAYnB,SAAS,mBAA2B;AAChC,QAAM,UAAU,QAAQ,IAAI;AAC5B,MAAI,WAAW,QAAQ,KAAK,EAAE,SAAS,GAAG;AACtC,UAAM,UAAU,QAAQ,KAAK;AAC7B,QAAI,QAAQ,WAAW,IAAI,GAAG;AAC1B,aAAO,KAAK,QAAQ,GAAG,QAAQ,MAAM,CAAC,CAAC;AAAA,IAC3C;AACA,QAAI,YAAY,KAAK;AACjB,aAAO,QAAQ;AAAA,IACnB;AACA,WAAO;AAAA,EACX;AACA,SAAO,KAAK,QAAQ,GAAG,QAAQ;AACnC;AACO,MAAM,aAAa,iBAAiB;AACpC,MAAM,oBAAoB,KAAK,YAAY,YAAY;AACvD,MAAM,gBAAgB,KAAK,YAAY,UAAU;AACjD,MAAM,kBAAkB,KAAK,YAAY,WAAW;AACpD,MAAM,mBAAmB,KAAK,iBAAiB,QAAQ;AACvD,MAAM,iBAAiB,KAAK,YAAY,MAAM;AAC9C,MAAM,mBAAmB,KAAK,YAAY,QAAQ;AAGlD,MAAM,YAAY,KAAK,iBAAiB,WAAW;AACnD,MAAM,UAAU,KAAK,iBAAiB,SAAS;AAC/C,MAAM,WAAW,KAAK,iBAAiB,UAAU;AACjD,MAAM,oBAAoB;AAC1B,MAAM,eAAe,KAAK,YAAY,cAAc;AACpD,MAAM,sBAAsB,KAAK,YAAY,sBAAsB;AACnE,MAAM,wBAAwB,KAAK,YAAY,aAAa;AAG5D,MAAM,qBAAqB,KAAK,YAAY,qBAAqB;AACjE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,yBAAyB,KAAK,YAAY,wBAAwB;AACxE,MAAM,aAAa,KAAK,YAAY,aAAa;AACjD,MAAM,wBAAwB,KAAK,YAAY,wBAAwB;AACvE,MAAM,uBAAuB,KAAK,YAAY,uBAAuB;AACrE,MAAM,oBAAoB,KAAK,YAAY,oBAAoB;AAG/D,MAAM,uBAAuB;AAC7B,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAGzB,MAAM,gBAAgB;AAItB,MAAM,qBAAqB;AAC3B,MAAM,sBAAsB;AAC5B,MAAM,uBAAuB;AAC7B,MAAM,qBAAqB,KAAK,KAAK;AAGrC,MAAM,uBAAuB;AAG7B,MAAM,wBAAkC,CAAC;AACzC,MAAM,uBAAiC,CAAC;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "titan-agent",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.10",
|
|
4
4
|
"description": "TITAN — Autonomous AI agent framework with self-improvement, multi-agent orchestration, 36 LLM providers, 16 channel adapters, GPU VRAM management, mesh networking, LiveKit voice, TITAN-Soma homeostatic drives, and a React Mission Control dashboard. Open-source, TypeScript, MIT licensed.",
|
|
5
5
|
"author": "Tony Elliott (https://github.com/Djtony707)",
|
|
6
6
|
"repository": {
|