mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-03 15:43:11 +08:00
Add benchmark results and tests for LSP graph builder and staged search
- Introduced a new benchmark results file for performance comparison on 2026-02-09. - Added a test for LspGraphBuilder to ensure it does not expand nodes at maximum depth. - Created a test for the staged search pipeline to validate fallback behavior when stage 1 returns empty results.
This commit is contained in:
39
ccw/src/core/services/cli-session-audit.ts
Normal file
39
ccw/src/core/services/cli-session-audit.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { existsSync, mkdirSync, appendFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
export type CliSessionAuditEventType =
|
||||
| 'session_created'
|
||||
| 'session_closed'
|
||||
| 'session_send'
|
||||
| 'session_execute'
|
||||
| 'session_resize'
|
||||
| 'session_share_created'
|
||||
| 'session_idle_reaped';
|
||||
|
||||
export interface CliSessionAuditEvent {
|
||||
type: CliSessionAuditEventType;
|
||||
timestamp: string;
|
||||
projectRoot: string;
|
||||
sessionKey?: string;
|
||||
tool?: string;
|
||||
resumeKey?: string;
|
||||
workingDir?: string;
|
||||
ip?: string;
|
||||
userAgent?: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function auditFilePath(projectRoot: string): string {
|
||||
return path.join(projectRoot, '.workflow', 'audit', 'cli-sessions.jsonl');
|
||||
}
|
||||
|
||||
export function appendCliSessionAudit(event: CliSessionAuditEvent): void {
|
||||
try {
|
||||
const filePath = auditFilePath(event.projectRoot);
|
||||
const dir = path.dirname(filePath);
|
||||
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
||||
appendFileSync(filePath, JSON.stringify(event) + '\n', { encoding: 'utf8' });
|
||||
} catch {
|
||||
// Best-effort: never fail API requests due to audit write errors.
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type CliSessionResumeStrategy
|
||||
} from './cli-session-command-builder.js';
|
||||
import { getCliSessionPolicy } from './cli-session-policy.js';
|
||||
import { appendCliSessionAudit } from './cli-session-audit.js';
|
||||
|
||||
export interface CliSession {
|
||||
sessionKey: string;
|
||||
@@ -147,10 +148,29 @@ export class CliSessionManager {
|
||||
private projectRoot: string;
|
||||
private emitter = new EventEmitter();
|
||||
private maxBufferBytes: number;
|
||||
private idleTimeoutMs: number;
|
||||
private reaperTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(projectRoot: string) {
|
||||
this.projectRoot = projectRoot;
|
||||
this.maxBufferBytes = getCliSessionPolicy().maxBufferBytes;
|
||||
const policy = getCliSessionPolicy();
|
||||
this.maxBufferBytes = policy.maxBufferBytes;
|
||||
this.idleTimeoutMs = policy.idleTimeoutMs;
|
||||
|
||||
if (this.idleTimeoutMs > 0) {
|
||||
this.reaperTimer = setInterval(() => {
|
||||
const reaped = this.closeIdleSessions(this.idleTimeoutMs);
|
||||
for (const sessionKey of reaped) {
|
||||
appendCliSessionAudit({
|
||||
type: 'session_idle_reaped',
|
||||
timestamp: nowIso(),
|
||||
projectRoot: this.projectRoot,
|
||||
sessionKey,
|
||||
});
|
||||
}
|
||||
}, 60_000);
|
||||
this.reaperTimer.unref?.();
|
||||
}
|
||||
}
|
||||
|
||||
listSessions(): CliSession[] {
|
||||
@@ -354,14 +374,14 @@ export class CliSessionManager {
|
||||
return () => this.emitter.off('output', handler);
|
||||
}
|
||||
|
||||
closeIdleSessions(idleTimeoutMs: number): number {
|
||||
if (idleTimeoutMs <= 0) return 0;
|
||||
closeIdleSessions(idleTimeoutMs: number): string[] {
|
||||
if (idleTimeoutMs <= 0) return [];
|
||||
const now = Date.now();
|
||||
let closed = 0;
|
||||
const closed: string[] = [];
|
||||
for (const s of this.sessions.values()) {
|
||||
if (now - s.lastActivityAt >= idleTimeoutMs) {
|
||||
this.close(s.sessionKey);
|
||||
closed += 1;
|
||||
closed.push(s.sessionKey);
|
||||
}
|
||||
}
|
||||
return closed;
|
||||
|
||||
83
ccw/src/core/services/cli-session-share.ts
Normal file
83
ccw/src/core/services/cli-session-share.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export type CliSessionShareMode = 'read' | 'write';
|
||||
|
||||
export interface CliSessionShareTokenRecord {
|
||||
token: string;
|
||||
sessionKey: string;
|
||||
projectRoot: string;
|
||||
mode: CliSessionShareMode;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface InternalTokenRecord extends CliSessionShareTokenRecord {
|
||||
expiresAtMs: number;
|
||||
}
|
||||
|
||||
function createTokenValue(): string {
|
||||
// 32 bytes => 43 chars base64url (approx), safe for URLs.
|
||||
return randomBytes(32).toString('base64url');
|
||||
}
|
||||
|
||||
export class CliSessionShareManager {
|
||||
private tokens = new Map<string, InternalTokenRecord>();
|
||||
|
||||
createToken(input: {
|
||||
sessionKey: string;
|
||||
projectRoot: string;
|
||||
mode: CliSessionShareMode;
|
||||
ttlMs?: number;
|
||||
}): CliSessionShareTokenRecord {
|
||||
const ttlMs = typeof input.ttlMs === 'number' ? Math.max(1_000, input.ttlMs) : 24 * 60 * 60_000;
|
||||
const expiresAtMs = Date.now() + ttlMs;
|
||||
const record: InternalTokenRecord = {
|
||||
token: createTokenValue(),
|
||||
sessionKey: input.sessionKey,
|
||||
projectRoot: input.projectRoot,
|
||||
mode: input.mode,
|
||||
expiresAt: new Date(expiresAtMs).toISOString(),
|
||||
expiresAtMs,
|
||||
};
|
||||
this.tokens.set(record.token, record);
|
||||
return record;
|
||||
}
|
||||
|
||||
validateToken(token: string, sessionKey: string): CliSessionShareTokenRecord | null {
|
||||
const record = this.tokens.get(token);
|
||||
if (!record) return null;
|
||||
if (record.sessionKey !== sessionKey) return null;
|
||||
if (Date.now() >= record.expiresAtMs) {
|
||||
this.tokens.delete(token);
|
||||
return null;
|
||||
}
|
||||
const { expiresAtMs: _expiresAtMs, ...publicRecord } = record;
|
||||
return publicRecord;
|
||||
}
|
||||
|
||||
revokeToken(token: string): boolean {
|
||||
return this.tokens.delete(token);
|
||||
}
|
||||
|
||||
cleanupExpired(): number {
|
||||
const now = Date.now();
|
||||
let removed = 0;
|
||||
for (const [token, record] of this.tokens) {
|
||||
if (now >= record.expiresAtMs) {
|
||||
this.tokens.delete(token);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
let singleton: CliSessionShareManager | null = null;
|
||||
|
||||
export function getCliSessionShareManager(): CliSessionShareManager {
|
||||
if (!singleton) singleton = new CliSessionShareManager();
|
||||
return singleton;
|
||||
}
|
||||
|
||||
export function describeShareAuthFailure(): { error: string; status: number } {
|
||||
return { error: 'Invalid or expired share token', status: 403 };
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export class NodeRunner {
|
||||
};
|
||||
}
|
||||
|
||||
const manager = getCliSessionManager(process.cwd());
|
||||
const manager = getCliSessionManager(this.context.workingDir || process.cwd());
|
||||
const routed = manager.execute(targetSessionKey, {
|
||||
tool,
|
||||
prompt: instruction,
|
||||
|
||||
Reference in New Issue
Block a user