@esotech/contextuate 2.0.0 → 2.1.1

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.
Files changed (103) hide show
  1. package/README.md +169 -1
  2. package/dist/commands/claude.d.ts +21 -0
  3. package/dist/commands/claude.js +213 -0
  4. package/dist/commands/context.d.ts +1 -0
  5. package/dist/commands/create.d.ts +3 -0
  6. package/dist/commands/index.d.ts +4 -0
  7. package/dist/commands/init.d.ts +7 -0
  8. package/dist/commands/init.js +67 -6
  9. package/dist/commands/install.d.ts +28 -0
  10. package/dist/commands/install.js +100 -11
  11. package/dist/commands/monitor.d.ts +55 -0
  12. package/dist/commands/monitor.js +1007 -0
  13. package/dist/commands/remove.d.ts +3 -0
  14. package/dist/commands/run.d.ts +6 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +113 -1
  17. package/dist/monitor/daemon/circuit-breaker.d.ts +121 -0
  18. package/dist/monitor/daemon/circuit-breaker.js +552 -0
  19. package/dist/monitor/daemon/cli.d.ts +8 -0
  20. package/dist/monitor/daemon/cli.js +82 -0
  21. package/dist/monitor/daemon/index.d.ts +137 -0
  22. package/dist/monitor/daemon/index.js +695 -0
  23. package/dist/monitor/daemon/notifier.d.ts +25 -0
  24. package/dist/monitor/daemon/notifier.js +98 -0
  25. package/dist/monitor/daemon/processor.d.ts +89 -0
  26. package/dist/monitor/daemon/processor.js +455 -0
  27. package/dist/monitor/daemon/state.d.ts +80 -0
  28. package/dist/monitor/daemon/state.js +162 -0
  29. package/dist/monitor/daemon/watcher.d.ts +47 -0
  30. package/dist/monitor/daemon/watcher.js +171 -0
  31. package/dist/monitor/daemon/wrapper-manager.d.ts +106 -0
  32. package/dist/monitor/daemon/wrapper-manager.js +374 -0
  33. package/dist/monitor/hooks/emit-event.js +652 -0
  34. package/dist/monitor/persistence/file-store.d.ts +88 -0
  35. package/dist/monitor/persistence/file-store.js +335 -0
  36. package/dist/monitor/persistence/index.d.ts +7 -0
  37. package/dist/monitor/persistence/index.js +10 -0
  38. package/dist/monitor/server/adapters/redis.d.ts +38 -0
  39. package/dist/monitor/server/adapters/redis.js +213 -0
  40. package/dist/monitor/server/adapters/unix-socket.d.ts +33 -0
  41. package/dist/monitor/server/adapters/unix-socket.js +182 -0
  42. package/dist/monitor/server/broker.d.ts +135 -0
  43. package/dist/monitor/server/broker.js +475 -0
  44. package/dist/monitor/server/cli.d.ts +8 -0
  45. package/dist/monitor/server/cli.js +98 -0
  46. package/dist/monitor/server/fastify.d.ts +16 -0
  47. package/dist/monitor/server/fastify.js +184 -0
  48. package/dist/monitor/server/index.d.ts +36 -0
  49. package/dist/monitor/server/index.js +153 -0
  50. package/dist/monitor/server/websocket.d.ts +80 -0
  51. package/dist/monitor/server/websocket.js +453 -0
  52. package/dist/monitor/ui/assets/index-4IssW9On.js +59 -0
  53. package/dist/monitor/ui/assets/index-vo9hLe5R.css +32 -0
  54. package/dist/monitor/ui/favicon.png +0 -0
  55. package/dist/monitor/ui/index.html +14 -0
  56. package/dist/monitor/ui/logo.png +0 -0
  57. package/dist/monitor/ui/logo.svg +1 -0
  58. package/dist/runtime/driver.d.ts +16 -0
  59. package/dist/runtime/tools.d.ts +10 -0
  60. package/dist/templates/README.md +33 -7
  61. package/dist/templates/agents/aegis.md +4 -0
  62. package/dist/templates/agents/archon.md +13 -22
  63. package/dist/templates/agents/atlas.md +4 -0
  64. package/dist/templates/agents/canvas.md +4 -0
  65. package/dist/templates/agents/chronicle.md +4 -0
  66. package/dist/templates/agents/chronos.md +4 -0
  67. package/dist/templates/agents/cipher.md +4 -0
  68. package/dist/templates/agents/crucible.md +4 -0
  69. package/dist/templates/agents/echo.md +4 -0
  70. package/dist/templates/agents/forge.md +4 -0
  71. package/dist/templates/agents/ledger.md +4 -0
  72. package/dist/templates/agents/meridian.md +4 -0
  73. package/dist/templates/agents/nexus.md +4 -0
  74. package/dist/templates/agents/pythia.md +217 -0
  75. package/dist/templates/agents/scribe.md +4 -0
  76. package/dist/templates/agents/sentinel.md +4 -0
  77. package/dist/templates/agents/{oracle.md → thoth.md} +11 -7
  78. package/dist/templates/agents/unity.md +4 -0
  79. package/dist/templates/agents/vox.md +4 -0
  80. package/dist/templates/agents/weaver.md +4 -0
  81. package/dist/templates/commands/consult.md +138 -0
  82. package/dist/templates/commands/orchestrate.md +173 -0
  83. package/dist/templates/framework-agents/documentation-expert.md +3 -3
  84. package/dist/templates/framework-agents/tools-expert.md +8 -8
  85. package/dist/templates/standards/agent-roles.md +68 -21
  86. package/dist/templates/standards/coding-standards.md +9 -26
  87. package/dist/templates/templates/context.md +17 -2
  88. package/dist/templates/templates/contextuate.md +21 -28
  89. package/dist/templates/tools/{agent-creator.tool.md → agent-creator.md} +3 -3
  90. package/dist/types/monitor.d.ts +660 -0
  91. package/dist/types/monitor.js +75 -0
  92. package/dist/utils/git.d.ts +9 -0
  93. package/dist/utils/tokens.d.ts +10 -0
  94. package/package.json +18 -5
  95. package/dist/templates/version.json +0 -8
  96. /package/dist/templates/templates/standards/{go.standards.md → go.md} +0 -0
  97. /package/dist/templates/templates/standards/{java.standards.md → java.md} +0 -0
  98. /package/dist/templates/templates/standards/{javascript.standards.md → javascript.md} +0 -0
  99. /package/dist/templates/templates/standards/{php.standards.md → php.md} +0 -0
  100. /package/dist/templates/templates/standards/{python.standards.md → python.md} +0 -0
  101. /package/dist/templates/tools/{quickref.tool.md → quickref.md} +0 -0
  102. /package/dist/templates/tools/{spawn.tool.md → spawn.md} +0 -0
  103. /package/dist/templates/tools/{standards-detector.tool.md → standards-detector.md} +0 -0
@@ -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;