@rk0429/agentic-relay 1.1.1 → 1.3.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/dist/relay.mjs +1557 -141
- package/package.json +1 -1
package/dist/relay.mjs
CHANGED
|
@@ -46,6 +46,120 @@ var init_logger = __esm({
|
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
+
// src/core/metadata-validation.ts
|
|
50
|
+
function isPlainObject(value) {
|
|
51
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const prototype = Object.getPrototypeOf(value);
|
|
55
|
+
return prototype === Object.prototype || prototype === null;
|
|
56
|
+
}
|
|
57
|
+
function utf8Size(value) {
|
|
58
|
+
return new TextEncoder().encode(value).length;
|
|
59
|
+
}
|
|
60
|
+
function validateMetadataKey(key) {
|
|
61
|
+
if (DANGEROUS_METADATA_KEYS.has(key)) {
|
|
62
|
+
throw new Error(`metadata key "${key}" is not allowed`);
|
|
63
|
+
}
|
|
64
|
+
if (key.length > MAX_METADATA_KEY_LENGTH) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`metadata key "${key}" exceeds ${MAX_METADATA_KEY_LENGTH} chars`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function validateMetadataValue(value, path2, depth) {
|
|
71
|
+
if (depth > MAX_METADATA_NESTING_DEPTH) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`metadata nesting depth exceeds ${MAX_METADATA_NESTING_DEPTH} at "${path2}"`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === "string") {
|
|
77
|
+
if (value.length > MAX_METADATA_STRING_VALUE_LENGTH) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`metadata string at "${path2}" exceeds ${MAX_METADATA_STRING_VALUE_LENGTH} chars`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean") {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
89
|
+
validateMetadataValue(value[index], `${path2}[${index}]`, depth + 1);
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === "object") {
|
|
94
|
+
if (!isPlainObject(value)) {
|
|
95
|
+
throw new Error(`metadata value at "${path2}" must be a plain object`);
|
|
96
|
+
}
|
|
97
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
98
|
+
validateMetadataKey(key);
|
|
99
|
+
validateMetadataValue(nestedValue, `${path2}.${key}`, depth + 1);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`metadata value at "${path2}" has unsupported type`);
|
|
104
|
+
}
|
|
105
|
+
function validateMetadata(raw) {
|
|
106
|
+
if (raw === void 0 || raw === null) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
if (!isPlainObject(raw)) {
|
|
110
|
+
throw new Error("metadata must be a plain object");
|
|
111
|
+
}
|
|
112
|
+
let serialized;
|
|
113
|
+
try {
|
|
114
|
+
serialized = JSON.stringify(raw);
|
|
115
|
+
} catch {
|
|
116
|
+
throw new Error("metadata must be JSON-serializable");
|
|
117
|
+
}
|
|
118
|
+
if (utf8Size(serialized) > MAX_METADATA_SIZE_BYTES) {
|
|
119
|
+
throw new Error(`metadata exceeds ${MAX_METADATA_SIZE_BYTES} bytes`);
|
|
120
|
+
}
|
|
121
|
+
const entries = Object.entries(raw);
|
|
122
|
+
if (entries.length > MAX_METADATA_KEY_COUNT) {
|
|
123
|
+
throw new Error(`metadata has ${entries.length} keys, max is ${MAX_METADATA_KEY_COUNT}`);
|
|
124
|
+
}
|
|
125
|
+
for (const [key, value] of entries) {
|
|
126
|
+
validateMetadataKey(key);
|
|
127
|
+
validateMetadataValue(value, key, 1);
|
|
128
|
+
}
|
|
129
|
+
const typed = raw;
|
|
130
|
+
if (typed.taskId !== void 0 && typeof typed.taskId !== "string") {
|
|
131
|
+
throw new Error("metadata.taskId must be a string");
|
|
132
|
+
}
|
|
133
|
+
if (typed.taskId !== void 0 && typeof typed.taskId === "string" && !TASK_ID_PATTERN.test(typed.taskId)) {
|
|
134
|
+
throw new Error("metadata.taskId must match ^(TASK|GOAL)-\\d{3,}$");
|
|
135
|
+
}
|
|
136
|
+
if (typed.agentType !== void 0 && typeof typed.agentType !== "string") {
|
|
137
|
+
throw new Error("metadata.agentType must be a string");
|
|
138
|
+
}
|
|
139
|
+
if (typed.label !== void 0 && typeof typed.label !== "string") {
|
|
140
|
+
throw new Error("metadata.label must be a string");
|
|
141
|
+
}
|
|
142
|
+
if (typed.tags !== void 0) {
|
|
143
|
+
if (!Array.isArray(typed.tags) || !typed.tags.every((tag) => typeof tag === "string")) {
|
|
144
|
+
throw new Error("metadata.tags must be string[]");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return typed;
|
|
148
|
+
}
|
|
149
|
+
var DANGEROUS_METADATA_KEYS, TASK_ID_PATTERN, MAX_METADATA_SIZE_BYTES, MAX_METADATA_KEY_COUNT, MAX_METADATA_KEY_LENGTH, MAX_METADATA_STRING_VALUE_LENGTH, MAX_METADATA_NESTING_DEPTH;
|
|
150
|
+
var init_metadata_validation = __esm({
|
|
151
|
+
"src/core/metadata-validation.ts"() {
|
|
152
|
+
"use strict";
|
|
153
|
+
DANGEROUS_METADATA_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
154
|
+
TASK_ID_PATTERN = /^(TASK|GOAL)-\d{3,}$/;
|
|
155
|
+
MAX_METADATA_SIZE_BYTES = 8 * 1024;
|
|
156
|
+
MAX_METADATA_KEY_COUNT = 20;
|
|
157
|
+
MAX_METADATA_KEY_LENGTH = 64;
|
|
158
|
+
MAX_METADATA_STRING_VALUE_LENGTH = 1024;
|
|
159
|
+
MAX_METADATA_NESTING_DEPTH = 3;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
49
163
|
// src/mcp-server/deferred-cleanup-task-store.ts
|
|
50
164
|
import { isTerminal } from "@modelcontextprotocol/sdk/experimental/tasks/interfaces.js";
|
|
51
165
|
import { randomBytes } from "crypto";
|
|
@@ -169,6 +283,424 @@ var init_deferred_cleanup_task_store = __esm({
|
|
|
169
283
|
}
|
|
170
284
|
});
|
|
171
285
|
|
|
286
|
+
// src/core/agent-event-store.ts
|
|
287
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
288
|
+
import { join as join6 } from "path";
|
|
289
|
+
import { homedir as homedir5 } from "os";
|
|
290
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
291
|
+
function getRelayHome2() {
|
|
292
|
+
return process.env["RELAY_HOME"] ?? join6(homedir5(), ".relay");
|
|
293
|
+
}
|
|
294
|
+
function isValidEventType(type) {
|
|
295
|
+
return type === "session-start" || type === "session-complete" || type === "session-error" || type === "session-stale" || type === "context-threshold";
|
|
296
|
+
}
|
|
297
|
+
var DEFAULT_CONFIG2, AgentEventStore;
|
|
298
|
+
var init_agent_event_store = __esm({
|
|
299
|
+
"src/core/agent-event-store.ts"() {
|
|
300
|
+
"use strict";
|
|
301
|
+
init_logger();
|
|
302
|
+
DEFAULT_CONFIG2 = {
|
|
303
|
+
maxEvents: 1e3,
|
|
304
|
+
ttlMs: 36e5,
|
|
305
|
+
backend: "jsonl",
|
|
306
|
+
sessionDir: join6(getRelayHome2(), "sessions"),
|
|
307
|
+
eventsFileName: "events.jsonl"
|
|
308
|
+
};
|
|
309
|
+
AgentEventStore = class {
|
|
310
|
+
config;
|
|
311
|
+
eventsFilePath;
|
|
312
|
+
cleanupTimer = null;
|
|
313
|
+
events = [];
|
|
314
|
+
constructor(config) {
|
|
315
|
+
const ttlMs = config?.ttlMs ?? (config?.ttlSec ?? 3600) * 1e3;
|
|
316
|
+
this.config = {
|
|
317
|
+
...DEFAULT_CONFIG2,
|
|
318
|
+
...config,
|
|
319
|
+
ttlMs
|
|
320
|
+
};
|
|
321
|
+
this.eventsFilePath = this.config.backend === "jsonl" ? join6(this.config.sessionDir, this.config.eventsFileName) : null;
|
|
322
|
+
if (this.eventsFilePath) {
|
|
323
|
+
mkdirSync(this.config.sessionDir, { recursive: true });
|
|
324
|
+
this.restoreFromJsonl();
|
|
325
|
+
}
|
|
326
|
+
this.cleanupTimer = setInterval(() => {
|
|
327
|
+
this.prune();
|
|
328
|
+
}, 6e4);
|
|
329
|
+
this.cleanupTimer.unref();
|
|
330
|
+
}
|
|
331
|
+
restoreFromJsonl() {
|
|
332
|
+
if (!this.eventsFilePath || !existsSync(this.eventsFilePath)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
let malformedCount = 0;
|
|
336
|
+
try {
|
|
337
|
+
const raw = readFileSync(this.eventsFilePath, "utf-8");
|
|
338
|
+
const lines = raw.split("\n").filter((line) => line.trim().length > 0);
|
|
339
|
+
for (const line of lines) {
|
|
340
|
+
try {
|
|
341
|
+
const parsed = JSON.parse(line);
|
|
342
|
+
if (typeof parsed.eventId !== "string" || typeof parsed.timestamp !== "string" || !isValidEventType(parsed.type) || typeof parsed.sessionId !== "string" || typeof parsed.backendId !== "string" || typeof parsed.metadata !== "object" || parsed.metadata === null || typeof parsed.data !== "object" || parsed.data === null) {
|
|
343
|
+
malformedCount += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
this.events.push({
|
|
347
|
+
eventId: parsed.eventId,
|
|
348
|
+
timestamp: parsed.timestamp,
|
|
349
|
+
type: parsed.type,
|
|
350
|
+
sessionId: parsed.sessionId,
|
|
351
|
+
parentSessionId: parsed.parentSessionId,
|
|
352
|
+
backendId: parsed.backendId,
|
|
353
|
+
metadata: parsed.metadata,
|
|
354
|
+
data: parsed.data
|
|
355
|
+
});
|
|
356
|
+
} catch {
|
|
357
|
+
malformedCount += 1;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
this.events.sort((a, b) => {
|
|
361
|
+
const diff = new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
|
|
362
|
+
return diff !== 0 ? diff : 0;
|
|
363
|
+
});
|
|
364
|
+
this.prune();
|
|
365
|
+
} catch (error) {
|
|
366
|
+
logger.warn(
|
|
367
|
+
`Failed to restore event store from JSONL: ${error instanceof Error ? error.message : String(error)}`
|
|
368
|
+
);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (malformedCount > 0) {
|
|
372
|
+
logger.warn(
|
|
373
|
+
`Skipped ${malformedCount} malformed event line(s) while restoring ${this.eventsFilePath}`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
record(event) {
|
|
378
|
+
const fullEvent = {
|
|
379
|
+
...event,
|
|
380
|
+
eventId: `evt-${nanoid2()}`,
|
|
381
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
382
|
+
metadata: event.metadata ?? {},
|
|
383
|
+
data: event.data ?? {}
|
|
384
|
+
};
|
|
385
|
+
this.events.push(fullEvent);
|
|
386
|
+
if (this.eventsFilePath) {
|
|
387
|
+
try {
|
|
388
|
+
appendFileSync(
|
|
389
|
+
this.eventsFilePath,
|
|
390
|
+
`${JSON.stringify(fullEvent)}
|
|
391
|
+
`,
|
|
392
|
+
"utf-8"
|
|
393
|
+
);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
logger.warn(
|
|
396
|
+
`Failed to append event JSONL: ${error instanceof Error ? error.message : String(error)}`
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
this.prune();
|
|
401
|
+
return fullEvent;
|
|
402
|
+
}
|
|
403
|
+
query(params) {
|
|
404
|
+
this.prune();
|
|
405
|
+
const limit = Math.max(1, Math.min(200, params.limit));
|
|
406
|
+
let candidates = this.events;
|
|
407
|
+
if (params.afterEventId) {
|
|
408
|
+
const index = candidates.findIndex((e) => e.eventId === params.afterEventId);
|
|
409
|
+
if (index < 0) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`RELAY_INVALID_CURSOR: cursor "${params.afterEventId}" was not found`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
candidates = candidates.slice(index + 1);
|
|
415
|
+
}
|
|
416
|
+
if (params.types && params.types.length > 0) {
|
|
417
|
+
const wanted = new Set(params.types);
|
|
418
|
+
candidates = candidates.filter((event) => wanted.has(event.type));
|
|
419
|
+
}
|
|
420
|
+
if (params.sessionId) {
|
|
421
|
+
candidates = candidates.filter((event) => event.sessionId === params.sessionId);
|
|
422
|
+
}
|
|
423
|
+
if (params.parentSessionId) {
|
|
424
|
+
if (params.recursive) {
|
|
425
|
+
const descendants = /* @__PURE__ */ new Set();
|
|
426
|
+
const queue = [params.parentSessionId];
|
|
427
|
+
while (queue.length > 0) {
|
|
428
|
+
const parent = queue.shift();
|
|
429
|
+
const children = this.events.filter((event) => event.parentSessionId === parent).map((event) => event.sessionId);
|
|
430
|
+
for (const child of children) {
|
|
431
|
+
if (!descendants.has(child)) {
|
|
432
|
+
descendants.add(child);
|
|
433
|
+
queue.push(child);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
candidates = candidates.filter((event) => descendants.has(event.sessionId));
|
|
438
|
+
} else {
|
|
439
|
+
candidates = candidates.filter(
|
|
440
|
+
(event) => event.parentSessionId === params.parentSessionId
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const events = candidates.slice(0, limit);
|
|
445
|
+
return {
|
|
446
|
+
events,
|
|
447
|
+
hasMore: candidates.length > events.length
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
prune() {
|
|
451
|
+
const now = Date.now();
|
|
452
|
+
const ttlCutoff = now - this.config.ttlMs;
|
|
453
|
+
let nextEvents = this.events.filter((event) => {
|
|
454
|
+
const ts = new Date(event.timestamp).getTime();
|
|
455
|
+
return !Number.isNaN(ts) && ts >= ttlCutoff;
|
|
456
|
+
});
|
|
457
|
+
if (nextEvents.length > this.config.maxEvents) {
|
|
458
|
+
nextEvents = nextEvents.slice(nextEvents.length - this.config.maxEvents);
|
|
459
|
+
}
|
|
460
|
+
if (nextEvents.length !== this.events.length) {
|
|
461
|
+
this.events = nextEvents;
|
|
462
|
+
if (this.eventsFilePath) {
|
|
463
|
+
try {
|
|
464
|
+
const serialized = this.events.length > 0 ? `${this.events.map((event) => JSON.stringify(event)).join("\n")}
|
|
465
|
+
` : "";
|
|
466
|
+
writeFileSync(this.eventsFilePath, serialized, "utf-8");
|
|
467
|
+
} catch (error) {
|
|
468
|
+
logger.warn(
|
|
469
|
+
`Failed to prune event JSONL file: ${error instanceof Error ? error.message : String(error)}`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
cleanup() {
|
|
476
|
+
if (this.cleanupTimer) {
|
|
477
|
+
clearInterval(this.cleanupTimer);
|
|
478
|
+
this.cleanupTimer = null;
|
|
479
|
+
}
|
|
480
|
+
this.events = [];
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// src/core/session-health-monitor.ts
|
|
487
|
+
var DEFAULT_CONFIG3, SessionHealthMonitor;
|
|
488
|
+
var init_session_health_monitor = __esm({
|
|
489
|
+
"src/core/session-health-monitor.ts"() {
|
|
490
|
+
"use strict";
|
|
491
|
+
init_logger();
|
|
492
|
+
DEFAULT_CONFIG3 = {
|
|
493
|
+
enabled: true,
|
|
494
|
+
heartbeatIntervalSec: 300,
|
|
495
|
+
staleThresholdSec: 300,
|
|
496
|
+
cleanupAfterSec: 3600,
|
|
497
|
+
maxActiveSessions: 20,
|
|
498
|
+
checkIntervalSec: 60,
|
|
499
|
+
memoryDir: "./memory"
|
|
500
|
+
};
|
|
501
|
+
SessionHealthMonitor = class {
|
|
502
|
+
constructor(config, sessionManager2, hooksEngine2, contextMonitor2, agentEventStore) {
|
|
503
|
+
this.sessionManager = sessionManager2;
|
|
504
|
+
this.hooksEngine = hooksEngine2;
|
|
505
|
+
this.contextMonitor = contextMonitor2;
|
|
506
|
+
this.agentEventStore = agentEventStore;
|
|
507
|
+
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
508
|
+
this.staleThresholdMs = this.config.staleThresholdSec * 1e3;
|
|
509
|
+
this.cleanupAfterMs = this.config.cleanupAfterSec * 1e3;
|
|
510
|
+
this.checkIntervalMs = this.config.checkIntervalSec * 1e3;
|
|
511
|
+
}
|
|
512
|
+
config;
|
|
513
|
+
staleThresholdMs;
|
|
514
|
+
cleanupAfterMs;
|
|
515
|
+
checkIntervalMs;
|
|
516
|
+
timer = null;
|
|
517
|
+
isChecking = false;
|
|
518
|
+
start() {
|
|
519
|
+
if (!this.config.enabled || this.timer) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
this.timer = setInterval(() => {
|
|
523
|
+
void this.checkHealthLoop();
|
|
524
|
+
}, this.checkIntervalMs);
|
|
525
|
+
this.timer.unref();
|
|
526
|
+
}
|
|
527
|
+
stop() {
|
|
528
|
+
if (this.timer) {
|
|
529
|
+
clearInterval(this.timer);
|
|
530
|
+
this.timer = null;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
async checkHealth(options) {
|
|
534
|
+
let sessions;
|
|
535
|
+
if (options?.sessionId) {
|
|
536
|
+
const session = await this.sessionManager.get(options.sessionId);
|
|
537
|
+
if (!session) {
|
|
538
|
+
throw new Error(`Session not found: ${options.sessionId}`);
|
|
539
|
+
}
|
|
540
|
+
sessions = [session];
|
|
541
|
+
} else if (options?.includeCompleted) {
|
|
542
|
+
sessions = await this.sessionManager.list();
|
|
543
|
+
} else {
|
|
544
|
+
sessions = await this.sessionManager.list({ status: "active" });
|
|
545
|
+
}
|
|
546
|
+
const now = Date.now();
|
|
547
|
+
const statuses = sessions.map(
|
|
548
|
+
(session) => this.buildHealthStatus(session, now)
|
|
549
|
+
);
|
|
550
|
+
const staleSessions = statuses.filter((status) => status.isStale).map((status) => status.relaySessionId);
|
|
551
|
+
return {
|
|
552
|
+
sessions: statuses,
|
|
553
|
+
staleSessions,
|
|
554
|
+
summary: {
|
|
555
|
+
total: statuses.length,
|
|
556
|
+
active: statuses.filter((s) => s.status === "active").length,
|
|
557
|
+
stale: staleSessions.length,
|
|
558
|
+
completed: statuses.filter((s) => s.status === "completed").length,
|
|
559
|
+
error: statuses.filter((s) => s.status === "error").length
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
isStale(session) {
|
|
564
|
+
if (session.status !== "active") {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
return this.getLastActivityAtMs(session) + this.staleThresholdMs < Date.now();
|
|
568
|
+
}
|
|
569
|
+
async handleStaleSession(session) {
|
|
570
|
+
if (session.status !== "active") {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
if (session.staleNotifiedAt) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const now = /* @__PURE__ */ new Date();
|
|
577
|
+
const staleSinceMs = now.getTime() - this.getLastActivityAtMs(session);
|
|
578
|
+
const errorMessage = `Session stale: no activity for ${staleSinceMs}ms`;
|
|
579
|
+
const contextUsage = this.contextMonitor?.getUsage(session.relaySessionId);
|
|
580
|
+
await this.sessionManager.update(session.relaySessionId, {
|
|
581
|
+
status: "error",
|
|
582
|
+
errorCode: "RELAY_SESSION_STALE",
|
|
583
|
+
errorMessage,
|
|
584
|
+
staleNotifiedAt: now
|
|
585
|
+
});
|
|
586
|
+
this.agentEventStore.record({
|
|
587
|
+
type: "session-stale",
|
|
588
|
+
sessionId: session.relaySessionId,
|
|
589
|
+
parentSessionId: session.parentSessionId ?? void 0,
|
|
590
|
+
backendId: session.backendId,
|
|
591
|
+
metadata: session.metadata,
|
|
592
|
+
data: {
|
|
593
|
+
lastHeartbeatAt: session.lastHeartbeatAt.toISOString(),
|
|
594
|
+
staleSinceMs,
|
|
595
|
+
taskId: session.metadata.taskId,
|
|
596
|
+
label: session.metadata.label,
|
|
597
|
+
agentType: session.metadata.agentType,
|
|
598
|
+
contextUsage: contextUsage ? {
|
|
599
|
+
usagePercent: contextUsage.usagePercent,
|
|
600
|
+
estimatedTokens: contextUsage.estimatedTokens,
|
|
601
|
+
contextWindow: contextUsage.contextWindow
|
|
602
|
+
} : void 0
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
if (this.hooksEngine) {
|
|
606
|
+
await this.hooksEngine.emit("on-session-stale", {
|
|
607
|
+
event: "on-session-stale",
|
|
608
|
+
sessionId: session.relaySessionId,
|
|
609
|
+
backendId: session.backendId,
|
|
610
|
+
timestamp: now.toISOString(),
|
|
611
|
+
data: {
|
|
612
|
+
lastHeartbeatAt: session.lastHeartbeatAt.toISOString(),
|
|
613
|
+
staleSinceMs,
|
|
614
|
+
taskId: session.metadata.taskId,
|
|
615
|
+
label: session.metadata.label,
|
|
616
|
+
agentType: session.metadata.agentType,
|
|
617
|
+
contextUsage: contextUsage ? {
|
|
618
|
+
usagePercent: contextUsage.usagePercent,
|
|
619
|
+
estimatedTokens: contextUsage.estimatedTokens
|
|
620
|
+
} : void 0,
|
|
621
|
+
memoryDir: this.config.memoryDir ?? "./memory"
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
async cleanupOldSessions() {
|
|
627
|
+
const { deletedSessionIds } = await this.sessionManager.cleanup(
|
|
628
|
+
this.cleanupAfterMs
|
|
629
|
+
);
|
|
630
|
+
if (this.contextMonitor) {
|
|
631
|
+
for (const sessionId of deletedSessionIds) {
|
|
632
|
+
this.contextMonitor.removeSession(sessionId);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
async checkHealthLoop() {
|
|
637
|
+
if (this.isChecking) return;
|
|
638
|
+
this.isChecking = true;
|
|
639
|
+
try {
|
|
640
|
+
const health = await this.checkHealth({ includeCompleted: false });
|
|
641
|
+
for (const staleId of health.staleSessions) {
|
|
642
|
+
const session = await this.sessionManager.get(staleId);
|
|
643
|
+
if (!session || !this.isStale(session)) {
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
await this.handleStaleSession(session);
|
|
647
|
+
}
|
|
648
|
+
await this.cleanupOldSessions();
|
|
649
|
+
const activeHealthyCount = health.sessions.filter(
|
|
650
|
+
(session) => session.status === "active" && !session.isStale
|
|
651
|
+
).length;
|
|
652
|
+
const warnThreshold = Math.ceil(this.config.maxActiveSessions * 0.8);
|
|
653
|
+
if (activeHealthyCount >= warnThreshold) {
|
|
654
|
+
logger.warn(
|
|
655
|
+
`Active session usage high: ${activeHealthyCount}/${this.config.maxActiveSessions}`
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
} catch (error) {
|
|
659
|
+
logger.warn(
|
|
660
|
+
`Session health check failed: ${error instanceof Error ? error.message : String(error)}`
|
|
661
|
+
);
|
|
662
|
+
} finally {
|
|
663
|
+
this.isChecking = false;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
buildHealthStatus(session, now) {
|
|
667
|
+
const lastActivityAtMs = this.getLastActivityAtMs(session);
|
|
668
|
+
const isStale = session.status === "active" && lastActivityAtMs + this.staleThresholdMs < now;
|
|
669
|
+
const issues = [];
|
|
670
|
+
if (isStale) {
|
|
671
|
+
issues.push("stale");
|
|
672
|
+
}
|
|
673
|
+
const usage = this.contextMonitor?.getUsage(session.relaySessionId);
|
|
674
|
+
if (usage && usage.usagePercent >= 95) {
|
|
675
|
+
issues.push("high_context_usage");
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
relaySessionId: session.relaySessionId,
|
|
679
|
+
status: session.status,
|
|
680
|
+
backendId: session.backendId,
|
|
681
|
+
healthy: issues.length === 0,
|
|
682
|
+
issues,
|
|
683
|
+
staleSince: isStale ? new Date(lastActivityAtMs + this.staleThresholdMs).toISOString() : void 0,
|
|
684
|
+
isStale,
|
|
685
|
+
lastHeartbeatAt: session.lastHeartbeatAt.toISOString(),
|
|
686
|
+
metadata: session.metadata,
|
|
687
|
+
contextUsage: usage ? {
|
|
688
|
+
usagePercent: usage.usagePercent,
|
|
689
|
+
estimatedTokens: usage.estimatedTokens,
|
|
690
|
+
contextWindow: usage.contextWindow
|
|
691
|
+
} : void 0
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
getLastActivityAtMs(session) {
|
|
695
|
+
return Math.max(
|
|
696
|
+
session.lastHeartbeatAt.getTime(),
|
|
697
|
+
session.updatedAt.getTime()
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
172
704
|
// src/mcp-server/recursion-guard.ts
|
|
173
705
|
import { createHash } from "crypto";
|
|
174
706
|
var RecursionGuard;
|
|
@@ -288,9 +820,9 @@ var init_recursion_guard = __esm({
|
|
|
288
820
|
|
|
289
821
|
// src/mcp-server/tools/spawn-agent.ts
|
|
290
822
|
import { z as z2 } from "zod";
|
|
291
|
-
import { nanoid as
|
|
292
|
-
import { existsSync, readFileSync } from "fs";
|
|
293
|
-
import { join as
|
|
823
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
824
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
825
|
+
import { join as join7, normalize, resolve, sep } from "path";
|
|
294
826
|
function buildContextInjection(metadata) {
|
|
295
827
|
const parts = [];
|
|
296
828
|
if (metadata.stateContent && typeof metadata.stateContent === "string") {
|
|
@@ -309,9 +841,9 @@ ${formatted}
|
|
|
309
841
|
}
|
|
310
842
|
function readPreviousState(dailynoteDir) {
|
|
311
843
|
try {
|
|
312
|
-
const statePath =
|
|
313
|
-
if (
|
|
314
|
-
return
|
|
844
|
+
const statePath = join7(dailynoteDir, "_state.md");
|
|
845
|
+
if (existsSync2(statePath)) {
|
|
846
|
+
return readFileSync2(statePath, "utf-8");
|
|
315
847
|
}
|
|
316
848
|
return null;
|
|
317
849
|
} catch (error) {
|
|
@@ -336,11 +868,11 @@ function validatePathWithinProject(filePath, projectRoot) {
|
|
|
336
868
|
function readAgentDefinition(definitionPath, projectRoot = process.cwd()) {
|
|
337
869
|
try {
|
|
338
870
|
const safeDefinitionPath = validatePathWithinProject(definitionPath, projectRoot);
|
|
339
|
-
if (!
|
|
871
|
+
if (!existsSync2(safeDefinitionPath)) {
|
|
340
872
|
logger.warn(`Agent definition file not found at ${safeDefinitionPath}`);
|
|
341
873
|
return null;
|
|
342
874
|
}
|
|
343
|
-
const content =
|
|
875
|
+
const content = readFileSync2(safeDefinitionPath, "utf-8");
|
|
344
876
|
if (content.trim().length === 0) {
|
|
345
877
|
logger.warn(`Agent definition file is empty at ${safeDefinitionPath}`);
|
|
346
878
|
return null;
|
|
@@ -356,25 +888,25 @@ function readAgentDefinition(definitionPath, projectRoot = process.cwd()) {
|
|
|
356
888
|
function readSkillContext(skillContext, projectRoot = process.cwd()) {
|
|
357
889
|
try {
|
|
358
890
|
const safeSkillPath = validatePathWithinProject(skillContext.skillPath, projectRoot);
|
|
359
|
-
const skillMdPath = validatePathWithinProject(
|
|
360
|
-
if (!
|
|
891
|
+
const skillMdPath = validatePathWithinProject(join7(safeSkillPath, "SKILL.md"), projectRoot);
|
|
892
|
+
if (!existsSync2(skillMdPath)) {
|
|
361
893
|
logger.warn(
|
|
362
894
|
`SKILL.md not found at ${skillMdPath}`
|
|
363
895
|
);
|
|
364
896
|
return null;
|
|
365
897
|
}
|
|
366
898
|
const parts = [];
|
|
367
|
-
const skillContent =
|
|
899
|
+
const skillContent = readFileSync2(skillMdPath, "utf-8");
|
|
368
900
|
parts.push(skillContent);
|
|
369
901
|
if (skillContext.subskill) {
|
|
370
|
-
const subskillPath = validatePathWithinProject(
|
|
902
|
+
const subskillPath = validatePathWithinProject(join7(
|
|
371
903
|
safeSkillPath,
|
|
372
904
|
"subskills",
|
|
373
905
|
skillContext.subskill,
|
|
374
906
|
"SUBSKILL.md"
|
|
375
907
|
), projectRoot);
|
|
376
|
-
if (
|
|
377
|
-
const subskillContent =
|
|
908
|
+
if (existsSync2(subskillPath)) {
|
|
909
|
+
const subskillContent = readFileSync2(subskillPath, "utf-8");
|
|
378
910
|
parts.push(subskillContent);
|
|
379
911
|
} else {
|
|
380
912
|
logger.warn(
|
|
@@ -393,11 +925,11 @@ function readSkillContext(skillContext, projectRoot = process.cwd()) {
|
|
|
393
925
|
function readProjectMcpJson(cwd) {
|
|
394
926
|
try {
|
|
395
927
|
const dir = cwd ?? process.cwd();
|
|
396
|
-
const mcpJsonPath =
|
|
397
|
-
if (!
|
|
928
|
+
const mcpJsonPath = join7(dir, ".mcp.json");
|
|
929
|
+
if (!existsSync2(mcpJsonPath)) {
|
|
398
930
|
return {};
|
|
399
931
|
}
|
|
400
|
-
const raw =
|
|
932
|
+
const raw = readFileSync2(mcpJsonPath, "utf-8");
|
|
401
933
|
const parsed = JSON.parse(raw);
|
|
402
934
|
const servers = parsed.mcpServers;
|
|
403
935
|
if (!servers || typeof servers !== "object") {
|
|
@@ -438,6 +970,17 @@ function buildChildMcpServers(parentMcpServers, childHttpUrl) {
|
|
|
438
970
|
}
|
|
439
971
|
return result;
|
|
440
972
|
}
|
|
973
|
+
function resolveValidatedSessionMetadata(input) {
|
|
974
|
+
const validated = validateMetadata(input.metadata);
|
|
975
|
+
const mergedMetadata = { ...validated };
|
|
976
|
+
if (mergedMetadata.agentType === void 0 && input.agent !== void 0) {
|
|
977
|
+
mergedMetadata.agentType = input.agent;
|
|
978
|
+
}
|
|
979
|
+
if (mergedMetadata.label === void 0 && input.label !== void 0) {
|
|
980
|
+
mergedMetadata.label = input.label;
|
|
981
|
+
}
|
|
982
|
+
return validateMetadata(mergedMetadata);
|
|
983
|
+
}
|
|
441
984
|
function inferFailureReason(stderr, stdout, sdkErrorMetadata) {
|
|
442
985
|
if (sdkErrorMetadata) {
|
|
443
986
|
if (sdkErrorMetadata.subtype === "error_max_turns") return "max_turns_exhausted";
|
|
@@ -449,13 +992,42 @@ function inferFailureReason(stderr, stdout, sdkErrorMetadata) {
|
|
|
449
992
|
if (combined.includes("429") || combined.includes("capacity_exhausted") || combined.includes("model_capacity_exhausted") || combined.includes("ratelimitexceeded") || combined.includes("resource_exhausted")) return "rate_limit";
|
|
450
993
|
return "adapter_error";
|
|
451
994
|
}
|
|
995
|
+
function failureReasonToErrorCode(reason) {
|
|
996
|
+
switch (reason) {
|
|
997
|
+
case "recursion_blocked":
|
|
998
|
+
return "RELAY_RECURSION_BLOCKED";
|
|
999
|
+
case "metadata_validation":
|
|
1000
|
+
return "RELAY_METADATA_VALIDATION";
|
|
1001
|
+
case "backend_unavailable":
|
|
1002
|
+
return "RELAY_BACKEND_UNAVAILABLE";
|
|
1003
|
+
case "instruction_file_error":
|
|
1004
|
+
return "RELAY_INSTRUCTION_FILE_ERROR";
|
|
1005
|
+
case "session_continuation_unsupported":
|
|
1006
|
+
return "RELAY_SESSION_CONTINUATION_UNSUPPORTED";
|
|
1007
|
+
case "session_not_found":
|
|
1008
|
+
return "RELAY_SESSION_NOT_FOUND";
|
|
1009
|
+
case "timeout":
|
|
1010
|
+
return "RELAY_TIMEOUT";
|
|
1011
|
+
case "max_turns_exhausted":
|
|
1012
|
+
return "RELAY_MAX_TURNS_EXHAUSTED";
|
|
1013
|
+
case "rate_limit":
|
|
1014
|
+
return "RELAY_RATE_LIMIT";
|
|
1015
|
+
case "adapter_error":
|
|
1016
|
+
return "RELAY_ADAPTER_ERROR";
|
|
1017
|
+
case "unknown":
|
|
1018
|
+
return "RELAY_UNKNOWN_ERROR";
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function isShortCircuitSpawnResult(value) {
|
|
1022
|
+
return "_noSession" in value;
|
|
1023
|
+
}
|
|
452
1024
|
function buildContextFromEnv() {
|
|
453
|
-
const traceId = process.env["RELAY_TRACE_ID"] ?? `trace-${
|
|
1025
|
+
const traceId = process.env["RELAY_TRACE_ID"] ?? `trace-${nanoid3()}`;
|
|
454
1026
|
const parentSessionId = process.env["RELAY_PARENT_SESSION_ID"] ?? null;
|
|
455
1027
|
const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
|
|
456
1028
|
return { traceId, parentSessionId, depth };
|
|
457
1029
|
}
|
|
458
|
-
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress) {
|
|
1030
|
+
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory") {
|
|
459
1031
|
onProgress?.({ stage: "initializing", percent: 0 });
|
|
460
1032
|
let effectiveBackend;
|
|
461
1033
|
let selectionReason;
|
|
@@ -509,16 +1081,55 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
509
1081
|
};
|
|
510
1082
|
}
|
|
511
1083
|
const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
512
|
-
|
|
1084
|
+
let validatedMetadata;
|
|
1085
|
+
try {
|
|
1086
|
+
validatedMetadata = resolveValidatedSessionMetadata(input);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1089
|
+
return {
|
|
1090
|
+
sessionId: "",
|
|
1091
|
+
exitCode: 1,
|
|
1092
|
+
stdout: "",
|
|
1093
|
+
stderr: `RELAY_METADATA_VALIDATION: ${message}`,
|
|
1094
|
+
failureReason: "metadata_validation"
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
let session;
|
|
1098
|
+
try {
|
|
1099
|
+
session = await sessionManager2.create({
|
|
1100
|
+
backendId: effectiveBackend,
|
|
1101
|
+
parentSessionId: envContext.parentSessionId ?? void 0,
|
|
1102
|
+
depth: envContext.depth + 1,
|
|
1103
|
+
metadata: validatedMetadata
|
|
1104
|
+
});
|
|
1105
|
+
} catch (error) {
|
|
1106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1107
|
+
return {
|
|
1108
|
+
sessionId: "",
|
|
1109
|
+
exitCode: 1,
|
|
1110
|
+
stdout: "",
|
|
1111
|
+
stderr: message,
|
|
1112
|
+
failureReason: "unknown"
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
agentEventStore?.record({
|
|
1116
|
+
type: "session-start",
|
|
1117
|
+
sessionId: session.relaySessionId,
|
|
1118
|
+
parentSessionId: session.parentSessionId ?? void 0,
|
|
513
1119
|
backendId: effectiveBackend,
|
|
514
|
-
|
|
515
|
-
|
|
1120
|
+
metadata: session.metadata,
|
|
1121
|
+
data: {
|
|
1122
|
+
taskId: session.metadata.taskId,
|
|
1123
|
+
label: session.metadata.label,
|
|
1124
|
+
agentType: session.metadata.agentType,
|
|
1125
|
+
selectedBackend: effectiveBackend
|
|
1126
|
+
}
|
|
516
1127
|
});
|
|
517
1128
|
let collectedMetadata = {};
|
|
518
1129
|
if (hooksEngine2 && !input.resumeSessionId) {
|
|
519
1130
|
try {
|
|
520
1131
|
const cwd = process.cwd();
|
|
521
|
-
const dailynoteDir =
|
|
1132
|
+
const dailynoteDir = join7(cwd, "daily_note");
|
|
522
1133
|
const hookInput = {
|
|
523
1134
|
schemaVersion: "1.0",
|
|
524
1135
|
event: "session-init",
|
|
@@ -528,7 +1139,7 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
528
1139
|
data: {
|
|
529
1140
|
workingDirectory: cwd,
|
|
530
1141
|
dailynoteDir,
|
|
531
|
-
isFirstSession: !
|
|
1142
|
+
isFirstSession: !existsSync2(join7(dailynoteDir, "_state.md")),
|
|
532
1143
|
previousState: readPreviousState(dailynoteDir)
|
|
533
1144
|
}
|
|
534
1145
|
};
|
|
@@ -592,7 +1203,7 @@ ${wrapped}` : wrapped;
|
|
|
592
1203
|
try {
|
|
593
1204
|
const projectRoot = process.cwd();
|
|
594
1205
|
const safePath = validatePathWithinProject(input.taskInstructionPath, projectRoot);
|
|
595
|
-
if (!
|
|
1206
|
+
if (!existsSync2(safePath)) {
|
|
596
1207
|
return {
|
|
597
1208
|
sessionId: "",
|
|
598
1209
|
exitCode: 1,
|
|
@@ -601,7 +1212,7 @@ ${wrapped}` : wrapped;
|
|
|
601
1212
|
failureReason: "instruction_file_error"
|
|
602
1213
|
};
|
|
603
1214
|
}
|
|
604
|
-
const instructionContent =
|
|
1215
|
+
const instructionContent = readFileSync2(safePath, "utf-8");
|
|
605
1216
|
effectivePrompt = `${instructionContent}
|
|
606
1217
|
|
|
607
1218
|
${input.prompt}`;
|
|
@@ -617,6 +1228,14 @@ ${input.prompt}`;
|
|
|
617
1228
|
}
|
|
618
1229
|
}
|
|
619
1230
|
onProgress?.({ stage: "spawning", percent: 10 });
|
|
1231
|
+
const heartbeatTimer = setInterval(() => {
|
|
1232
|
+
void sessionManager2.updateHeartbeat(session.relaySessionId).catch((heartbeatError) => {
|
|
1233
|
+
logger.warn(
|
|
1234
|
+
`Internal heartbeat update failed: ${heartbeatError instanceof Error ? heartbeatError.message : String(heartbeatError)}`
|
|
1235
|
+
);
|
|
1236
|
+
});
|
|
1237
|
+
}, 3e4);
|
|
1238
|
+
heartbeatTimer.unref();
|
|
620
1239
|
try {
|
|
621
1240
|
const executePromise = (async () => {
|
|
622
1241
|
if (input.resumeSessionId) {
|
|
@@ -678,17 +1297,30 @@ ${input.prompt}`;
|
|
|
678
1297
|
});
|
|
679
1298
|
}
|
|
680
1299
|
})();
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
stdout: result.stdout,
|
|
687
|
-
stderr: result.stderr,
|
|
688
|
-
..."_failureReason" in result ? { failureReason: result._failureReason } : {}
|
|
689
|
-
};
|
|
1300
|
+
let rawResult;
|
|
1301
|
+
try {
|
|
1302
|
+
rawResult = await executePromise;
|
|
1303
|
+
} finally {
|
|
1304
|
+
clearInterval(heartbeatTimer);
|
|
690
1305
|
}
|
|
691
1306
|
onProgress?.({ stage: "executing", percent: 50 });
|
|
1307
|
+
let isShortCircuit = false;
|
|
1308
|
+
let failureReasonFromShortCircuit;
|
|
1309
|
+
let result;
|
|
1310
|
+
if (isShortCircuitSpawnResult(rawResult)) {
|
|
1311
|
+
isShortCircuit = true;
|
|
1312
|
+
failureReasonFromShortCircuit = rawResult._failureReason;
|
|
1313
|
+
result = {
|
|
1314
|
+
exitCode: rawResult.exitCode,
|
|
1315
|
+
stdout: rawResult.stdout,
|
|
1316
|
+
stderr: rawResult.stderr,
|
|
1317
|
+
nativeSessionId: void 0,
|
|
1318
|
+
tokenUsage: void 0,
|
|
1319
|
+
sdkErrorMetadata: void 0
|
|
1320
|
+
};
|
|
1321
|
+
} else {
|
|
1322
|
+
result = rawResult;
|
|
1323
|
+
}
|
|
692
1324
|
if (contextMonitor2) {
|
|
693
1325
|
const estimatedTokens = Math.ceil(
|
|
694
1326
|
(result.stdout.length + result.stderr.length) / 4
|
|
@@ -696,16 +1328,28 @@ ${input.prompt}`;
|
|
|
696
1328
|
contextMonitor2.updateUsage(
|
|
697
1329
|
session.relaySessionId,
|
|
698
1330
|
effectiveBackend,
|
|
699
|
-
estimatedTokens
|
|
1331
|
+
estimatedTokens,
|
|
1332
|
+
session.metadata
|
|
700
1333
|
);
|
|
1334
|
+
await sessionManager2.updateHeartbeat(session.relaySessionId).catch(() => void 0);
|
|
1335
|
+
}
|
|
1336
|
+
if (!isShortCircuit) {
|
|
1337
|
+
guard.recordSpawn(context);
|
|
701
1338
|
}
|
|
702
|
-
guard.recordSpawn(context);
|
|
703
1339
|
const status = result.exitCode === 0 ? "completed" : "error";
|
|
704
|
-
const failureReason = result.exitCode !== 0 ? inferFailureReason(result.stderr, result.stdout, result.sdkErrorMetadata) : void 0;
|
|
1340
|
+
const failureReason = result.exitCode !== 0 ? failureReasonFromShortCircuit ?? inferFailureReason(result.stderr, result.stdout, result.sdkErrorMetadata) : void 0;
|
|
705
1341
|
await sessionManager2.update(session.relaySessionId, {
|
|
706
1342
|
status,
|
|
707
|
-
...result.nativeSessionId ? { nativeSessionId: result.nativeSessionId } : {}
|
|
1343
|
+
...result.nativeSessionId ? { nativeSessionId: result.nativeSessionId } : {},
|
|
1344
|
+
...status === "error" ? {
|
|
1345
|
+
errorMessage: result.stderr.slice(0, 500),
|
|
1346
|
+
errorCode: failureReason ? failureReasonToErrorCode(failureReason) : "RELAY_UNKNOWN_ERROR"
|
|
1347
|
+
} : {
|
|
1348
|
+
errorMessage: void 0,
|
|
1349
|
+
errorCode: void 0
|
|
1350
|
+
}
|
|
708
1351
|
});
|
|
1352
|
+
contextMonitor2?.removeSession(session.relaySessionId);
|
|
709
1353
|
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
710
1354
|
const metadata = {
|
|
711
1355
|
durationMs: new Date(completedAt).getTime() - new Date(spawnStartedAt).getTime(),
|
|
@@ -716,6 +1360,103 @@ ${input.prompt}`;
|
|
|
716
1360
|
completedAt,
|
|
717
1361
|
...result.tokenUsage ? { tokenUsage: result.tokenUsage } : {}
|
|
718
1362
|
};
|
|
1363
|
+
if (status === "completed") {
|
|
1364
|
+
agentEventStore?.record({
|
|
1365
|
+
type: "session-complete",
|
|
1366
|
+
sessionId: session.relaySessionId,
|
|
1367
|
+
parentSessionId: session.parentSessionId ?? void 0,
|
|
1368
|
+
backendId: effectiveBackend,
|
|
1369
|
+
metadata: session.metadata,
|
|
1370
|
+
data: {
|
|
1371
|
+
exitCode: result.exitCode,
|
|
1372
|
+
durationMs: metadata.durationMs,
|
|
1373
|
+
taskId: session.metadata.taskId,
|
|
1374
|
+
label: session.metadata.label,
|
|
1375
|
+
agentType: session.metadata.agentType,
|
|
1376
|
+
nativeSessionId: result.nativeSessionId,
|
|
1377
|
+
selectedBackend: effectiveBackend,
|
|
1378
|
+
tokenUsage: result.tokenUsage
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
if (hooksEngine2) {
|
|
1382
|
+
try {
|
|
1383
|
+
await hooksEngine2.emit("on-session-complete", {
|
|
1384
|
+
event: "on-session-complete",
|
|
1385
|
+
sessionId: session.relaySessionId,
|
|
1386
|
+
backendId: effectiveBackend,
|
|
1387
|
+
timestamp: completedAt,
|
|
1388
|
+
data: {
|
|
1389
|
+
exitCode: result.exitCode,
|
|
1390
|
+
durationMs: metadata.durationMs,
|
|
1391
|
+
taskId: session.metadata.taskId,
|
|
1392
|
+
label: session.metadata.label,
|
|
1393
|
+
agentType: session.metadata.agentType,
|
|
1394
|
+
nativeSessionId: result.nativeSessionId,
|
|
1395
|
+
selectedBackend: effectiveBackend,
|
|
1396
|
+
tokenUsage: result.tokenUsage,
|
|
1397
|
+
memoryDir: hookMemoryDir
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
} catch (hookError) {
|
|
1401
|
+
logger.debug(
|
|
1402
|
+
`on-session-complete hook error: ${hookError instanceof Error ? hookError.message : String(hookError)}`
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
} else if (failureReason) {
|
|
1407
|
+
const retryableReasons = [
|
|
1408
|
+
"timeout",
|
|
1409
|
+
"rate_limit",
|
|
1410
|
+
"adapter_error",
|
|
1411
|
+
"unknown"
|
|
1412
|
+
];
|
|
1413
|
+
const isRetryable = retryableReasons.includes(failureReason);
|
|
1414
|
+
const errorMessage = result.stderr.slice(0, 500);
|
|
1415
|
+
agentEventStore?.record({
|
|
1416
|
+
type: "session-error",
|
|
1417
|
+
sessionId: session.relaySessionId,
|
|
1418
|
+
parentSessionId: session.parentSessionId ?? void 0,
|
|
1419
|
+
backendId: effectiveBackend,
|
|
1420
|
+
metadata: session.metadata,
|
|
1421
|
+
data: {
|
|
1422
|
+
exitCode: result.exitCode,
|
|
1423
|
+
failureReason,
|
|
1424
|
+
errorMessage,
|
|
1425
|
+
durationMs: metadata.durationMs,
|
|
1426
|
+
taskId: session.metadata.taskId,
|
|
1427
|
+
label: session.metadata.label,
|
|
1428
|
+
agentType: session.metadata.agentType,
|
|
1429
|
+
selectedBackend: effectiveBackend,
|
|
1430
|
+
isRetryable
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
if (hooksEngine2) {
|
|
1434
|
+
try {
|
|
1435
|
+
await hooksEngine2.emit("on-session-error", {
|
|
1436
|
+
event: "on-session-error",
|
|
1437
|
+
sessionId: session.relaySessionId,
|
|
1438
|
+
backendId: effectiveBackend,
|
|
1439
|
+
timestamp: completedAt,
|
|
1440
|
+
data: {
|
|
1441
|
+
exitCode: result.exitCode,
|
|
1442
|
+
failureReason,
|
|
1443
|
+
errorMessage,
|
|
1444
|
+
durationMs: metadata.durationMs,
|
|
1445
|
+
taskId: session.metadata.taskId,
|
|
1446
|
+
label: session.metadata.label,
|
|
1447
|
+
agentType: session.metadata.agentType,
|
|
1448
|
+
selectedBackend: effectiveBackend,
|
|
1449
|
+
isRetryable,
|
|
1450
|
+
memoryDir: hookMemoryDir
|
|
1451
|
+
}
|
|
1452
|
+
});
|
|
1453
|
+
} catch (hookError) {
|
|
1454
|
+
logger.debug(
|
|
1455
|
+
`on-session-error hook error: ${hookError instanceof Error ? hookError.message : String(hookError)}`
|
|
1456
|
+
);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
719
1460
|
onProgress?.({ stage: "completed", percent: 100 });
|
|
720
1461
|
if (hooksEngine2) {
|
|
721
1462
|
try {
|
|
@@ -758,9 +1499,69 @@ ${input.prompt}`;
|
|
|
758
1499
|
...failureReason ? { failureReason } : {}
|
|
759
1500
|
};
|
|
760
1501
|
} catch (error) {
|
|
761
|
-
|
|
1502
|
+
clearInterval(heartbeatTimer);
|
|
762
1503
|
const message = error instanceof Error ? error.message : String(error);
|
|
763
1504
|
const catchFailureReason = message.toLowerCase().includes("timed out") || message.toLowerCase().includes("timeout") ? "timeout" : "unknown";
|
|
1505
|
+
await sessionManager2.update(session.relaySessionId, {
|
|
1506
|
+
status: "error",
|
|
1507
|
+
errorMessage: message.slice(0, 500),
|
|
1508
|
+
errorCode: failureReasonToErrorCode(catchFailureReason)
|
|
1509
|
+
});
|
|
1510
|
+
contextMonitor2?.removeSession(session.relaySessionId);
|
|
1511
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1512
|
+
const durationMs = new Date(completedAt).getTime() - new Date(spawnStartedAt).getTime();
|
|
1513
|
+
const errorMessage = message.slice(0, 500);
|
|
1514
|
+
const retryableReasons = [
|
|
1515
|
+
"timeout",
|
|
1516
|
+
"rate_limit",
|
|
1517
|
+
"adapter_error",
|
|
1518
|
+
"unknown"
|
|
1519
|
+
];
|
|
1520
|
+
const isRetryable = retryableReasons.includes(catchFailureReason);
|
|
1521
|
+
agentEventStore?.record({
|
|
1522
|
+
type: "session-error",
|
|
1523
|
+
sessionId: session.relaySessionId,
|
|
1524
|
+
parentSessionId: session.parentSessionId ?? void 0,
|
|
1525
|
+
backendId: effectiveBackend,
|
|
1526
|
+
metadata: session.metadata,
|
|
1527
|
+
data: {
|
|
1528
|
+
exitCode: 1,
|
|
1529
|
+
failureReason: catchFailureReason,
|
|
1530
|
+
errorMessage,
|
|
1531
|
+
durationMs,
|
|
1532
|
+
taskId: session.metadata.taskId,
|
|
1533
|
+
label: session.metadata.label,
|
|
1534
|
+
agentType: session.metadata.agentType,
|
|
1535
|
+
selectedBackend: effectiveBackend,
|
|
1536
|
+
isRetryable
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
if (hooksEngine2) {
|
|
1540
|
+
try {
|
|
1541
|
+
await hooksEngine2.emit("on-session-error", {
|
|
1542
|
+
event: "on-session-error",
|
|
1543
|
+
sessionId: session.relaySessionId,
|
|
1544
|
+
backendId: effectiveBackend,
|
|
1545
|
+
timestamp: completedAt,
|
|
1546
|
+
data: {
|
|
1547
|
+
exitCode: 1,
|
|
1548
|
+
failureReason: catchFailureReason,
|
|
1549
|
+
errorMessage,
|
|
1550
|
+
durationMs,
|
|
1551
|
+
taskId: session.metadata.taskId,
|
|
1552
|
+
label: session.metadata.label,
|
|
1553
|
+
agentType: session.metadata.agentType,
|
|
1554
|
+
selectedBackend: effectiveBackend,
|
|
1555
|
+
isRetryable,
|
|
1556
|
+
memoryDir: hookMemoryDir
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
} catch (hookError) {
|
|
1560
|
+
logger.debug(
|
|
1561
|
+
`on-session-error hook error: ${hookError instanceof Error ? hookError.message : String(hookError)}`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
764
1565
|
return {
|
|
765
1566
|
sessionId: session.relaySessionId,
|
|
766
1567
|
exitCode: 1,
|
|
@@ -776,6 +1577,7 @@ var init_spawn_agent = __esm({
|
|
|
776
1577
|
"use strict";
|
|
777
1578
|
init_recursion_guard();
|
|
778
1579
|
init_logger();
|
|
1580
|
+
init_metadata_validation();
|
|
779
1581
|
spawnAgentInputSchema = z2.object({
|
|
780
1582
|
fallbackBackend: z2.enum(["claude", "codex", "gemini"]).optional().describe(
|
|
781
1583
|
"Optional fallback backend. Used only when BackendSelector is not active or cannot determine a backend. When BackendSelector is active, backend is auto-selected by priority: preferredBackend > agentType mapping > taskType mapping > default (claude)."
|
|
@@ -811,7 +1613,14 @@ var init_spawn_agent = __esm({
|
|
|
811
1613
|
taskInstructionPath: z2.string().optional().describe(
|
|
812
1614
|
"Path to a file containing task instructions. Content is prepended to the prompt. Path is resolved relative to the project root and validated against path traversal."
|
|
813
1615
|
),
|
|
814
|
-
label: z2.string().optional().describe("Human-readable label for identifying this agent in parallel results and logs.")
|
|
1616
|
+
label: z2.string().optional().describe("Human-readable label for identifying this agent in parallel results and logs."),
|
|
1617
|
+
metadata: z2.object({
|
|
1618
|
+
taskId: z2.string().regex(TASK_ID_PATTERN).optional(),
|
|
1619
|
+
agentType: z2.string().optional(),
|
|
1620
|
+
label: z2.string().optional(),
|
|
1621
|
+
parentTaskId: z2.string().optional(),
|
|
1622
|
+
tags: z2.array(z2.string()).optional()
|
|
1623
|
+
}).catchall(z2.unknown()).optional().describe("Session metadata for task linkage and orchestration context.")
|
|
815
1624
|
});
|
|
816
1625
|
}
|
|
817
1626
|
});
|
|
@@ -908,7 +1717,31 @@ var init_conflict_detector = __esm({
|
|
|
908
1717
|
});
|
|
909
1718
|
|
|
910
1719
|
// src/mcp-server/tools/spawn-agents-parallel.ts
|
|
911
|
-
async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress) {
|
|
1720
|
+
async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress, agentEventStore, hookMemoryDir = "./memory") {
|
|
1721
|
+
for (let index = 0; index < agents.length; index += 1) {
|
|
1722
|
+
try {
|
|
1723
|
+
resolveValidatedSessionMetadata(agents[index]);
|
|
1724
|
+
} catch (error) {
|
|
1725
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1726
|
+
const reason = `RELAY_METADATA_VALIDATION: agent index ${index}: ${message}`;
|
|
1727
|
+
return {
|
|
1728
|
+
results: agents.map((agent, resultIndex) => ({
|
|
1729
|
+
index: resultIndex,
|
|
1730
|
+
sessionId: "",
|
|
1731
|
+
exitCode: 1,
|
|
1732
|
+
stdout: "",
|
|
1733
|
+
stderr: reason,
|
|
1734
|
+
error: reason,
|
|
1735
|
+
failureReason: "metadata_validation",
|
|
1736
|
+
...agent.label ? { label: agent.label } : {},
|
|
1737
|
+
originalInput: agent
|
|
1738
|
+
})),
|
|
1739
|
+
totalCount: agents.length,
|
|
1740
|
+
successCount: 0,
|
|
1741
|
+
failureCount: agents.length
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
912
1745
|
const envContext = buildContextFromEnv();
|
|
913
1746
|
if (envContext.depth >= guard.getConfig().maxDepth) {
|
|
914
1747
|
const reason = `Max depth exceeded: ${envContext.depth} >= ${guard.getConfig().maxDepth}`;
|
|
@@ -964,7 +1797,10 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
|
|
|
964
1797
|
hooksEngine2,
|
|
965
1798
|
contextMonitor2,
|
|
966
1799
|
backendSelector,
|
|
967
|
-
childHttpUrl
|
|
1800
|
+
childHttpUrl,
|
|
1801
|
+
void 0,
|
|
1802
|
+
agentEventStore,
|
|
1803
|
+
hookMemoryDir
|
|
968
1804
|
).then((result) => {
|
|
969
1805
|
completedCount++;
|
|
970
1806
|
onProgress?.({
|
|
@@ -1034,17 +1870,35 @@ var init_spawn_agents_parallel = __esm({
|
|
|
1034
1870
|
|
|
1035
1871
|
// src/mcp-server/tools/list-sessions.ts
|
|
1036
1872
|
import { z as z3 } from "zod";
|
|
1037
|
-
async function executeListSessions(input, sessionManager2) {
|
|
1873
|
+
async function executeListSessions(input, sessionManager2, options) {
|
|
1874
|
+
const validatedInput = listSessionsInputSchema.parse(input);
|
|
1875
|
+
const normalizedBackendId = validatedInput.backendId ?? validatedInput.backend;
|
|
1876
|
+
if (validatedInput.backendId && validatedInput.backend && validatedInput.backendId !== validatedInput.backend) {
|
|
1877
|
+
logger.warn(
|
|
1878
|
+
`list_sessions: both backendId and backend were provided; backendId="${validatedInput.backendId}" is used`
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
const staleThresholdMs = (options?.staleThresholdSec ?? 300) * 1e3;
|
|
1038
1882
|
const sessions = await sessionManager2.list({
|
|
1039
|
-
backendId:
|
|
1040
|
-
limit:
|
|
1883
|
+
backendId: normalizedBackendId,
|
|
1884
|
+
limit: validatedInput.limit,
|
|
1885
|
+
status: validatedInput.staleOnly ? "active" : validatedInput.status,
|
|
1886
|
+
taskId: validatedInput.taskId,
|
|
1887
|
+
label: validatedInput.label,
|
|
1888
|
+
tags: validatedInput.tags,
|
|
1889
|
+
staleOnly: validatedInput.staleOnly ?? false,
|
|
1890
|
+
staleThresholdMs
|
|
1041
1891
|
});
|
|
1042
1892
|
return {
|
|
1043
1893
|
sessions: sessions.map((s) => ({
|
|
1044
1894
|
relaySessionId: s.relaySessionId,
|
|
1045
1895
|
backendId: s.backendId,
|
|
1046
1896
|
status: s.status,
|
|
1047
|
-
createdAt: s.createdAt.toISOString()
|
|
1897
|
+
createdAt: s.createdAt.toISOString(),
|
|
1898
|
+
updatedAt: s.updatedAt.toISOString(),
|
|
1899
|
+
lastHeartbeatAt: s.lastHeartbeatAt.toISOString(),
|
|
1900
|
+
isStale: s.status === "active" && s.lastHeartbeatAt.getTime() + staleThresholdMs < Date.now(),
|
|
1901
|
+
metadata: s.metadata
|
|
1048
1902
|
}))
|
|
1049
1903
|
};
|
|
1050
1904
|
}
|
|
@@ -1052,15 +1906,82 @@ var listSessionsInputSchema;
|
|
|
1052
1906
|
var init_list_sessions = __esm({
|
|
1053
1907
|
"src/mcp-server/tools/list-sessions.ts"() {
|
|
1054
1908
|
"use strict";
|
|
1909
|
+
init_logger();
|
|
1910
|
+
init_metadata_validation();
|
|
1055
1911
|
listSessionsInputSchema = z3.object({
|
|
1912
|
+
backendId: z3.enum(["claude", "codex", "gemini"]).optional(),
|
|
1056
1913
|
backend: z3.enum(["claude", "codex", "gemini"]).optional(),
|
|
1057
|
-
limit: z3.number().optional().default(10)
|
|
1914
|
+
limit: z3.number().int().min(1).max(100).optional().default(10),
|
|
1915
|
+
status: z3.enum(["active", "completed", "error"]).optional(),
|
|
1916
|
+
taskId: z3.string().regex(TASK_ID_PATTERN).optional(),
|
|
1917
|
+
label: z3.string().optional(),
|
|
1918
|
+
tags: z3.array(z3.string()).optional(),
|
|
1919
|
+
staleOnly: z3.boolean().optional().default(false)
|
|
1058
1920
|
});
|
|
1059
1921
|
}
|
|
1060
1922
|
});
|
|
1061
1923
|
|
|
1062
|
-
// src/mcp-server/tools/
|
|
1924
|
+
// src/mcp-server/tools/check-session-health.ts
|
|
1063
1925
|
import { z as z4 } from "zod";
|
|
1926
|
+
async function executeCheckSessionHealth(input, sessionHealthMonitor) {
|
|
1927
|
+
return sessionHealthMonitor.checkHealth({
|
|
1928
|
+
sessionId: input.sessionId,
|
|
1929
|
+
includeCompleted: input.includeCompleted ?? false
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
var checkSessionHealthInputSchema;
|
|
1933
|
+
var init_check_session_health = __esm({
|
|
1934
|
+
"src/mcp-server/tools/check-session-health.ts"() {
|
|
1935
|
+
"use strict";
|
|
1936
|
+
checkSessionHealthInputSchema = z4.object({
|
|
1937
|
+
sessionId: z4.string().optional().describe("Specific session to inspect. Omit to check all sessions."),
|
|
1938
|
+
includeCompleted: z4.boolean().optional().default(false).describe("When true, include completed/error sessions in addition to active ones.")
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
// src/mcp-server/tools/poll-agent-events.ts
|
|
1944
|
+
import { z as z5 } from "zod";
|
|
1945
|
+
function executePollAgentEvents(input, agentEventStore) {
|
|
1946
|
+
const limit = input.limit ?? 50;
|
|
1947
|
+
const result = agentEventStore.query({
|
|
1948
|
+
afterEventId: input.cursor,
|
|
1949
|
+
types: input.types,
|
|
1950
|
+
sessionId: input.sessionId,
|
|
1951
|
+
parentSessionId: input.parentSessionId,
|
|
1952
|
+
recursive: false,
|
|
1953
|
+
limit
|
|
1954
|
+
});
|
|
1955
|
+
const lastEventId = result.events.length > 0 ? result.events[result.events.length - 1].eventId : input.cursor ?? null;
|
|
1956
|
+
return {
|
|
1957
|
+
events: result.events,
|
|
1958
|
+
lastEventId,
|
|
1959
|
+
hasMore: result.hasMore
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
var EVENT_TYPE_VALUES, pollAgentEventsInputSchema;
|
|
1963
|
+
var init_poll_agent_events = __esm({
|
|
1964
|
+
"src/mcp-server/tools/poll-agent-events.ts"() {
|
|
1965
|
+
"use strict";
|
|
1966
|
+
EVENT_TYPE_VALUES = [
|
|
1967
|
+
"session-start",
|
|
1968
|
+
"session-complete",
|
|
1969
|
+
"session-error",
|
|
1970
|
+
"session-stale",
|
|
1971
|
+
"context-threshold"
|
|
1972
|
+
];
|
|
1973
|
+
pollAgentEventsInputSchema = z5.object({
|
|
1974
|
+
cursor: z5.string().optional().describe("Exclusive cursor (previous lastEventId). Omit to start from oldest."),
|
|
1975
|
+
types: z5.array(z5.enum(EVENT_TYPE_VALUES)).optional().describe("Filter by event types."),
|
|
1976
|
+
sessionId: z5.string().optional().describe("Filter by session ID."),
|
|
1977
|
+
parentSessionId: z5.string().optional().describe("Filter to direct child sessions of this parent session."),
|
|
1978
|
+
limit: z5.number().int().min(1).max(200).optional().default(50).describe("Maximum number of events to return.")
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
// src/mcp-server/tools/get-context-status.ts
|
|
1984
|
+
import { z as z6 } from "zod";
|
|
1064
1985
|
async function executeGetContextStatus(input, sessionManager2, contextMonitor2) {
|
|
1065
1986
|
const session = await sessionManager2.get(input.sessionId);
|
|
1066
1987
|
if (!session) {
|
|
@@ -1092,8 +2013,8 @@ var getContextStatusInputSchema;
|
|
|
1092
2013
|
var init_get_context_status = __esm({
|
|
1093
2014
|
"src/mcp-server/tools/get-context-status.ts"() {
|
|
1094
2015
|
"use strict";
|
|
1095
|
-
getContextStatusInputSchema =
|
|
1096
|
-
sessionId:
|
|
2016
|
+
getContextStatusInputSchema = z6.object({
|
|
2017
|
+
sessionId: z6.string()
|
|
1097
2018
|
});
|
|
1098
2019
|
}
|
|
1099
2020
|
});
|
|
@@ -1366,7 +2287,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
1366
2287
|
import { InMemoryTaskMessageQueue } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
1367
2288
|
import { createServer } from "http";
|
|
1368
2289
|
import { randomUUID } from "crypto";
|
|
1369
|
-
import { z as
|
|
2290
|
+
import { z as z7 } from "zod";
|
|
1370
2291
|
function createMcpServerOptions() {
|
|
1371
2292
|
const taskStore = new DeferredCleanupTaskStore();
|
|
1372
2293
|
return {
|
|
@@ -1381,41 +2302,79 @@ var init_server = __esm({
|
|
|
1381
2302
|
"src/mcp-server/server.ts"() {
|
|
1382
2303
|
"use strict";
|
|
1383
2304
|
init_deferred_cleanup_task_store();
|
|
2305
|
+
init_agent_event_store();
|
|
2306
|
+
init_session_health_monitor();
|
|
1384
2307
|
init_recursion_guard();
|
|
1385
2308
|
init_spawn_agent();
|
|
1386
2309
|
init_spawn_agents_parallel();
|
|
1387
2310
|
init_list_sessions();
|
|
2311
|
+
init_check_session_health();
|
|
2312
|
+
init_poll_agent_events();
|
|
1388
2313
|
init_get_context_status();
|
|
1389
2314
|
init_list_available_backends();
|
|
1390
2315
|
init_backend_selector();
|
|
2316
|
+
init_metadata_validation();
|
|
1391
2317
|
init_logger();
|
|
1392
2318
|
init_types();
|
|
1393
2319
|
init_response_formatter();
|
|
1394
2320
|
spawnAgentsParallelInputShape = {
|
|
1395
|
-
agents:
|
|
2321
|
+
agents: z7.array(spawnAgentInputSchema).min(1).max(10).describe(
|
|
1396
2322
|
"Array of agent configurations to execute in parallel (1-10 agents)"
|
|
1397
2323
|
)
|
|
1398
2324
|
};
|
|
1399
2325
|
MAX_CHILD_HTTP_SESSIONS = 100;
|
|
1400
2326
|
RelayMCPServer = class {
|
|
1401
|
-
constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2, inlineSummaryLength, responseOutputDir) {
|
|
2327
|
+
constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2, inlineSummaryLength, responseOutputDir, relayConfig) {
|
|
1402
2328
|
this.registry = registry2;
|
|
1403
2329
|
this.sessionManager = sessionManager2;
|
|
1404
2330
|
this.hooksEngine = hooksEngine2;
|
|
1405
2331
|
this.contextMonitor = contextMonitor2;
|
|
1406
2332
|
this.inlineSummaryLength = inlineSummaryLength;
|
|
1407
2333
|
this.responseOutputDir = responseOutputDir;
|
|
2334
|
+
this.relayConfig = relayConfig;
|
|
1408
2335
|
this.guard = new RecursionGuard(guardConfig);
|
|
1409
2336
|
this.backendSelector = new BackendSelector();
|
|
2337
|
+
this.staleThresholdSec = relayConfig?.sessionHealth?.staleThresholdSec ?? 300;
|
|
2338
|
+
this.hookMemoryDir = relayConfig?.hooks?.memoryDir ?? "./memory";
|
|
2339
|
+
this.agentEventStore = new AgentEventStore({
|
|
2340
|
+
backend: relayConfig?.eventStore?.backend,
|
|
2341
|
+
maxEvents: relayConfig?.eventStore?.maxEvents,
|
|
2342
|
+
ttlSec: relayConfig?.eventStore?.ttlSec,
|
|
2343
|
+
sessionDir: relayConfig?.eventStore?.sessionDir ?? this.sessionManager.getSessionsDir(),
|
|
2344
|
+
eventsFileName: relayConfig?.eventStore?.eventsFileName
|
|
2345
|
+
});
|
|
2346
|
+
if (this.contextMonitor) {
|
|
2347
|
+
this.contextMonitor.setAgentEventStore(this.agentEventStore);
|
|
2348
|
+
}
|
|
2349
|
+
this.sessionHealthMonitor = new SessionHealthMonitor(
|
|
2350
|
+
{
|
|
2351
|
+
enabled: relayConfig?.sessionHealth?.enabled,
|
|
2352
|
+
heartbeatIntervalSec: relayConfig?.sessionHealth?.heartbeatIntervalSec,
|
|
2353
|
+
staleThresholdSec: relayConfig?.sessionHealth?.staleThresholdSec,
|
|
2354
|
+
cleanupAfterSec: relayConfig?.sessionHealth?.cleanupAfterSec,
|
|
2355
|
+
maxActiveSessions: relayConfig?.sessionHealth?.maxActiveSessions,
|
|
2356
|
+
checkIntervalSec: relayConfig?.sessionHealth?.checkIntervalSec,
|
|
2357
|
+
memoryDir: this.hookMemoryDir
|
|
2358
|
+
},
|
|
2359
|
+
this.sessionManager,
|
|
2360
|
+
this.hooksEngine ?? null,
|
|
2361
|
+
this.contextMonitor ?? null,
|
|
2362
|
+
this.agentEventStore
|
|
2363
|
+
);
|
|
1410
2364
|
this.server = new McpServer(
|
|
1411
|
-
{ name: "agentic-relay", version: "1.
|
|
2365
|
+
{ name: "agentic-relay", version: "1.3.0" },
|
|
1412
2366
|
createMcpServerOptions()
|
|
1413
2367
|
);
|
|
1414
2368
|
this.registerTools(this.server);
|
|
2369
|
+
this.sessionHealthMonitor.start();
|
|
1415
2370
|
}
|
|
1416
2371
|
server;
|
|
1417
2372
|
guard;
|
|
1418
2373
|
backendSelector;
|
|
2374
|
+
agentEventStore;
|
|
2375
|
+
sessionHealthMonitor;
|
|
2376
|
+
staleThresholdSec;
|
|
2377
|
+
hookMemoryDir;
|
|
1419
2378
|
_childHttpServer;
|
|
1420
2379
|
_childHttpUrl;
|
|
1421
2380
|
/** URL for child agents to connect via HTTP. Available after start() in stdio mode. */
|
|
@@ -1441,7 +2400,10 @@ var init_server = __esm({
|
|
|
1441
2400
|
this.hooksEngine,
|
|
1442
2401
|
this.contextMonitor,
|
|
1443
2402
|
this.backendSelector,
|
|
1444
|
-
this._childHttpUrl
|
|
2403
|
+
this._childHttpUrl,
|
|
2404
|
+
void 0,
|
|
2405
|
+
this.agentEventStore,
|
|
2406
|
+
this.hookMemoryDir
|
|
1445
2407
|
);
|
|
1446
2408
|
const controlOptions = {
|
|
1447
2409
|
inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
|
|
@@ -1493,7 +2455,10 @@ var init_server = __esm({
|
|
|
1493
2455
|
this.hooksEngine,
|
|
1494
2456
|
this.contextMonitor,
|
|
1495
2457
|
this.backendSelector,
|
|
1496
|
-
this._childHttpUrl
|
|
2458
|
+
this._childHttpUrl,
|
|
2459
|
+
void 0,
|
|
2460
|
+
this.agentEventStore,
|
|
2461
|
+
this.hookMemoryDir
|
|
1497
2462
|
);
|
|
1498
2463
|
const controlOptions = {
|
|
1499
2464
|
inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
|
|
@@ -1586,12 +2551,12 @@ var init_server = __esm({
|
|
|
1586
2551
|
"retry_failed_agents",
|
|
1587
2552
|
"Retry only the failed agents from a previous spawn_agents_parallel call. Pass the failed results array (with originalInput) directly.",
|
|
1588
2553
|
{
|
|
1589
|
-
failedResults:
|
|
1590
|
-
index:
|
|
2554
|
+
failedResults: z7.array(z7.object({
|
|
2555
|
+
index: z7.number(),
|
|
1591
2556
|
originalInput: spawnAgentInputSchema
|
|
1592
2557
|
})).min(1).describe("Array of failed results with their original input configurations"),
|
|
1593
|
-
overrides:
|
|
1594
|
-
preferredBackend:
|
|
2558
|
+
overrides: z7.object({
|
|
2559
|
+
preferredBackend: z7.enum(["claude", "codex", "gemini"]).optional()
|
|
1595
2560
|
}).optional().describe("Parameter overrides applied to all retried agents")
|
|
1596
2561
|
},
|
|
1597
2562
|
async (params) => {
|
|
@@ -1611,7 +2576,10 @@ var init_server = __esm({
|
|
|
1611
2576
|
this.hooksEngine,
|
|
1612
2577
|
this.contextMonitor,
|
|
1613
2578
|
this.backendSelector,
|
|
1614
|
-
this._childHttpUrl
|
|
2579
|
+
this._childHttpUrl,
|
|
2580
|
+
void 0,
|
|
2581
|
+
this.agentEventStore,
|
|
2582
|
+
this.hookMemoryDir
|
|
1615
2583
|
);
|
|
1616
2584
|
const controlOptions = {
|
|
1617
2585
|
inlineSummaryLength: this.inlineSummaryLength ?? DEFAULT_INLINE_SUMMARY_LENGTH,
|
|
@@ -1635,14 +2603,93 @@ var init_server = __esm({
|
|
|
1635
2603
|
"list_sessions",
|
|
1636
2604
|
"List relay sessions, optionally filtered by backend.",
|
|
1637
2605
|
{
|
|
1638
|
-
|
|
1639
|
-
|
|
2606
|
+
backendId: z7.enum(["claude", "codex", "gemini"]).optional().describe("Filter sessions by backend type."),
|
|
2607
|
+
backend: z7.enum(["claude", "codex", "gemini"]).optional().describe("Filter sessions by backend type."),
|
|
2608
|
+
limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of sessions to return. Default: 10."),
|
|
2609
|
+
status: z7.enum(["active", "completed", "error"]).optional().describe("Filter sessions by status."),
|
|
2610
|
+
taskId: z7.string().regex(TASK_ID_PATTERN).optional().describe("Filter by metadata.taskId."),
|
|
2611
|
+
label: z7.string().optional().describe("Case-insensitive partial match for metadata.label."),
|
|
2612
|
+
tags: z7.array(z7.string()).optional().describe("Filter sessions containing all provided tags."),
|
|
2613
|
+
staleOnly: z7.boolean().optional().describe("When true, return only stale active sessions.")
|
|
1640
2614
|
},
|
|
1641
2615
|
async (params) => {
|
|
1642
2616
|
try {
|
|
1643
2617
|
const result = await executeListSessions(
|
|
1644
|
-
{
|
|
1645
|
-
|
|
2618
|
+
{
|
|
2619
|
+
backendId: params.backendId,
|
|
2620
|
+
backend: params.backend,
|
|
2621
|
+
limit: params.limit ?? 10,
|
|
2622
|
+
status: params.status,
|
|
2623
|
+
taskId: params.taskId,
|
|
2624
|
+
label: params.label,
|
|
2625
|
+
tags: params.tags,
|
|
2626
|
+
staleOnly: params.staleOnly ?? false
|
|
2627
|
+
},
|
|
2628
|
+
this.sessionManager,
|
|
2629
|
+
{ staleThresholdSec: this.staleThresholdSec }
|
|
2630
|
+
);
|
|
2631
|
+
return {
|
|
2632
|
+
content: [
|
|
2633
|
+
{
|
|
2634
|
+
type: "text",
|
|
2635
|
+
text: JSON.stringify(result, null, 2)
|
|
2636
|
+
}
|
|
2637
|
+
]
|
|
2638
|
+
};
|
|
2639
|
+
} catch (error) {
|
|
2640
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2641
|
+
return {
|
|
2642
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2643
|
+
isError: true
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
);
|
|
2648
|
+
server.tool(
|
|
2649
|
+
"check_session_health",
|
|
2650
|
+
"Check relay session health without side effects.",
|
|
2651
|
+
checkSessionHealthInputSchema.shape,
|
|
2652
|
+
async (params) => {
|
|
2653
|
+
try {
|
|
2654
|
+
const result = await executeCheckSessionHealth(
|
|
2655
|
+
{
|
|
2656
|
+
sessionId: params.sessionId,
|
|
2657
|
+
includeCompleted: params.includeCompleted ?? false
|
|
2658
|
+
},
|
|
2659
|
+
this.sessionHealthMonitor
|
|
2660
|
+
);
|
|
2661
|
+
return {
|
|
2662
|
+
content: [
|
|
2663
|
+
{
|
|
2664
|
+
type: "text",
|
|
2665
|
+
text: JSON.stringify(result, null, 2)
|
|
2666
|
+
}
|
|
2667
|
+
]
|
|
2668
|
+
};
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2671
|
+
return {
|
|
2672
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
2673
|
+
isError: true
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
);
|
|
2678
|
+
server.tool(
|
|
2679
|
+
"poll_agent_events",
|
|
2680
|
+
"Poll agent lifecycle events using a cursor.",
|
|
2681
|
+
pollAgentEventsInputSchema.shape,
|
|
2682
|
+
async (params) => {
|
|
2683
|
+
try {
|
|
2684
|
+
const result = executePollAgentEvents(
|
|
2685
|
+
{
|
|
2686
|
+
cursor: params.cursor,
|
|
2687
|
+
types: params.types,
|
|
2688
|
+
sessionId: params.sessionId,
|
|
2689
|
+
parentSessionId: params.parentSessionId,
|
|
2690
|
+
limit: params.limit ?? 50
|
|
2691
|
+
},
|
|
2692
|
+
this.agentEventStore
|
|
1646
2693
|
);
|
|
1647
2694
|
return {
|
|
1648
2695
|
content: [
|
|
@@ -1665,7 +2712,7 @@ var init_server = __esm({
|
|
|
1665
2712
|
"get_context_status",
|
|
1666
2713
|
"Get the context usage status of a relay session. Returns usage data from ContextMonitor when available, otherwise estimated values.",
|
|
1667
2714
|
{
|
|
1668
|
-
sessionId:
|
|
2715
|
+
sessionId: z7.string().describe("Relay session ID to query context usage for.")
|
|
1669
2716
|
},
|
|
1670
2717
|
async (params) => {
|
|
1671
2718
|
try {
|
|
@@ -1753,6 +2800,8 @@ var init_server = __esm({
|
|
|
1753
2800
|
await new Promise((resolve3) => {
|
|
1754
2801
|
httpServer.on("close", resolve3);
|
|
1755
2802
|
});
|
|
2803
|
+
this._httpServer = void 0;
|
|
2804
|
+
await this.close();
|
|
1756
2805
|
}
|
|
1757
2806
|
/**
|
|
1758
2807
|
* Start an HTTP server for child agents.
|
|
@@ -1781,7 +2830,7 @@ var init_server = __esm({
|
|
|
1781
2830
|
sessionIdGenerator: () => randomUUID()
|
|
1782
2831
|
});
|
|
1783
2832
|
const server = new McpServer(
|
|
1784
|
-
{ name: "agentic-relay", version: "1.
|
|
2833
|
+
{ name: "agentic-relay", version: "1.3.0" },
|
|
1785
2834
|
createMcpServerOptions()
|
|
1786
2835
|
);
|
|
1787
2836
|
this.registerTools(server);
|
|
@@ -1826,6 +2875,17 @@ var init_server = __esm({
|
|
|
1826
2875
|
});
|
|
1827
2876
|
});
|
|
1828
2877
|
}
|
|
2878
|
+
async close() {
|
|
2879
|
+
this.sessionHealthMonitor.stop();
|
|
2880
|
+
this.agentEventStore.cleanup();
|
|
2881
|
+
if (this._childHttpServer) {
|
|
2882
|
+
await new Promise((resolve3) => {
|
|
2883
|
+
this._childHttpServer.close(() => resolve3());
|
|
2884
|
+
});
|
|
2885
|
+
this._childHttpServer = void 0;
|
|
2886
|
+
this._childHttpUrl = void 0;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
1829
2889
|
/** Exposed for testing and graceful shutdown */
|
|
1830
2890
|
get httpServer() {
|
|
1831
2891
|
return this._httpServer;
|
|
@@ -1837,8 +2897,8 @@ var init_server = __esm({
|
|
|
1837
2897
|
|
|
1838
2898
|
// src/bin/relay.ts
|
|
1839
2899
|
import { defineCommand as defineCommand10, runMain } from "citty";
|
|
1840
|
-
import { join as
|
|
1841
|
-
import { homedir as
|
|
2900
|
+
import { join as join11 } from "path";
|
|
2901
|
+
import { homedir as homedir7 } from "os";
|
|
1842
2902
|
|
|
1843
2903
|
// src/infrastructure/process-manager.ts
|
|
1844
2904
|
init_logger();
|
|
@@ -3179,7 +4239,9 @@ ${prompt}`;
|
|
|
3179
4239
|
};
|
|
3180
4240
|
|
|
3181
4241
|
// src/core/session-manager.ts
|
|
3182
|
-
|
|
4242
|
+
init_logger();
|
|
4243
|
+
init_metadata_validation();
|
|
4244
|
+
import { readFile as readFile4, writeFile as writeFile4, readdir, mkdir as mkdir4, chmod, rename, unlink } from "fs/promises";
|
|
3183
4245
|
import { join as join4 } from "path";
|
|
3184
4246
|
import { homedir as homedir4 } from "os";
|
|
3185
4247
|
import { nanoid } from "nanoid";
|
|
@@ -3193,22 +4255,41 @@ function toSessionData(session) {
|
|
|
3193
4255
|
return {
|
|
3194
4256
|
...session,
|
|
3195
4257
|
createdAt: session.createdAt.toISOString(),
|
|
3196
|
-
updatedAt: session.updatedAt.toISOString()
|
|
4258
|
+
updatedAt: session.updatedAt.toISOString(),
|
|
4259
|
+
lastHeartbeatAt: session.lastHeartbeatAt.toISOString(),
|
|
4260
|
+
staleNotifiedAt: session.staleNotifiedAt?.toISOString() ?? null
|
|
3197
4261
|
};
|
|
3198
4262
|
}
|
|
3199
4263
|
function fromSessionData(data) {
|
|
4264
|
+
const createdAt = new Date(data.createdAt);
|
|
4265
|
+
const updatedAt = new Date(data.updatedAt);
|
|
4266
|
+
const fallbackHeartbeat = data.lastHeartbeatAt ?? data.createdAt ?? data.updatedAt;
|
|
4267
|
+
const lastHeartbeatAt = new Date(fallbackHeartbeat);
|
|
4268
|
+
const staleNotifiedAt = data.staleNotifiedAt ? new Date(data.staleNotifiedAt) : null;
|
|
3200
4269
|
return {
|
|
3201
4270
|
...data,
|
|
3202
|
-
createdAt
|
|
3203
|
-
updatedAt
|
|
4271
|
+
createdAt,
|
|
4272
|
+
updatedAt,
|
|
4273
|
+
lastHeartbeatAt: Number.isNaN(lastHeartbeatAt.getTime()) ? updatedAt : lastHeartbeatAt,
|
|
4274
|
+
staleNotifiedAt: staleNotifiedAt && !Number.isNaN(staleNotifiedAt.getTime()) ? staleNotifiedAt : null,
|
|
4275
|
+
metadata: data.metadata && typeof data.metadata === "object" ? data.metadata : {}
|
|
3204
4276
|
};
|
|
3205
4277
|
}
|
|
3206
4278
|
var SessionManager = class _SessionManager {
|
|
3207
4279
|
static SESSION_ID_PATTERN = /^relay-[A-Za-z0-9_-]+$/;
|
|
4280
|
+
static DEFAULT_STALE_THRESHOLD_MS = 3e5;
|
|
4281
|
+
static PROTECTED_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
4282
|
+
"taskId",
|
|
4283
|
+
"parentTaskId",
|
|
4284
|
+
"agentType"
|
|
4285
|
+
]);
|
|
3208
4286
|
sessionsDir;
|
|
3209
4287
|
constructor(sessionsDir) {
|
|
3210
4288
|
this.sessionsDir = sessionsDir ?? getSessionsDir(getRelayHome());
|
|
3211
4289
|
}
|
|
4290
|
+
getSessionsDir() {
|
|
4291
|
+
return this.sessionsDir;
|
|
4292
|
+
}
|
|
3212
4293
|
/** Ensure the sessions directory exists. */
|
|
3213
4294
|
async ensureDir() {
|
|
3214
4295
|
await mkdir4(this.sessionsDir, { recursive: true });
|
|
@@ -3219,6 +4300,54 @@ var SessionManager = class _SessionManager {
|
|
|
3219
4300
|
}
|
|
3220
4301
|
return join4(this.sessionsDir, `${relaySessionId}.json`);
|
|
3221
4302
|
}
|
|
4303
|
+
async writeSession(session) {
|
|
4304
|
+
const filePath = this.sessionPath(session.relaySessionId);
|
|
4305
|
+
const tempPath = `${filePath}.${nanoid()}.tmp`;
|
|
4306
|
+
try {
|
|
4307
|
+
await writeFile4(tempPath, JSON.stringify(toSessionData(session), null, 2), "utf-8");
|
|
4308
|
+
await chmod(tempPath, 384);
|
|
4309
|
+
await rename(tempPath, filePath);
|
|
4310
|
+
} catch (error) {
|
|
4311
|
+
await unlink(tempPath).catch(() => void 0);
|
|
4312
|
+
throw error;
|
|
4313
|
+
}
|
|
4314
|
+
}
|
|
4315
|
+
ensureValidTransition(currentStatus, nextStatus) {
|
|
4316
|
+
if (currentStatus === nextStatus) {
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
if (currentStatus === "completed" || currentStatus === "error") {
|
|
4320
|
+
throw new Error(
|
|
4321
|
+
`RELAY_INVALID_TRANSITION: ${currentStatus} -> ${nextStatus}`
|
|
4322
|
+
);
|
|
4323
|
+
}
|
|
4324
|
+
if (currentStatus === "active" && nextStatus !== "completed" && nextStatus !== "error") {
|
|
4325
|
+
throw new Error(
|
|
4326
|
+
`RELAY_INVALID_TRANSITION: ${currentStatus} -> ${nextStatus}`
|
|
4327
|
+
);
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
getLastActivityAtMs(session) {
|
|
4331
|
+
return Math.max(
|
|
4332
|
+
session.lastHeartbeatAt.getTime(),
|
|
4333
|
+
session.updatedAt.getTime()
|
|
4334
|
+
);
|
|
4335
|
+
}
|
|
4336
|
+
isStale(session, staleThresholdMs, now = Date.now()) {
|
|
4337
|
+
if (session.status !== "active") return false;
|
|
4338
|
+
return this.getLastActivityAtMs(session) + staleThresholdMs < now;
|
|
4339
|
+
}
|
|
4340
|
+
mergeMetadata(existing, updates) {
|
|
4341
|
+
const merged = { ...existing };
|
|
4342
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
4343
|
+
if (_SessionManager.PROTECTED_METADATA_KEYS.has(key) && Object.prototype.hasOwnProperty.call(existing, key)) {
|
|
4344
|
+
logger.warn(`Attempted to overwrite protected metadata key: ${key}`);
|
|
4345
|
+
continue;
|
|
4346
|
+
}
|
|
4347
|
+
merged[key] = value;
|
|
4348
|
+
}
|
|
4349
|
+
return merged;
|
|
4350
|
+
}
|
|
3222
4351
|
/** Create a new relay session. */
|
|
3223
4352
|
async create(params) {
|
|
3224
4353
|
await this.ensureDir();
|
|
@@ -3231,15 +4360,12 @@ var SessionManager = class _SessionManager {
|
|
|
3231
4360
|
depth: params.depth ?? 0,
|
|
3232
4361
|
createdAt: now,
|
|
3233
4362
|
updatedAt: now,
|
|
3234
|
-
status: "active"
|
|
4363
|
+
status: "active",
|
|
4364
|
+
lastHeartbeatAt: now,
|
|
4365
|
+
staleNotifiedAt: null,
|
|
4366
|
+
metadata: validateMetadata(params.metadata)
|
|
3235
4367
|
};
|
|
3236
|
-
|
|
3237
|
-
await writeFile4(
|
|
3238
|
-
sessionFilePath,
|
|
3239
|
-
JSON.stringify(toSessionData(session), null, 2),
|
|
3240
|
-
"utf-8"
|
|
3241
|
-
);
|
|
3242
|
-
await chmod(sessionFilePath, 384);
|
|
4368
|
+
await this.writeSession(session);
|
|
3243
4369
|
return session;
|
|
3244
4370
|
}
|
|
3245
4371
|
/** Update an existing session. */
|
|
@@ -3248,18 +4374,38 @@ var SessionManager = class _SessionManager {
|
|
|
3248
4374
|
if (!session) {
|
|
3249
4375
|
throw new Error(`Session not found: ${relaySessionId}`);
|
|
3250
4376
|
}
|
|
4377
|
+
if (updates.status) {
|
|
4378
|
+
this.ensureValidTransition(session.status, updates.status);
|
|
4379
|
+
}
|
|
3251
4380
|
const updated = {
|
|
3252
4381
|
...session,
|
|
3253
4382
|
...updates,
|
|
4383
|
+
metadata: validateMetadata(
|
|
4384
|
+
updates.metadata ? this.mergeMetadata(session.metadata, updates.metadata) : session.metadata
|
|
4385
|
+
),
|
|
3254
4386
|
updatedAt: /* @__PURE__ */ new Date()
|
|
3255
4387
|
};
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
)
|
|
3262
|
-
|
|
4388
|
+
await this.writeSession(updated);
|
|
4389
|
+
}
|
|
4390
|
+
/** Update heartbeat timestamp for an active session. */
|
|
4391
|
+
async updateHeartbeat(relaySessionId) {
|
|
4392
|
+
const session = await this.get(relaySessionId);
|
|
4393
|
+
if (!session) {
|
|
4394
|
+
throw new Error(`Session not found: ${relaySessionId}`);
|
|
4395
|
+
}
|
|
4396
|
+
if (session.status !== "active") {
|
|
4397
|
+
throw new Error(
|
|
4398
|
+
`RELAY_INVALID_TRANSITION: ${session.status} -> active`
|
|
4399
|
+
);
|
|
4400
|
+
}
|
|
4401
|
+
const now = /* @__PURE__ */ new Date();
|
|
4402
|
+
const updated = {
|
|
4403
|
+
...session,
|
|
4404
|
+
lastHeartbeatAt: now,
|
|
4405
|
+
staleNotifiedAt: null,
|
|
4406
|
+
updatedAt: now
|
|
4407
|
+
};
|
|
4408
|
+
await this.writeSession(updated);
|
|
3263
4409
|
}
|
|
3264
4410
|
/** Get a session by relay session ID. */
|
|
3265
4411
|
async get(relaySessionId) {
|
|
@@ -3294,6 +4440,31 @@ var SessionManager = class _SessionManager {
|
|
|
3294
4440
|
if (filter?.backendId && session.backendId !== filter.backendId) {
|
|
3295
4441
|
continue;
|
|
3296
4442
|
}
|
|
4443
|
+
if (filter?.status && session.status !== filter.status) {
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4446
|
+
if (filter?.taskId && session.metadata.taskId !== filter.taskId) {
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
if (filter?.label && filter.label.trim().length > 0) {
|
|
4450
|
+
const target = (session.metadata.label ?? "").toLowerCase();
|
|
4451
|
+
if (!target.includes(filter.label.toLowerCase())) {
|
|
4452
|
+
continue;
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
if (filter?.tags && filter.tags.length > 0) {
|
|
4456
|
+
const sessionTags = Array.isArray(session.metadata.tags) ? session.metadata.tags : [];
|
|
4457
|
+
const hasAllTags = filter.tags.every((tag) => sessionTags.includes(tag));
|
|
4458
|
+
if (!hasAllTags) {
|
|
4459
|
+
continue;
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
if (filter?.staleOnly) {
|
|
4463
|
+
const threshold = filter.staleThresholdMs ?? _SessionManager.DEFAULT_STALE_THRESHOLD_MS;
|
|
4464
|
+
if (!this.isStale(session, threshold)) {
|
|
4465
|
+
continue;
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
3297
4468
|
sessions.push(session);
|
|
3298
4469
|
} catch {
|
|
3299
4470
|
}
|
|
@@ -3306,6 +4477,41 @@ var SessionManager = class _SessionManager {
|
|
|
3306
4477
|
}
|
|
3307
4478
|
return sessions;
|
|
3308
4479
|
}
|
|
4480
|
+
async listStale(thresholdMs) {
|
|
4481
|
+
const activeSessions = await this.list({ status: "active" });
|
|
4482
|
+
const now = Date.now();
|
|
4483
|
+
return activeSessions.filter((session) => this.isStale(session, thresholdMs, now));
|
|
4484
|
+
}
|
|
4485
|
+
async delete(relaySessionId) {
|
|
4486
|
+
const filePath = this.sessionPath(relaySessionId);
|
|
4487
|
+
try {
|
|
4488
|
+
await unlink(filePath);
|
|
4489
|
+
} catch (error) {
|
|
4490
|
+
if (error.code === "ENOENT") {
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
throw error;
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
async cleanup(olderThanMs) {
|
|
4497
|
+
const sessions = await this.list();
|
|
4498
|
+
const now = Date.now();
|
|
4499
|
+
const deletedSessionIds = [];
|
|
4500
|
+
for (const session of sessions) {
|
|
4501
|
+
if (session.status !== "completed" && session.status !== "error") {
|
|
4502
|
+
continue;
|
|
4503
|
+
}
|
|
4504
|
+
if (session.updatedAt.getTime() + olderThanMs >= now) {
|
|
4505
|
+
continue;
|
|
4506
|
+
}
|
|
4507
|
+
await this.delete(session.relaySessionId);
|
|
4508
|
+
deletedSessionIds.push(session.relaySessionId);
|
|
4509
|
+
}
|
|
4510
|
+
return {
|
|
4511
|
+
deletedCount: deletedSessionIds.length,
|
|
4512
|
+
deletedSessionIds
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
3309
4515
|
};
|
|
3310
4516
|
|
|
3311
4517
|
// src/core/config-manager.ts
|
|
@@ -3328,7 +4534,10 @@ var hookEventSchema = z.enum([
|
|
|
3328
4534
|
"on-error",
|
|
3329
4535
|
"on-context-threshold",
|
|
3330
4536
|
"pre-spawn",
|
|
3331
|
-
"post-spawn"
|
|
4537
|
+
"post-spawn",
|
|
4538
|
+
"on-session-complete",
|
|
4539
|
+
"on-session-error",
|
|
4540
|
+
"on-session-stale"
|
|
3332
4541
|
]);
|
|
3333
4542
|
var hookDefinitionSchema = z.object({
|
|
3334
4543
|
event: hookEventSchema,
|
|
@@ -3350,7 +4559,8 @@ var hookChainSchema = z.object({
|
|
|
3350
4559
|
});
|
|
3351
4560
|
var hooksConfigSchema = z.object({
|
|
3352
4561
|
definitions: z.array(hookDefinitionSchema),
|
|
3353
|
-
chains: z.array(hookChainSchema).optional()
|
|
4562
|
+
chains: z.array(hookChainSchema).optional(),
|
|
4563
|
+
memoryDir: z.string().optional()
|
|
3354
4564
|
});
|
|
3355
4565
|
var backendContextConfigSchema = z.object({
|
|
3356
4566
|
contextWindow: z.number().positive().optional(),
|
|
@@ -3365,18 +4575,38 @@ var relayConfigSchema = z.object({
|
|
|
3365
4575
|
gemini: z.record(z.unknown()).optional()
|
|
3366
4576
|
}).optional(),
|
|
3367
4577
|
hooks: hooksConfigSchema.optional(),
|
|
4578
|
+
sessionHealth: z.object({
|
|
4579
|
+
enabled: z.boolean().optional(),
|
|
4580
|
+
heartbeatIntervalSec: z.number().positive().optional(),
|
|
4581
|
+
staleThresholdSec: z.number().positive().optional(),
|
|
4582
|
+
cleanupAfterSec: z.number().positive().optional(),
|
|
4583
|
+
maxActiveSessions: z.number().int().positive().optional(),
|
|
4584
|
+
checkIntervalSec: z.number().positive().optional()
|
|
4585
|
+
}).optional(),
|
|
3368
4586
|
contextMonitor: z.object({
|
|
3369
4587
|
enabled: z.boolean().optional(),
|
|
3370
4588
|
thresholdPercent: z.number().min(0).max(100).optional(),
|
|
3371
4589
|
notifyThreshold: z.number().positive().optional(),
|
|
3372
4590
|
notifyPercent: z.number().min(0).max(100).optional(),
|
|
3373
4591
|
notifyMethod: z.enum(["stderr", "hook"]).optional(),
|
|
4592
|
+
thresholdLevels: z.object({
|
|
4593
|
+
warning: z.number().min(0).max(100).optional(),
|
|
4594
|
+
critical: z.number().min(0).max(100).optional(),
|
|
4595
|
+
emergency: z.number().min(0).max(100).optional()
|
|
4596
|
+
}).optional(),
|
|
3374
4597
|
backends: z.object({
|
|
3375
4598
|
claude: backendContextConfigSchema,
|
|
3376
4599
|
codex: backendContextConfigSchema,
|
|
3377
4600
|
gemini: backendContextConfigSchema
|
|
3378
4601
|
}).optional()
|
|
3379
4602
|
}).optional(),
|
|
4603
|
+
eventStore: z.object({
|
|
4604
|
+
backend: z.enum(["memory", "jsonl"]).optional(),
|
|
4605
|
+
maxEvents: z.number().int().positive().optional(),
|
|
4606
|
+
ttlSec: z.number().positive().optional(),
|
|
4607
|
+
sessionDir: z.string().optional(),
|
|
4608
|
+
eventsFileName: z.string().optional()
|
|
4609
|
+
}).optional(),
|
|
3380
4610
|
mcpServerMode: z.object({
|
|
3381
4611
|
maxDepth: z.number().int().positive(),
|
|
3382
4612
|
maxCallsPerSession: z.number().int().positive(),
|
|
@@ -3397,7 +4627,7 @@ function deepMerge(target, source) {
|
|
|
3397
4627
|
for (const key of Object.keys(source)) {
|
|
3398
4628
|
const sourceVal = source[key];
|
|
3399
4629
|
const targetVal = result[key];
|
|
3400
|
-
if (
|
|
4630
|
+
if (isPlainObject2(sourceVal) && isPlainObject2(targetVal)) {
|
|
3401
4631
|
result[key] = deepMerge(
|
|
3402
4632
|
targetVal,
|
|
3403
4633
|
sourceVal
|
|
@@ -3408,14 +4638,14 @@ function deepMerge(target, source) {
|
|
|
3408
4638
|
}
|
|
3409
4639
|
return result;
|
|
3410
4640
|
}
|
|
3411
|
-
function
|
|
4641
|
+
function isPlainObject2(value) {
|
|
3412
4642
|
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
3413
4643
|
}
|
|
3414
4644
|
function getByPath(obj, path2) {
|
|
3415
4645
|
const parts = path2.split(".");
|
|
3416
4646
|
let current = obj;
|
|
3417
4647
|
for (const part of parts) {
|
|
3418
|
-
if (!
|
|
4648
|
+
if (!isPlainObject2(current)) return void 0;
|
|
3419
4649
|
current = current[part];
|
|
3420
4650
|
}
|
|
3421
4651
|
return current;
|
|
@@ -3425,7 +4655,7 @@ function setByPath(obj, path2, value) {
|
|
|
3425
4655
|
let current = obj;
|
|
3426
4656
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
3427
4657
|
const part = parts[i];
|
|
3428
|
-
if (!
|
|
4658
|
+
if (!isPlainObject2(current[part])) {
|
|
3429
4659
|
current[part] = {};
|
|
3430
4660
|
}
|
|
3431
4661
|
current = current[part];
|
|
@@ -3534,7 +4764,7 @@ var ConfigManager = class {
|
|
|
3534
4764
|
try {
|
|
3535
4765
|
const raw = await readFile5(filePath, "utf-8");
|
|
3536
4766
|
const parsed = JSON.parse(raw);
|
|
3537
|
-
if (!
|
|
4767
|
+
if (!isPlainObject2(parsed)) return {};
|
|
3538
4768
|
return parsed;
|
|
3539
4769
|
} catch {
|
|
3540
4770
|
return {};
|
|
@@ -3678,7 +4908,7 @@ var HooksEngine = class _HooksEngine {
|
|
|
3678
4908
|
}
|
|
3679
4909
|
/** Load hook definitions from config and register listeners on EventBus */
|
|
3680
4910
|
loadConfig(config) {
|
|
3681
|
-
this.definitions = config.definitions.filter((def) => {
|
|
4911
|
+
this.definitions = (config.definitions ?? []).filter((def) => {
|
|
3682
4912
|
if (def.enabled === false) return false;
|
|
3683
4913
|
try {
|
|
3684
4914
|
this.validateCommand(def.command);
|
|
@@ -3906,13 +5136,17 @@ var DEFAULT_BACKEND_CONTEXT = {
|
|
|
3906
5136
|
gemini: { contextWindow: 1048576, compactThreshold: 524288 }
|
|
3907
5137
|
};
|
|
3908
5138
|
var DEFAULT_NOTIFY_PERCENT = 70;
|
|
5139
|
+
var DEFAULT_CRITICAL_PERCENT = 85;
|
|
5140
|
+
var DEFAULT_EMERGENCY_PERCENT = 95;
|
|
3909
5141
|
var DEFAULT_CONFIG = {
|
|
3910
5142
|
enabled: true,
|
|
3911
|
-
notifyMethod: "hook"
|
|
5143
|
+
notifyMethod: "hook",
|
|
5144
|
+
memoryDir: "./memory"
|
|
3912
5145
|
};
|
|
3913
5146
|
var ContextMonitor = class {
|
|
3914
|
-
constructor(hooksEngine2, config) {
|
|
5147
|
+
constructor(hooksEngine2, config, agentEventStore) {
|
|
3915
5148
|
this.hooksEngine = hooksEngine2;
|
|
5149
|
+
this.agentEventStore = agentEventStore;
|
|
3916
5150
|
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
3917
5151
|
if (this.config.thresholdPercent !== void 0 && this.config.notifyPercent === void 0 && this.config.notifyThreshold === void 0) {
|
|
3918
5152
|
this.config.notifyPercent = this.config.thresholdPercent;
|
|
@@ -3920,6 +5154,7 @@ var ContextMonitor = class {
|
|
|
3920
5154
|
}
|
|
3921
5155
|
config;
|
|
3922
5156
|
usageMap = /* @__PURE__ */ new Map();
|
|
5157
|
+
agentEventStore;
|
|
3923
5158
|
/** Get backend context config, merging user overrides with defaults */
|
|
3924
5159
|
getBackendConfig(backendId) {
|
|
3925
5160
|
const defaults = DEFAULT_BACKEND_CONTEXT[backendId];
|
|
@@ -3935,19 +5170,60 @@ var ContextMonitor = class {
|
|
|
3935
5170
|
return this.config.notifyThreshold;
|
|
3936
5171
|
}
|
|
3937
5172
|
const backendConfig = this.getBackendConfig(backendId);
|
|
3938
|
-
const notifyPercent = this.config.notifyPercent ?? DEFAULT_NOTIFY_PERCENT;
|
|
5173
|
+
const notifyPercent = this.config.thresholdLevels?.warning ?? this.config.notifyPercent ?? DEFAULT_NOTIFY_PERCENT;
|
|
3939
5174
|
return Math.round(backendConfig.contextWindow * notifyPercent / 100);
|
|
3940
5175
|
}
|
|
5176
|
+
getWarningPercent(backendId) {
|
|
5177
|
+
if (this.config.thresholdLevels?.warning !== void 0) {
|
|
5178
|
+
return this.config.thresholdLevels.warning;
|
|
5179
|
+
}
|
|
5180
|
+
if (this.config.notifyThreshold !== void 0) {
|
|
5181
|
+
const backendConfig = this.getBackendConfig(backendId);
|
|
5182
|
+
if (backendConfig.contextWindow <= 0) return DEFAULT_NOTIFY_PERCENT;
|
|
5183
|
+
return Math.round(
|
|
5184
|
+
this.config.notifyThreshold / backendConfig.contextWindow * 100
|
|
5185
|
+
);
|
|
5186
|
+
}
|
|
5187
|
+
return this.config.notifyPercent ?? DEFAULT_NOTIFY_PERCENT;
|
|
5188
|
+
}
|
|
5189
|
+
getThresholdLevels(backendId, contextWindow) {
|
|
5190
|
+
const warningPercent = this.getWarningPercent(backendId);
|
|
5191
|
+
const warningTokens = this.getNotifyThreshold(backendId);
|
|
5192
|
+
const criticalPercent = this.config.thresholdLevels?.critical ?? DEFAULT_CRITICAL_PERCENT;
|
|
5193
|
+
const emergencyPercent = this.config.thresholdLevels?.emergency ?? DEFAULT_EMERGENCY_PERCENT;
|
|
5194
|
+
return [
|
|
5195
|
+
["warning", warningPercent, warningTokens],
|
|
5196
|
+
[
|
|
5197
|
+
"critical",
|
|
5198
|
+
criticalPercent,
|
|
5199
|
+
Math.round(contextWindow * criticalPercent / 100)
|
|
5200
|
+
],
|
|
5201
|
+
[
|
|
5202
|
+
"emergency",
|
|
5203
|
+
emergencyPercent,
|
|
5204
|
+
Math.round(contextWindow * emergencyPercent / 100)
|
|
5205
|
+
]
|
|
5206
|
+
];
|
|
5207
|
+
}
|
|
5208
|
+
isNotified(sessionId, level) {
|
|
5209
|
+
const entry = this.usageMap.get(sessionId);
|
|
5210
|
+
return entry?.notifiedLevels.has(level) ?? false;
|
|
5211
|
+
}
|
|
5212
|
+
markNotified(sessionId, level) {
|
|
5213
|
+
const entry = this.usageMap.get(sessionId);
|
|
5214
|
+
if (!entry) return;
|
|
5215
|
+
entry.notifiedLevels.add(level);
|
|
5216
|
+
}
|
|
3941
5217
|
/** Update token usage for a session and check threshold */
|
|
3942
|
-
updateUsage(sessionId, backendId, estimatedTokens) {
|
|
5218
|
+
updateUsage(sessionId, backendId, estimatedTokens, sessionMetadata) {
|
|
3943
5219
|
if (!this.config.enabled) return;
|
|
3944
5220
|
const backendConfig = this.getBackendConfig(backendId);
|
|
3945
5221
|
const contextWindow = backendConfig.contextWindow;
|
|
3946
5222
|
const usagePercent = contextWindow > 0 ? Math.round(estimatedTokens / contextWindow * 100) : 0;
|
|
3947
5223
|
const existing = this.usageMap.get(sessionId);
|
|
3948
|
-
let
|
|
5224
|
+
let notifiedLevels = existing?.notifiedLevels ?? /* @__PURE__ */ new Set();
|
|
3949
5225
|
if (existing && estimatedTokens < existing.estimatedTokens * 0.7) {
|
|
3950
|
-
|
|
5226
|
+
notifiedLevels = /* @__PURE__ */ new Set();
|
|
3951
5227
|
}
|
|
3952
5228
|
this.usageMap.set(sessionId, {
|
|
3953
5229
|
estimatedTokens,
|
|
@@ -3955,19 +5231,27 @@ var ContextMonitor = class {
|
|
|
3955
5231
|
compactThreshold: backendConfig.compactThreshold,
|
|
3956
5232
|
usagePercent,
|
|
3957
5233
|
backendId,
|
|
3958
|
-
|
|
5234
|
+
notifiedLevels,
|
|
5235
|
+
sessionMetadata: sessionMetadata ?? existing?.sessionMetadata
|
|
3959
5236
|
});
|
|
3960
|
-
const
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
5237
|
+
const levels = this.getThresholdLevels(backendId, contextWindow);
|
|
5238
|
+
for (const [level, _thresholdPercent, thresholdTokens] of levels) {
|
|
5239
|
+
if (estimatedTokens < thresholdTokens) {
|
|
5240
|
+
continue;
|
|
5241
|
+
}
|
|
5242
|
+
if (this.isNotified(sessionId, level)) {
|
|
5243
|
+
continue;
|
|
5244
|
+
}
|
|
5245
|
+
this.markNotified(sessionId, level);
|
|
5246
|
+
this.notifyLevel(
|
|
3965
5247
|
sessionId,
|
|
3966
5248
|
backendId,
|
|
5249
|
+
level,
|
|
3967
5250
|
usagePercent,
|
|
3968
5251
|
estimatedTokens,
|
|
3969
5252
|
contextWindow,
|
|
3970
|
-
backendConfig.compactThreshold
|
|
5253
|
+
backendConfig.compactThreshold,
|
|
5254
|
+
this.usageMap.get(sessionId)?.sessionMetadata
|
|
3971
5255
|
);
|
|
3972
5256
|
}
|
|
3973
5257
|
}
|
|
@@ -3993,15 +5277,93 @@ var ContextMonitor = class {
|
|
|
3993
5277
|
removeSession(sessionId) {
|
|
3994
5278
|
this.usageMap.delete(sessionId);
|
|
3995
5279
|
}
|
|
3996
|
-
|
|
5280
|
+
setAgentEventStore(agentEventStore) {
|
|
5281
|
+
this.agentEventStore = agentEventStore;
|
|
5282
|
+
}
|
|
5283
|
+
parseSaveResult(value) {
|
|
5284
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5285
|
+
return null;
|
|
5286
|
+
}
|
|
5287
|
+
const candidate = value;
|
|
5288
|
+
if (typeof candidate.saved !== "boolean") {
|
|
5289
|
+
return null;
|
|
5290
|
+
}
|
|
5291
|
+
const saveResult = { saved: candidate.saved };
|
|
5292
|
+
if (typeof candidate.snapshotPath === "string") {
|
|
5293
|
+
saveResult.snapshotPath = candidate.snapshotPath;
|
|
5294
|
+
}
|
|
5295
|
+
if (typeof candidate.error === "string") {
|
|
5296
|
+
saveResult.error = candidate.error;
|
|
5297
|
+
}
|
|
5298
|
+
return saveResult;
|
|
5299
|
+
}
|
|
5300
|
+
extractSaveResult(results) {
|
|
5301
|
+
for (let index = results.length - 1; index >= 0; index -= 1) {
|
|
5302
|
+
const metadata = results[index]?.output.metadata;
|
|
5303
|
+
const nested = this.parseSaveResult(
|
|
5304
|
+
metadata?.saveResult
|
|
5305
|
+
);
|
|
5306
|
+
if (nested) {
|
|
5307
|
+
return nested;
|
|
5308
|
+
}
|
|
5309
|
+
const topLevel = this.parseSaveResult(metadata);
|
|
5310
|
+
if (topLevel) {
|
|
5311
|
+
return topLevel;
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
const failed = results.find(
|
|
5315
|
+
(result) => result.exitCode !== 0 || (result.stderr?.trim().length ?? 0) > 0
|
|
5316
|
+
);
|
|
5317
|
+
if (failed) {
|
|
5318
|
+
return {
|
|
5319
|
+
saved: false,
|
|
5320
|
+
error: failed.stderr || `hook exited with code ${failed.exitCode}`
|
|
5321
|
+
};
|
|
5322
|
+
}
|
|
5323
|
+
return { saved: false };
|
|
5324
|
+
}
|
|
5325
|
+
recordThresholdEvent(sessionId, backendId, level, usagePercent, currentTokens, contextWindow, compactThreshold, remainingBeforeCompact, saveResult, sessionMetadata) {
|
|
5326
|
+
this.agentEventStore?.record({
|
|
5327
|
+
type: "context-threshold",
|
|
5328
|
+
sessionId,
|
|
5329
|
+
backendId,
|
|
5330
|
+
parentSessionId: void 0,
|
|
5331
|
+
metadata: sessionMetadata ?? {},
|
|
5332
|
+
data: {
|
|
5333
|
+
usagePercent,
|
|
5334
|
+
currentTokens,
|
|
5335
|
+
contextWindow,
|
|
5336
|
+
compactThreshold,
|
|
5337
|
+
remainingBeforeCompact,
|
|
5338
|
+
level,
|
|
5339
|
+
thresholdLevel: level,
|
|
5340
|
+
saveResult,
|
|
5341
|
+
taskId: sessionMetadata?.taskId
|
|
5342
|
+
}
|
|
5343
|
+
});
|
|
5344
|
+
}
|
|
5345
|
+
notifyLevel(sessionId, backendId, level, usagePercent, currentTokens, contextWindow, compactThreshold, sessionMetadata) {
|
|
3997
5346
|
const remainingBeforeCompact = Math.max(
|
|
3998
5347
|
0,
|
|
3999
5348
|
compactThreshold - currentTokens
|
|
4000
5349
|
);
|
|
4001
|
-
const
|
|
5350
|
+
const initialSaveResult = { saved: false };
|
|
5351
|
+
const warningMessage = `${backendId} session ${sessionId} level=${level} at ${usagePercent}% (${currentTokens}/${contextWindow} tokens). Compact in ~${remainingBeforeCompact} tokens. Save your work state now.`;
|
|
4002
5352
|
if (this.config.notifyMethod === "stderr") {
|
|
5353
|
+
this.recordThresholdEvent(
|
|
5354
|
+
sessionId,
|
|
5355
|
+
backendId,
|
|
5356
|
+
level,
|
|
5357
|
+
usagePercent,
|
|
5358
|
+
currentTokens,
|
|
5359
|
+
contextWindow,
|
|
5360
|
+
compactThreshold,
|
|
5361
|
+
remainingBeforeCompact,
|
|
5362
|
+
initialSaveResult,
|
|
5363
|
+
sessionMetadata
|
|
5364
|
+
);
|
|
4003
5365
|
process.stderr.write(
|
|
4004
|
-
`[relay] Context
|
|
5366
|
+
`[relay] Context ${level}: ${warningMessage}
|
|
4005
5367
|
`
|
|
4006
5368
|
);
|
|
4007
5369
|
} else if (this.config.notifyMethod === "hook" && this.hooksEngine) {
|
|
@@ -4015,11 +5377,60 @@ var ContextMonitor = class {
|
|
|
4015
5377
|
currentTokens,
|
|
4016
5378
|
contextWindow,
|
|
4017
5379
|
compactThreshold,
|
|
4018
|
-
remainingBeforeCompact
|
|
5380
|
+
remainingBeforeCompact,
|
|
5381
|
+
level,
|
|
5382
|
+
thresholdLevel: level,
|
|
5383
|
+
taskId: sessionMetadata?.taskId,
|
|
5384
|
+
label: sessionMetadata?.label,
|
|
5385
|
+
agentType: sessionMetadata?.agentType,
|
|
5386
|
+
saveResult: initialSaveResult,
|
|
5387
|
+
memoryDir: this.config.memoryDir ?? "./memory"
|
|
4019
5388
|
}
|
|
4020
5389
|
};
|
|
4021
|
-
void this.hooksEngine.emit("on-context-threshold", hookInput).
|
|
4022
|
-
|
|
5390
|
+
void this.hooksEngine.emit("on-context-threshold", hookInput).then((results) => {
|
|
5391
|
+
const saveResult = this.extractSaveResult(results);
|
|
5392
|
+
this.recordThresholdEvent(
|
|
5393
|
+
sessionId,
|
|
5394
|
+
backendId,
|
|
5395
|
+
level,
|
|
5396
|
+
usagePercent,
|
|
5397
|
+
currentTokens,
|
|
5398
|
+
contextWindow,
|
|
5399
|
+
compactThreshold,
|
|
5400
|
+
remainingBeforeCompact,
|
|
5401
|
+
saveResult,
|
|
5402
|
+
sessionMetadata
|
|
5403
|
+
);
|
|
5404
|
+
}).catch(
|
|
5405
|
+
(e) => {
|
|
5406
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
5407
|
+
this.recordThresholdEvent(
|
|
5408
|
+
sessionId,
|
|
5409
|
+
backendId,
|
|
5410
|
+
level,
|
|
5411
|
+
usagePercent,
|
|
5412
|
+
currentTokens,
|
|
5413
|
+
contextWindow,
|
|
5414
|
+
compactThreshold,
|
|
5415
|
+
remainingBeforeCompact,
|
|
5416
|
+
{ saved: false, error: errorMessage },
|
|
5417
|
+
sessionMetadata
|
|
5418
|
+
);
|
|
5419
|
+
logger.debug("Context threshold hook error:", e);
|
|
5420
|
+
}
|
|
5421
|
+
);
|
|
5422
|
+
} else {
|
|
5423
|
+
this.recordThresholdEvent(
|
|
5424
|
+
sessionId,
|
|
5425
|
+
backendId,
|
|
5426
|
+
level,
|
|
5427
|
+
usagePercent,
|
|
5428
|
+
currentTokens,
|
|
5429
|
+
contextWindow,
|
|
5430
|
+
compactThreshold,
|
|
5431
|
+
remainingBeforeCompact,
|
|
5432
|
+
initialSaveResult,
|
|
5433
|
+
sessionMetadata
|
|
4023
5434
|
);
|
|
4024
5435
|
}
|
|
4025
5436
|
}
|
|
@@ -4615,16 +6026,17 @@ function createMCPCommand(configManager2, registry2, sessionManager2, hooksEngin
|
|
|
4615
6026
|
let guardConfig;
|
|
4616
6027
|
let inlineSummaryLength;
|
|
4617
6028
|
let responseOutputDir;
|
|
6029
|
+
let relayConfig;
|
|
4618
6030
|
try {
|
|
4619
|
-
|
|
4620
|
-
if (
|
|
6031
|
+
relayConfig = await configManager2.getConfig();
|
|
6032
|
+
if (relayConfig.mcpServerMode) {
|
|
4621
6033
|
guardConfig = {
|
|
4622
|
-
maxDepth:
|
|
4623
|
-
maxCallsPerSession:
|
|
4624
|
-
timeoutSec:
|
|
6034
|
+
maxDepth: relayConfig.mcpServerMode.maxDepth ?? 5,
|
|
6035
|
+
maxCallsPerSession: relayConfig.mcpServerMode.maxCallsPerSession ?? 20,
|
|
6036
|
+
timeoutSec: relayConfig.mcpServerMode.timeoutSec ?? 86400
|
|
4625
6037
|
};
|
|
4626
|
-
inlineSummaryLength =
|
|
4627
|
-
responseOutputDir =
|
|
6038
|
+
inlineSummaryLength = relayConfig.mcpServerMode.inlineSummaryLength;
|
|
6039
|
+
responseOutputDir = relayConfig.mcpServerMode.responseOutputDir;
|
|
4628
6040
|
}
|
|
4629
6041
|
} catch {
|
|
4630
6042
|
}
|
|
@@ -4637,7 +6049,8 @@ function createMCPCommand(configManager2, registry2, sessionManager2, hooksEngin
|
|
|
4637
6049
|
hooksEngine2,
|
|
4638
6050
|
contextMonitor2,
|
|
4639
6051
|
inlineSummaryLength,
|
|
4640
|
-
responseOutputDir
|
|
6052
|
+
responseOutputDir,
|
|
6053
|
+
relayConfig
|
|
4641
6054
|
);
|
|
4642
6055
|
await server.start({ transport, port });
|
|
4643
6056
|
}
|
|
@@ -4799,7 +6212,7 @@ function createVersionCommand(registry2) {
|
|
|
4799
6212
|
description: "Show relay and backend versions"
|
|
4800
6213
|
},
|
|
4801
6214
|
async run() {
|
|
4802
|
-
const relayVersion = "1.
|
|
6215
|
+
const relayVersion = "1.3.0";
|
|
4803
6216
|
console.log(`agentic-relay v${relayVersion}`);
|
|
4804
6217
|
console.log("");
|
|
4805
6218
|
console.log("Backends:");
|
|
@@ -4824,8 +6237,8 @@ function createVersionCommand(registry2) {
|
|
|
4824
6237
|
// src/commands/doctor.ts
|
|
4825
6238
|
import { defineCommand as defineCommand8 } from "citty";
|
|
4826
6239
|
import { access, constants, readdir as readdir2 } from "fs/promises";
|
|
4827
|
-
import { join as
|
|
4828
|
-
import { homedir as
|
|
6240
|
+
import { join as join9 } from "path";
|
|
6241
|
+
import { homedir as homedir6 } from "os";
|
|
4829
6242
|
import { execFile as execFile2 } from "child_process";
|
|
4830
6243
|
import { promisify as promisify2 } from "util";
|
|
4831
6244
|
var execFileAsync2 = promisify2(execFile2);
|
|
@@ -4885,8 +6298,8 @@ async function checkConfig(configManager2) {
|
|
|
4885
6298
|
}
|
|
4886
6299
|
}
|
|
4887
6300
|
async function checkSessionsDir() {
|
|
4888
|
-
const relayHome2 = process.env["RELAY_HOME"] ??
|
|
4889
|
-
const sessionsDir =
|
|
6301
|
+
const relayHome2 = process.env["RELAY_HOME"] ?? join9(homedir6(), ".relay");
|
|
6302
|
+
const sessionsDir = join9(relayHome2, "sessions");
|
|
4890
6303
|
try {
|
|
4891
6304
|
await access(sessionsDir, constants.W_OK);
|
|
4892
6305
|
return {
|
|
@@ -4999,8 +6412,8 @@ async function checkBackendAuthEnv() {
|
|
|
4999
6412
|
return results;
|
|
5000
6413
|
}
|
|
5001
6414
|
async function checkSessionsDiskUsage() {
|
|
5002
|
-
const relayHome2 = process.env["RELAY_HOME"] ??
|
|
5003
|
-
const sessionsDir =
|
|
6415
|
+
const relayHome2 = process.env["RELAY_HOME"] ?? join9(homedir6(), ".relay");
|
|
6416
|
+
const sessionsDir = join9(relayHome2, "sessions");
|
|
5004
6417
|
try {
|
|
5005
6418
|
const entries = await readdir2(sessionsDir);
|
|
5006
6419
|
const fileCount = entries.length;
|
|
@@ -5074,8 +6487,8 @@ function createDoctorCommand(registry2, configManager2) {
|
|
|
5074
6487
|
init_logger();
|
|
5075
6488
|
import { defineCommand as defineCommand9 } from "citty";
|
|
5076
6489
|
import { mkdir as mkdir6, writeFile as writeFile6, access as access2, readFile as readFile6 } from "fs/promises";
|
|
5077
|
-
import { join as
|
|
5078
|
-
var
|
|
6490
|
+
import { join as join10 } from "path";
|
|
6491
|
+
var DEFAULT_CONFIG4 = {
|
|
5079
6492
|
defaultBackend: "claude",
|
|
5080
6493
|
backends: {},
|
|
5081
6494
|
mcpServers: {}
|
|
@@ -5088,8 +6501,8 @@ function createInitCommand() {
|
|
|
5088
6501
|
},
|
|
5089
6502
|
async run() {
|
|
5090
6503
|
const projectDir = process.cwd();
|
|
5091
|
-
const relayDir =
|
|
5092
|
-
const configPath =
|
|
6504
|
+
const relayDir = join10(projectDir, ".relay");
|
|
6505
|
+
const configPath = join10(relayDir, "config.json");
|
|
5093
6506
|
try {
|
|
5094
6507
|
await access2(relayDir);
|
|
5095
6508
|
logger.info(
|
|
@@ -5101,11 +6514,11 @@ function createInitCommand() {
|
|
|
5101
6514
|
await mkdir6(relayDir, { recursive: true });
|
|
5102
6515
|
await writeFile6(
|
|
5103
6516
|
configPath,
|
|
5104
|
-
JSON.stringify(
|
|
6517
|
+
JSON.stringify(DEFAULT_CONFIG4, null, 2) + "\n",
|
|
5105
6518
|
"utf-8"
|
|
5106
6519
|
);
|
|
5107
6520
|
logger.success(`Created ${configPath}`);
|
|
5108
|
-
const gitignorePath =
|
|
6521
|
+
const gitignorePath = join10(projectDir, ".gitignore");
|
|
5109
6522
|
try {
|
|
5110
6523
|
const gitignoreContent = await readFile6(gitignorePath, "utf-8");
|
|
5111
6524
|
if (!gitignoreContent.includes(".relay/config.local.json")) {
|
|
@@ -5131,8 +6544,8 @@ registry.registerLazy("claude", () => new ClaudeAdapter(processManager));
|
|
|
5131
6544
|
registry.registerLazy("codex", () => new CodexAdapter(processManager));
|
|
5132
6545
|
registry.registerLazy("gemini", () => new GeminiAdapter(processManager));
|
|
5133
6546
|
var sessionManager = new SessionManager();
|
|
5134
|
-
var relayHome = process.env["RELAY_HOME"] ??
|
|
5135
|
-
var projectRelayDir =
|
|
6547
|
+
var relayHome = process.env["RELAY_HOME"] ?? join11(homedir7(), ".relay");
|
|
6548
|
+
var projectRelayDir = join11(process.cwd(), ".relay");
|
|
5136
6549
|
var configManager = new ConfigManager(relayHome, projectRelayDir);
|
|
5137
6550
|
var authManager = new AuthManager(registry);
|
|
5138
6551
|
var eventBus = new EventBus();
|
|
@@ -5143,13 +6556,16 @@ void configManager.getConfig().then((config) => {
|
|
|
5143
6556
|
hooksEngine.loadConfig(config.hooks);
|
|
5144
6557
|
}
|
|
5145
6558
|
if (config.contextMonitor) {
|
|
5146
|
-
contextMonitor = new ContextMonitor(hooksEngine,
|
|
6559
|
+
contextMonitor = new ContextMonitor(hooksEngine, {
|
|
6560
|
+
...config.contextMonitor,
|
|
6561
|
+
memoryDir: config.hooks?.memoryDir
|
|
6562
|
+
});
|
|
5147
6563
|
}
|
|
5148
6564
|
}).catch((e) => logger.debug("Config load failed:", e));
|
|
5149
6565
|
var main = defineCommand10({
|
|
5150
6566
|
meta: {
|
|
5151
6567
|
name: "relay",
|
|
5152
|
-
version: "1.
|
|
6568
|
+
version: "1.3.0",
|
|
5153
6569
|
description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
|
|
5154
6570
|
},
|
|
5155
6571
|
subCommands: {
|