@esotech/contextuate 2.0.0 → 2.1.0
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.
- package/README.md +169 -1
- package/dist/commands/claude.d.ts +21 -0
- package/dist/commands/claude.js +213 -0
- package/dist/commands/context.d.ts +1 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/index.d.ts +4 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.js +67 -6
- package/dist/commands/install.d.ts +28 -0
- package/dist/commands/install.js +116 -11
- package/dist/commands/monitor.d.ts +55 -0
- package/dist/commands/monitor.js +1007 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/run.d.ts +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +113 -1
- package/dist/monitor/daemon/circuit-breaker.d.ts +121 -0
- package/dist/monitor/daemon/circuit-breaker.js +552 -0
- package/dist/monitor/daemon/cli.d.ts +8 -0
- package/dist/monitor/daemon/cli.js +82 -0
- package/dist/monitor/daemon/index.d.ts +137 -0
- package/dist/monitor/daemon/index.js +695 -0
- package/dist/monitor/daemon/notifier.d.ts +25 -0
- package/dist/monitor/daemon/notifier.js +98 -0
- package/dist/monitor/daemon/processor.d.ts +89 -0
- package/dist/monitor/daemon/processor.js +455 -0
- package/dist/monitor/daemon/state.d.ts +80 -0
- package/dist/monitor/daemon/state.js +162 -0
- package/dist/monitor/daemon/watcher.d.ts +47 -0
- package/dist/monitor/daemon/watcher.js +171 -0
- package/dist/monitor/daemon/wrapper-manager.d.ts +106 -0
- package/dist/monitor/daemon/wrapper-manager.js +374 -0
- package/dist/monitor/hooks/emit-event.js +652 -0
- package/dist/monitor/persistence/file-store.d.ts +88 -0
- package/dist/monitor/persistence/file-store.js +335 -0
- package/dist/monitor/persistence/index.d.ts +7 -0
- package/dist/monitor/persistence/index.js +10 -0
- package/dist/monitor/server/adapters/redis.d.ts +38 -0
- package/dist/monitor/server/adapters/redis.js +213 -0
- package/dist/monitor/server/adapters/unix-socket.d.ts +33 -0
- package/dist/monitor/server/adapters/unix-socket.js +182 -0
- package/dist/monitor/server/broker.d.ts +135 -0
- package/dist/monitor/server/broker.js +475 -0
- package/dist/monitor/server/cli.d.ts +8 -0
- package/dist/monitor/server/cli.js +98 -0
- package/dist/monitor/server/fastify.d.ts +16 -0
- package/dist/monitor/server/fastify.js +184 -0
- package/dist/monitor/server/index.d.ts +36 -0
- package/dist/monitor/server/index.js +153 -0
- package/dist/monitor/server/websocket.d.ts +80 -0
- package/dist/monitor/server/websocket.js +453 -0
- package/dist/monitor/ui/assets/index-4IssW9On.js +59 -0
- package/dist/monitor/ui/assets/index-vo9hLe5R.css +32 -0
- package/dist/monitor/ui/favicon.png +0 -0
- package/dist/monitor/ui/index.html +14 -0
- package/dist/monitor/ui/logo.png +0 -0
- package/dist/monitor/ui/logo.svg +1 -0
- package/dist/runtime/driver.d.ts +16 -0
- package/dist/runtime/tools.d.ts +10 -0
- package/dist/templates/README.md +33 -7
- package/dist/templates/agents/aegis.md +4 -0
- package/dist/templates/agents/archon.md +13 -22
- package/dist/templates/agents/atlas.md +4 -0
- package/dist/templates/agents/canvas.md +4 -0
- package/dist/templates/agents/chronicle.md +4 -0
- package/dist/templates/agents/chronos.md +4 -0
- package/dist/templates/agents/cipher.md +4 -0
- package/dist/templates/agents/crucible.md +4 -0
- package/dist/templates/agents/echo.md +4 -0
- package/dist/templates/agents/forge.md +4 -0
- package/dist/templates/agents/ledger.md +4 -0
- package/dist/templates/agents/meridian.md +4 -0
- package/dist/templates/agents/nexus.md +4 -0
- package/dist/templates/agents/pythia.md +217 -0
- package/dist/templates/agents/scribe.md +4 -0
- package/dist/templates/agents/sentinel.md +4 -0
- package/dist/templates/agents/{oracle.md → thoth.md} +11 -7
- package/dist/templates/agents/unity.md +4 -0
- package/dist/templates/agents/vox.md +4 -0
- package/dist/templates/agents/weaver.md +4 -0
- package/dist/templates/framework-agents/documentation-expert.md +3 -3
- package/dist/templates/framework-agents/tools-expert.md +8 -8
- package/dist/templates/skills/consult.md +138 -0
- package/dist/templates/skills/orchestrate.md +173 -0
- package/dist/templates/skills/pythia.md +37 -0
- package/dist/templates/standards/agent-roles.md +68 -21
- package/dist/templates/standards/coding-standards.md +9 -26
- package/dist/templates/templates/context.md +17 -2
- package/dist/templates/templates/contextuate.md +21 -28
- package/dist/templates/templates/standards/go.md +167 -0
- package/dist/templates/templates/standards/java.md +167 -0
- package/dist/templates/templates/standards/javascript.md +292 -0
- package/dist/templates/templates/standards/php.md +181 -0
- package/dist/templates/templates/standards/python.md +175 -0
- package/dist/templates/tools/agent-creator.md +252 -0
- package/dist/templates/tools/agent-creator.tool.md +2 -2
- package/dist/templates/tools/quickref.md +216 -0
- package/dist/templates/tools/spawn.md +31 -0
- package/dist/templates/tools/standards-detector.md +301 -0
- package/dist/templates/version.json +1 -1
- package/dist/types/monitor.d.ts +660 -0
- package/dist/types/monitor.js +75 -0
- package/dist/utils/git.d.ts +9 -0
- package/dist/utils/tokens.d.ts +10 -0
- package/package.json +18 -5
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notifier
|
|
3
|
+
*
|
|
4
|
+
* Sends notifications to the UI server via Unix socket and/or Redis.
|
|
5
|
+
* Uses fire-and-forget pattern for resilience.
|
|
6
|
+
*/
|
|
7
|
+
import { MonitorEvent, MonitorConfig, SessionMeta } from '../../types/monitor.js';
|
|
8
|
+
export type BroadcastCallback = (data: any) => void;
|
|
9
|
+
export declare class Notifier {
|
|
10
|
+
private config;
|
|
11
|
+
private broadcast;
|
|
12
|
+
constructor(config: MonitorConfig, broadcast: BroadcastCallback);
|
|
13
|
+
/**
|
|
14
|
+
* Notify UI server about a new event
|
|
15
|
+
*/
|
|
16
|
+
notify(event: MonitorEvent): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Notify UI server about a session update
|
|
19
|
+
*/
|
|
20
|
+
notifySessionUpdate(session: SessionMeta): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Send notification via Redis pub/sub
|
|
23
|
+
*/
|
|
24
|
+
private notifyRedis;
|
|
25
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Notifier
|
|
4
|
+
*
|
|
5
|
+
* Sends notifications to the UI server via Unix socket and/or Redis.
|
|
6
|
+
* Uses fire-and-forget pattern for resilience.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.Notifier = void 0;
|
|
43
|
+
class Notifier {
|
|
44
|
+
constructor(config, broadcast) {
|
|
45
|
+
this.config = config;
|
|
46
|
+
this.broadcast = broadcast;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Notify UI server about a new event
|
|
50
|
+
*/
|
|
51
|
+
async notify(event) {
|
|
52
|
+
// Broadcast to local UI clients via callback
|
|
53
|
+
this.broadcast({ type: 'event', event });
|
|
54
|
+
// If Redis mode, also publish for UI aggregation across machines
|
|
55
|
+
if (this.config.mode === 'redis' && this.config.redis) {
|
|
56
|
+
await this.notifyRedis({ type: 'event', event });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Notify UI server about a session update
|
|
61
|
+
*/
|
|
62
|
+
async notifySessionUpdate(session) {
|
|
63
|
+
const notification = {
|
|
64
|
+
type: 'session_update',
|
|
65
|
+
session,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
};
|
|
68
|
+
// Broadcast to local UI clients via callback
|
|
69
|
+
this.broadcast(notification);
|
|
70
|
+
// If Redis mode, also publish for UI aggregation
|
|
71
|
+
if (this.config.mode === 'redis' && this.config.redis) {
|
|
72
|
+
await this.notifyRedis(notification);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Send notification via Redis pub/sub
|
|
77
|
+
*/
|
|
78
|
+
async notifyRedis(data) {
|
|
79
|
+
// Redis notification for multi-machine UI aggregation
|
|
80
|
+
try {
|
|
81
|
+
const Redis = (await Promise.resolve().then(() => __importStar(require('ioredis')))).default;
|
|
82
|
+
const client = new Redis({
|
|
83
|
+
host: this.config.redis?.host || 'localhost',
|
|
84
|
+
port: this.config.redis?.port || 6379,
|
|
85
|
+
password: this.config.redis?.password || undefined,
|
|
86
|
+
lazyConnect: true,
|
|
87
|
+
maxRetriesPerRequest: 0,
|
|
88
|
+
});
|
|
89
|
+
await client.connect();
|
|
90
|
+
await client.publish(this.config.redis?.channel || 'contextuate:ui', JSON.stringify(data));
|
|
91
|
+
await client.quit();
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// Fire-and-forget, don't fail
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.Notifier = Notifier;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Processor
|
|
3
|
+
*
|
|
4
|
+
* Core event processing logic extracted from broker.ts.
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Session correlation (parent-child linking)
|
|
7
|
+
* - Subagent lifecycle tracking
|
|
8
|
+
* - Virtual session routing
|
|
9
|
+
* - Event persistence
|
|
10
|
+
*/
|
|
11
|
+
import { MonitorEvent } from '../../types/monitor.js';
|
|
12
|
+
import { StateManager } from './state.js';
|
|
13
|
+
import { Notifier } from './notifier.js';
|
|
14
|
+
import type { CircuitBreaker } from './circuit-breaker.js';
|
|
15
|
+
export declare class EventProcessor {
|
|
16
|
+
private state;
|
|
17
|
+
private notifier;
|
|
18
|
+
private sessions;
|
|
19
|
+
private circuitBreaker;
|
|
20
|
+
constructor(state: StateManager, notifier: Notifier);
|
|
21
|
+
/**
|
|
22
|
+
* Set the circuit breaker instance for health monitoring
|
|
23
|
+
*/
|
|
24
|
+
setCircuitBreaker(circuitBreaker: CircuitBreaker): void;
|
|
25
|
+
/**
|
|
26
|
+
* Load existing sessions from disk
|
|
27
|
+
*/
|
|
28
|
+
loadSessions(): Promise<void>;
|
|
29
|
+
private processedEventIds;
|
|
30
|
+
/**
|
|
31
|
+
* Process a single event
|
|
32
|
+
* @param event The event to process
|
|
33
|
+
* @param filepath The source file path (null if from socket)
|
|
34
|
+
*/
|
|
35
|
+
processEvent(event: MonitorEvent, filepath: string | null): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Find wrapper ID associated with a session (for circuit breaker)
|
|
38
|
+
* This is a placeholder - the daemon will provide better association
|
|
39
|
+
*/
|
|
40
|
+
private findWrapperForSession;
|
|
41
|
+
/**
|
|
42
|
+
* Handle SubagentStart event - create child session immediately
|
|
43
|
+
*/
|
|
44
|
+
private handleSubagentStart;
|
|
45
|
+
/**
|
|
46
|
+
* Generate a short unique ID for virtual sessions
|
|
47
|
+
*/
|
|
48
|
+
private generateVirtualSessionId;
|
|
49
|
+
/**
|
|
50
|
+
* Start tracking a subagent context when Task tool is called
|
|
51
|
+
*/
|
|
52
|
+
private startSubagentContext;
|
|
53
|
+
/**
|
|
54
|
+
* End the current subagent context
|
|
55
|
+
*/
|
|
56
|
+
private endSubagentContext;
|
|
57
|
+
/**
|
|
58
|
+
* Extract agent type from Task tool input
|
|
59
|
+
*/
|
|
60
|
+
private extractAgentType;
|
|
61
|
+
/**
|
|
62
|
+
* Track potential sub-agent spawns from Task tool calls
|
|
63
|
+
*/
|
|
64
|
+
private trackSubagentSpawn;
|
|
65
|
+
/**
|
|
66
|
+
* Update session state based on event
|
|
67
|
+
*/
|
|
68
|
+
private updateSession;
|
|
69
|
+
/**
|
|
70
|
+
* Try to correlate a new session with a pending sub-agent spawn
|
|
71
|
+
*/
|
|
72
|
+
private correlateParent;
|
|
73
|
+
/**
|
|
74
|
+
* Check if two working directories match (handles git worktrees)
|
|
75
|
+
*/
|
|
76
|
+
private directoriesMatch;
|
|
77
|
+
/**
|
|
78
|
+
* Persist session metadata to disk
|
|
79
|
+
*/
|
|
80
|
+
private persistSession;
|
|
81
|
+
/**
|
|
82
|
+
* Persist event to session's events.jsonl file
|
|
83
|
+
*/
|
|
84
|
+
private persistEvent;
|
|
85
|
+
/**
|
|
86
|
+
* Move processed file from raw/ to processed/
|
|
87
|
+
*/
|
|
88
|
+
private moveToProcessed;
|
|
89
|
+
}
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Event Processor
|
|
4
|
+
*
|
|
5
|
+
* Core event processing logic extracted from broker.ts.
|
|
6
|
+
* Handles:
|
|
7
|
+
* - Session correlation (parent-child linking)
|
|
8
|
+
* - Subagent lifecycle tracking
|
|
9
|
+
* - Virtual session routing
|
|
10
|
+
* - Event persistence
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.EventProcessor = void 0;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const monitor_js_1 = require("../../types/monitor.js");
|
|
50
|
+
const PATHS = (0, monitor_js_1.getDefaultMonitorPaths)();
|
|
51
|
+
const SUBAGENT_CORRELATION_WINDOW_MS = 30000;
|
|
52
|
+
class EventProcessor {
|
|
53
|
+
constructor(state, notifier) {
|
|
54
|
+
this.sessions = new Map();
|
|
55
|
+
this.circuitBreaker = null;
|
|
56
|
+
// Track processed event IDs to prevent duplicates (socket + file watcher)
|
|
57
|
+
this.processedEventIds = new Set();
|
|
58
|
+
this.state = state;
|
|
59
|
+
this.notifier = notifier;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Set the circuit breaker instance for health monitoring
|
|
63
|
+
*/
|
|
64
|
+
setCircuitBreaker(circuitBreaker) {
|
|
65
|
+
this.circuitBreaker = circuitBreaker;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load existing sessions from disk
|
|
69
|
+
*/
|
|
70
|
+
async loadSessions() {
|
|
71
|
+
try {
|
|
72
|
+
const sessionDirs = await fs.promises.readdir(PATHS.sessionsDir);
|
|
73
|
+
for (const dir of sessionDirs) {
|
|
74
|
+
const metaPath = path.join(PATHS.sessionsDir, dir, 'meta.json');
|
|
75
|
+
try {
|
|
76
|
+
const data = await fs.promises.readFile(metaPath, 'utf8');
|
|
77
|
+
const session = JSON.parse(data);
|
|
78
|
+
this.sessions.set(session.sessionId, session);
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
// Skip invalid session directories
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.log(`[Processor] Loaded ${this.sessions.size} sessions`);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
// No sessions directory yet
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Process a single event
|
|
92
|
+
* @param event The event to process
|
|
93
|
+
* @param filepath The source file path (null if from socket)
|
|
94
|
+
*/
|
|
95
|
+
async processEvent(event, filepath) {
|
|
96
|
+
// Deduplicate events (can arrive via socket AND file watcher)
|
|
97
|
+
if (this.processedEventIds.has(event.id)) {
|
|
98
|
+
// Already processed via socket, just move the file
|
|
99
|
+
if (filepath) {
|
|
100
|
+
await this.moveToProcessed(filepath);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.processedEventIds.add(event.id);
|
|
105
|
+
// Limit memory usage by keeping only recent event IDs
|
|
106
|
+
if (this.processedEventIds.size > 10000) {
|
|
107
|
+
const arr = Array.from(this.processedEventIds);
|
|
108
|
+
this.processedEventIds = new Set(arr.slice(-5000));
|
|
109
|
+
}
|
|
110
|
+
const originalSessionId = event.sessionId;
|
|
111
|
+
// Handle subagent lifecycle
|
|
112
|
+
if (event.hookType === 'PreToolUse' && event.data?.toolName === 'Task') {
|
|
113
|
+
await this.startSubagentContext(event);
|
|
114
|
+
}
|
|
115
|
+
else if (event.eventType === 'subagent_start') {
|
|
116
|
+
// Handle SubagentStart event for proper hierarchy
|
|
117
|
+
await this.handleSubagentStart(event);
|
|
118
|
+
}
|
|
119
|
+
else if (event.hookType === 'SubagentStop' || event.eventType === 'subagent_stop') {
|
|
120
|
+
await this.endSubagentContext(event);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Route to active subagent if exists
|
|
124
|
+
const activeStack = this.state.getActiveSubagentStack(originalSessionId);
|
|
125
|
+
if (activeStack.length > 0) {
|
|
126
|
+
const active = activeStack[activeStack.length - 1];
|
|
127
|
+
event = { ...event, sessionId: active.virtualSessionId, parentSessionId: originalSessionId };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Track pending subagent spawns
|
|
131
|
+
this.trackSubagentSpawn(event);
|
|
132
|
+
// Update session
|
|
133
|
+
await this.updateSession(event);
|
|
134
|
+
// Persist event
|
|
135
|
+
await this.persistEvent(event);
|
|
136
|
+
// Update state checkpoint
|
|
137
|
+
this.state.lastProcessedTimestamp = event.timestamp;
|
|
138
|
+
// Move raw file to processed (only if from file watcher)
|
|
139
|
+
if (filepath) {
|
|
140
|
+
await this.moveToProcessed(filepath);
|
|
141
|
+
}
|
|
142
|
+
// Notify UI server
|
|
143
|
+
await this.notifier.notify(event);
|
|
144
|
+
// Feed event to circuit breaker for health monitoring
|
|
145
|
+
if (this.circuitBreaker) {
|
|
146
|
+
// Try to find associated wrapper for this session
|
|
147
|
+
const wrapperId = this.findWrapperForSession(event.sessionId);
|
|
148
|
+
this.circuitBreaker.processEvent(event, wrapperId);
|
|
149
|
+
// Clean up circuit breaker tracking when session ends
|
|
150
|
+
if (event.eventType === 'session_end' || event.eventType === 'agent_complete') {
|
|
151
|
+
this.circuitBreaker.removeSession(event.sessionId);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Find wrapper ID associated with a session (for circuit breaker)
|
|
157
|
+
* This is a placeholder - the daemon will provide better association
|
|
158
|
+
*/
|
|
159
|
+
findWrapperForSession(sessionId) {
|
|
160
|
+
// This will be enhanced by the daemon's wrapper-session correlation
|
|
161
|
+
// For now, return null and let the circuit breaker handle it
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Handle SubagentStart event - create child session immediately
|
|
166
|
+
*/
|
|
167
|
+
async handleSubagentStart(event) {
|
|
168
|
+
const parentSessionId = event.parentSessionId || undefined;
|
|
169
|
+
// Ensure parent session exists first
|
|
170
|
+
if (parentSessionId && !this.sessions.has(parentSessionId)) {
|
|
171
|
+
const parentSession = {
|
|
172
|
+
sessionId: parentSessionId,
|
|
173
|
+
machineId: event.machineId,
|
|
174
|
+
workingDirectory: event.workingDirectory,
|
|
175
|
+
startTime: event.timestamp - 1, // Slightly before child
|
|
176
|
+
status: 'active',
|
|
177
|
+
childSessionIds: [],
|
|
178
|
+
tokenUsage: { totalInput: 0, totalOutput: 0 },
|
|
179
|
+
isUserInitiated: true, // Parent is user-initiated
|
|
180
|
+
isPinned: false,
|
|
181
|
+
};
|
|
182
|
+
this.sessions.set(parentSessionId, parentSession);
|
|
183
|
+
await this.persistSession(parentSession);
|
|
184
|
+
await this.notifier.notifySessionUpdate(parentSession);
|
|
185
|
+
console.log(`[Processor] Created parent session: ${parentSessionId}`);
|
|
186
|
+
}
|
|
187
|
+
// SubagentStart should have its own session_id from Claude
|
|
188
|
+
// Create the child session with parent relationship
|
|
189
|
+
const session = {
|
|
190
|
+
sessionId: event.sessionId,
|
|
191
|
+
parentSessionId,
|
|
192
|
+
machineId: event.machineId,
|
|
193
|
+
workingDirectory: event.workingDirectory,
|
|
194
|
+
startTime: event.timestamp,
|
|
195
|
+
status: 'active',
|
|
196
|
+
childSessionIds: [],
|
|
197
|
+
tokenUsage: { totalInput: 0, totalOutput: 0 },
|
|
198
|
+
agentType: event.data?.subagent?.type?.toLowerCase() || undefined,
|
|
199
|
+
isUserInitiated: false,
|
|
200
|
+
isPinned: false,
|
|
201
|
+
};
|
|
202
|
+
this.sessions.set(session.sessionId, session);
|
|
203
|
+
await this.persistSession(session);
|
|
204
|
+
// Add child to parent's childSessionIds
|
|
205
|
+
if (parentSessionId) {
|
|
206
|
+
const parent = this.sessions.get(parentSessionId);
|
|
207
|
+
if (parent && !parent.childSessionIds.includes(session.sessionId)) {
|
|
208
|
+
parent.childSessionIds.push(session.sessionId);
|
|
209
|
+
await this.persistSession(parent);
|
|
210
|
+
await this.notifier.notifySessionUpdate(parent);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
await this.notifier.notifySessionUpdate(session);
|
|
214
|
+
console.log(`[Processor] SubagentStart: ${session.sessionId} (type: ${session.agentType}, parent: ${session.parentSessionId})`);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Generate a short unique ID for virtual sessions
|
|
218
|
+
*/
|
|
219
|
+
generateVirtualSessionId() {
|
|
220
|
+
const chars = 'abcdef0123456789';
|
|
221
|
+
let id = '';
|
|
222
|
+
for (let i = 0; i < 8; i++) {
|
|
223
|
+
id += chars[Math.floor(Math.random() * chars.length)];
|
|
224
|
+
}
|
|
225
|
+
return id;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Start tracking a subagent context when Task tool is called
|
|
229
|
+
*/
|
|
230
|
+
async startSubagentContext(event) {
|
|
231
|
+
const virtualId = this.generateVirtualSessionId();
|
|
232
|
+
const agentType = this.extractAgentType(event);
|
|
233
|
+
const subagent = {
|
|
234
|
+
virtualSessionId: virtualId,
|
|
235
|
+
parentSessionId: event.sessionId,
|
|
236
|
+
agentType: agentType?.toLowerCase(),
|
|
237
|
+
startTime: event.timestamp,
|
|
238
|
+
};
|
|
239
|
+
this.state.pushActiveSubagent(event.sessionId, subagent);
|
|
240
|
+
// Create the virtual session immediately with agentType
|
|
241
|
+
const session = {
|
|
242
|
+
sessionId: virtualId,
|
|
243
|
+
machineId: event.machineId,
|
|
244
|
+
workingDirectory: event.workingDirectory,
|
|
245
|
+
startTime: event.timestamp,
|
|
246
|
+
status: 'active',
|
|
247
|
+
parentSessionId: event.sessionId,
|
|
248
|
+
childSessionIds: [],
|
|
249
|
+
tokenUsage: { totalInput: 0, totalOutput: 0 },
|
|
250
|
+
isUserInitiated: false,
|
|
251
|
+
isPinned: false,
|
|
252
|
+
agentType: agentType?.toLowerCase(),
|
|
253
|
+
};
|
|
254
|
+
this.sessions.set(virtualId, session);
|
|
255
|
+
await this.persistSession(session);
|
|
256
|
+
// Add to parent's children
|
|
257
|
+
const parent = this.sessions.get(event.sessionId);
|
|
258
|
+
if (parent && !parent.childSessionIds.includes(virtualId)) {
|
|
259
|
+
parent.childSessionIds.push(virtualId);
|
|
260
|
+
await this.persistSession(parent);
|
|
261
|
+
await this.notifier.notifySessionUpdate(parent);
|
|
262
|
+
}
|
|
263
|
+
await this.notifier.notifySessionUpdate(session);
|
|
264
|
+
console.log(`[Processor] Started subagent: ${virtualId} (type: ${agentType || 'unknown'}, parent: ${event.sessionId})`);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* End the current subagent context
|
|
268
|
+
*/
|
|
269
|
+
async endSubagentContext(event) {
|
|
270
|
+
const subagent = this.state.popActiveSubagent(event.sessionId);
|
|
271
|
+
if (subagent) {
|
|
272
|
+
// Mark virtual session as completed
|
|
273
|
+
const session = this.sessions.get(subagent.virtualSessionId);
|
|
274
|
+
if (session) {
|
|
275
|
+
session.status = 'completed';
|
|
276
|
+
session.endTime = event.timestamp;
|
|
277
|
+
await this.persistSession(session);
|
|
278
|
+
await this.notifier.notifySessionUpdate(session);
|
|
279
|
+
}
|
|
280
|
+
console.log(`[Processor] Ended subagent: ${subagent.virtualSessionId} (type: ${subagent.agentType || 'unknown'})`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Extract agent type from Task tool input
|
|
285
|
+
*/
|
|
286
|
+
extractAgentType(event) {
|
|
287
|
+
const toolInput = event.data?.toolInput;
|
|
288
|
+
const type = toolInput?.subagent_type || toolInput?.agentType;
|
|
289
|
+
return type ? type.toLowerCase() : undefined;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Track potential sub-agent spawns from Task tool calls
|
|
293
|
+
*/
|
|
294
|
+
trackSubagentSpawn(event) {
|
|
295
|
+
if (event.hookType === 'PreToolUse' && event.data?.toolName === 'Task') {
|
|
296
|
+
const spawn = {
|
|
297
|
+
parentSessionId: event.sessionId,
|
|
298
|
+
workingDirectory: event.workingDirectory,
|
|
299
|
+
timestamp: event.timestamp,
|
|
300
|
+
agentType: this.extractAgentType(event),
|
|
301
|
+
};
|
|
302
|
+
const spawns = this.state.pendingSubagentSpawns;
|
|
303
|
+
spawns.push(spawn);
|
|
304
|
+
// Clean up old spawns (older than correlation window)
|
|
305
|
+
const cutoff = Date.now() - SUBAGENT_CORRELATION_WINDOW_MS;
|
|
306
|
+
this.state.pendingSubagentSpawns = spawns.filter(s => s.timestamp > cutoff);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Update session state based on event
|
|
311
|
+
*/
|
|
312
|
+
async updateSession(event) {
|
|
313
|
+
let session = this.sessions.get(event.sessionId);
|
|
314
|
+
if (!session) {
|
|
315
|
+
// New session
|
|
316
|
+
const parentSessionId = event.parentSessionId || this.correlateParent(event);
|
|
317
|
+
const isUserInitiated = !parentSessionId;
|
|
318
|
+
session = {
|
|
319
|
+
sessionId: event.sessionId,
|
|
320
|
+
machineId: event.machineId,
|
|
321
|
+
workingDirectory: event.workingDirectory,
|
|
322
|
+
startTime: event.timestamp,
|
|
323
|
+
status: 'active',
|
|
324
|
+
parentSessionId,
|
|
325
|
+
childSessionIds: [],
|
|
326
|
+
tokenUsage: { totalInput: 0, totalOutput: 0 },
|
|
327
|
+
isUserInitiated,
|
|
328
|
+
isPinned: false, // Manual pinning only
|
|
329
|
+
};
|
|
330
|
+
// Add to parent's children
|
|
331
|
+
if (parentSessionId) {
|
|
332
|
+
const parent = this.sessions.get(parentSessionId);
|
|
333
|
+
if (parent && !parent.childSessionIds.includes(event.sessionId)) {
|
|
334
|
+
parent.childSessionIds.push(event.sessionId);
|
|
335
|
+
await this.persistSession(parent);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
this.sessions.set(event.sessionId, session);
|
|
339
|
+
console.log(`[Processor] New session: ${event.sessionId} (parent: ${parentSessionId || 'none'})`);
|
|
340
|
+
}
|
|
341
|
+
// Update session based on event
|
|
342
|
+
if (event.eventType === 'session_end' || event.eventType === 'agent_complete') {
|
|
343
|
+
session.status = 'completed';
|
|
344
|
+
session.endTime = event.timestamp;
|
|
345
|
+
// Use session token usage from transcript parsing
|
|
346
|
+
if (event.data.sessionTokenUsage) {
|
|
347
|
+
session.tokenUsage = {
|
|
348
|
+
totalInput: event.data.sessionTokenUsage.input || 0,
|
|
349
|
+
totalOutput: event.data.sessionTokenUsage.output || 0,
|
|
350
|
+
totalCacheRead: event.data.sessionTokenUsage.cacheRead || 0,
|
|
351
|
+
totalCacheCreation: (event.data.sessionTokenUsage.cacheCreation5m || 0) +
|
|
352
|
+
(event.data.sessionTokenUsage.cacheCreation1h || 0),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// Store model and transcript path
|
|
356
|
+
if (event.data.model) {
|
|
357
|
+
session.model = event.data.model;
|
|
358
|
+
}
|
|
359
|
+
if (event.data.transcriptPath) {
|
|
360
|
+
session.transcriptPath = event.data.transcriptPath;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else if (event.eventType === 'error') {
|
|
364
|
+
session.status = 'error';
|
|
365
|
+
}
|
|
366
|
+
// Accumulate tokens
|
|
367
|
+
if (event.data?.tokenUsage) {
|
|
368
|
+
session.tokenUsage.totalInput += event.data.tokenUsage.input || 0;
|
|
369
|
+
session.tokenUsage.totalOutput += event.data.tokenUsage.output || 0;
|
|
370
|
+
}
|
|
371
|
+
await this.persistSession(session);
|
|
372
|
+
await this.notifier.notifySessionUpdate(session);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Try to correlate a new session with a pending sub-agent spawn
|
|
376
|
+
*/
|
|
377
|
+
correlateParent(event) {
|
|
378
|
+
const spawns = this.state.pendingSubagentSpawns;
|
|
379
|
+
const cutoff = event.timestamp - SUBAGENT_CORRELATION_WINDOW_MS;
|
|
380
|
+
for (const spawn of spawns) {
|
|
381
|
+
if (spawn.timestamp < cutoff)
|
|
382
|
+
continue;
|
|
383
|
+
if (spawn.parentSessionId === event.sessionId)
|
|
384
|
+
continue;
|
|
385
|
+
if (!this.directoriesMatch(spawn.workingDirectory, event.workingDirectory))
|
|
386
|
+
continue;
|
|
387
|
+
return spawn.parentSessionId;
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Check if two working directories match (handles git worktrees)
|
|
393
|
+
*/
|
|
394
|
+
directoriesMatch(dir1, dir2) {
|
|
395
|
+
// Normalize paths
|
|
396
|
+
const norm1 = dir1.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
397
|
+
const norm2 = dir2.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
398
|
+
// Exact match
|
|
399
|
+
if (norm1 === norm2)
|
|
400
|
+
return true;
|
|
401
|
+
// One contains the other
|
|
402
|
+
if (norm1.startsWith(norm2 + '/') || norm2.startsWith(norm1 + '/'))
|
|
403
|
+
return true;
|
|
404
|
+
// Check for common parent (worktree scenario)
|
|
405
|
+
const parent1 = norm1.split('/').slice(0, -1).join('/');
|
|
406
|
+
const parent2 = norm2.split('/').slice(0, -1).join('/');
|
|
407
|
+
if (parent1 === parent2)
|
|
408
|
+
return true;
|
|
409
|
+
// Check for shared ancestor up to 3 levels
|
|
410
|
+
const parts1 = norm1.split('/');
|
|
411
|
+
const parts2 = norm2.split('/');
|
|
412
|
+
const minLength = Math.min(parts1.length, parts2.length);
|
|
413
|
+
// Find common prefix depth (at least 3 levels like /home/user/project)
|
|
414
|
+
for (let i = Math.min(minLength, parts1.length - 3); i >= 3; i--) {
|
|
415
|
+
const prefix1 = parts1.slice(0, i).join('/');
|
|
416
|
+
const prefix2 = parts2.slice(0, i).join('/');
|
|
417
|
+
if (prefix1 === prefix2)
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Persist session metadata to disk
|
|
424
|
+
*/
|
|
425
|
+
async persistSession(session) {
|
|
426
|
+
const sessionDir = path.join(PATHS.sessionsDir, session.sessionId);
|
|
427
|
+
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
428
|
+
const metaPath = path.join(sessionDir, 'meta.json');
|
|
429
|
+
await fs.promises.writeFile(metaPath, JSON.stringify(session, null, 2));
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Persist event to session's events.jsonl file
|
|
433
|
+
*/
|
|
434
|
+
async persistEvent(event) {
|
|
435
|
+
const sessionDir = path.join(PATHS.sessionsDir, event.sessionId);
|
|
436
|
+
await fs.promises.mkdir(sessionDir, { recursive: true });
|
|
437
|
+
const eventsPath = path.join(sessionDir, 'events.jsonl');
|
|
438
|
+
await fs.promises.appendFile(eventsPath, JSON.stringify(event) + '\n');
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Move processed file from raw/ to processed/
|
|
442
|
+
*/
|
|
443
|
+
async moveToProcessed(filepath) {
|
|
444
|
+
try {
|
|
445
|
+
await fs.promises.mkdir(PATHS.processedDir, { recursive: true });
|
|
446
|
+
const filename = path.basename(filepath);
|
|
447
|
+
const destPath = path.join(PATHS.processedDir, filename);
|
|
448
|
+
await fs.promises.rename(filepath, destPath);
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
console.error('[Processor] Failed to move to processed:', err);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
exports.EventProcessor = EventProcessor;
|