@agent-team-foundation/first-tree-hub 0.2.0 → 0.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/cli/index.mjs +30 -3
- package/dist/{core-CD3xEbyB.mjs → core-uG-Hkr9K.mjs} +1092 -443
- package/dist/drizzle/0006_agent_tree_path.sql +1 -0
- package/dist/drizzle/meta/_journal.json +7 -0
- package/dist/index.mjs +1 -1
- package/dist/web/assets/{index-CHZINY3I.js → index-B8NgnD3u.js} +46 -41
- package/dist/web/assets/index-BURu6jt9.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Drt799Rs.css +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ZodError, z } from "zod";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { parse, stringify } from "yaml";
|
|
5
5
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import bcrypt from "bcrypt";
|
|
8
|
-
import { and, desc, eq, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm";
|
|
8
|
+
import { and, desc, eq, gt, inArray, isNotNull, isNull, lt, ne, sql } from "drizzle-orm";
|
|
9
9
|
import { drizzle } from "drizzle-orm/postgres-js";
|
|
10
10
|
import postgres from "postgres";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
@@ -36,7 +36,7 @@ import { SignJWT, jwtVerify } from "jose";
|
|
|
36
36
|
import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
|
|
37
37
|
//#region ../shared/dist/config/index.mjs
|
|
38
38
|
/** Declare a config field with a Zod schema and optional metadata. */
|
|
39
|
-
function field(schema, options) {
|
|
39
|
+
function field$1(schema, options) {
|
|
40
40
|
return {
|
|
41
41
|
_tag: "field",
|
|
42
42
|
_type: void 0,
|
|
@@ -45,46 +45,47 @@ function field(schema, options) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
/** Mark a config group as optional — present only when at least one field has an explicit value. */
|
|
48
|
-
function optional(shape) {
|
|
48
|
+
function optional$1(shape) {
|
|
49
49
|
return {
|
|
50
50
|
_tag: "optional",
|
|
51
51
|
shape
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
/** Define a config shape. Identity function used for type inference. */
|
|
55
|
-
function defineConfig(shape) {
|
|
55
|
+
function defineConfig$1(shape) {
|
|
56
56
|
return shape;
|
|
57
57
|
}
|
|
58
|
-
const agentConfigSchema = defineConfig({
|
|
59
|
-
token: field(z.string(), { secret: true }),
|
|
60
|
-
type: field(z.string().default("claude-code")),
|
|
61
|
-
|
|
62
|
-
concurrency: field(z.number().int().positive().default(5)),
|
|
58
|
+
const agentConfigSchema = defineConfig$1({
|
|
59
|
+
token: field$1(z.string(), { secret: true }),
|
|
60
|
+
type: field$1(z.string().default("claude-code")),
|
|
61
|
+
concurrency: field$1(z.number().int().positive().default(5)),
|
|
63
62
|
session: {
|
|
64
|
-
idle_timeout: field(z.number().int().positive().default(300)),
|
|
65
|
-
max_sessions: field(z.number().int().positive().default(10))
|
|
63
|
+
idle_timeout: field$1(z.number().int().positive().default(300)),
|
|
64
|
+
max_sessions: field$1(z.number().int().positive().default(10))
|
|
66
65
|
}
|
|
67
66
|
});
|
|
68
67
|
/** Store the resolved config as a singleton. Called by initConfig(). */
|
|
69
68
|
function setConfig(config) {}
|
|
70
69
|
/** Reset the config singleton. For testing only. */
|
|
71
70
|
function resetConfig() {}
|
|
72
|
-
const clientConfigSchema = defineConfig({
|
|
73
|
-
server: { url: field(z.string(), {
|
|
71
|
+
const clientConfigSchema = defineConfig$1({
|
|
72
|
+
server: { url: field$1(z.string(), {
|
|
74
73
|
env: "FIRST_TREE_HUB_SERVER_URL",
|
|
75
74
|
prompt: {
|
|
76
75
|
message: "Server URL:",
|
|
77
76
|
default: "http://localhost:8000"
|
|
78
77
|
}
|
|
79
78
|
}) },
|
|
80
|
-
logLevel: field(z.enum([
|
|
79
|
+
logLevel: field$1(z.enum([
|
|
81
80
|
"debug",
|
|
82
81
|
"info",
|
|
83
82
|
"warn",
|
|
84
83
|
"error"
|
|
85
84
|
]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
|
|
86
85
|
});
|
|
87
|
-
const
|
|
86
|
+
const DEFAULT_HOME_DIR$1 = join(homedir(), ".first-tree-hub");
|
|
87
|
+
const DEFAULT_CONFIG_DIR = join(DEFAULT_HOME_DIR$1, "config");
|
|
88
|
+
const DEFAULT_DATA_DIR$1 = join(DEFAULT_HOME_DIR$1, "data");
|
|
88
89
|
function isFieldDef(value) {
|
|
89
90
|
return typeof value === "object" && value !== null && "_tag" in value && value._tag === "field";
|
|
90
91
|
}
|
|
@@ -425,9 +426,9 @@ function loadAgents(options) {
|
|
|
425
426
|
}
|
|
426
427
|
return result;
|
|
427
428
|
}
|
|
428
|
-
const serverConfigSchema = defineConfig({
|
|
429
|
+
const serverConfigSchema = defineConfig$1({
|
|
429
430
|
database: {
|
|
430
|
-
url: field(z.string(), {
|
|
431
|
+
url: field$1(z.string(), {
|
|
431
432
|
env: "FIRST_TREE_HUB_DATABASE_URL",
|
|
432
433
|
auto: "docker-pg",
|
|
433
434
|
prompt: {
|
|
@@ -442,34 +443,34 @@ const serverConfigSchema = defineConfig({
|
|
|
442
443
|
}]
|
|
443
444
|
}
|
|
444
445
|
}),
|
|
445
|
-
provider: field(z.enum(["docker", "external"]).default("docker"))
|
|
446
|
+
provider: field$1(z.enum(["docker", "external"]).default("docker"))
|
|
446
447
|
},
|
|
447
448
|
server: {
|
|
448
|
-
port: field(z.number().default(8e3), { env: "FIRST_TREE_HUB_PORT" }),
|
|
449
|
-
host: field(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" })
|
|
449
|
+
port: field$1(z.number().default(8e3), { env: "FIRST_TREE_HUB_PORT" }),
|
|
450
|
+
host: field$1(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" })
|
|
450
451
|
},
|
|
451
452
|
secrets: {
|
|
452
|
-
jwtSecret: field(z.string(), {
|
|
453
|
+
jwtSecret: field$1(z.string(), {
|
|
453
454
|
env: "FIRST_TREE_HUB_JWT_SECRET",
|
|
454
455
|
auto: "random:base64url:32",
|
|
455
456
|
secret: true
|
|
456
457
|
}),
|
|
457
|
-
encryptionKey: field(z.string(), {
|
|
458
|
+
encryptionKey: field$1(z.string(), {
|
|
458
459
|
env: "FIRST_TREE_HUB_ENCRYPTION_KEY",
|
|
459
460
|
auto: "random:hex:32",
|
|
460
461
|
secret: true
|
|
461
462
|
})
|
|
462
463
|
},
|
|
463
464
|
contextTree: {
|
|
464
|
-
repo: field(z.string(), {
|
|
465
|
+
repo: field$1(z.string(), {
|
|
465
466
|
env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
|
|
466
467
|
prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
|
|
467
468
|
}),
|
|
468
|
-
branch: field(z.string().default("main")),
|
|
469
|
-
syncInterval: field(z.number().default(60))
|
|
469
|
+
branch: field$1(z.string().default("main")),
|
|
470
|
+
syncInterval: field$1(z.number().default(60))
|
|
470
471
|
},
|
|
471
472
|
github: {
|
|
472
|
-
token: field(z.string(), {
|
|
473
|
+
token: field$1(z.string(), {
|
|
473
474
|
env: "FIRST_TREE_HUB_GITHUB_TOKEN",
|
|
474
475
|
secret: true,
|
|
475
476
|
prompt: {
|
|
@@ -477,16 +478,16 @@ const serverConfigSchema = defineConfig({
|
|
|
477
478
|
type: "password"
|
|
478
479
|
}
|
|
479
480
|
}),
|
|
480
|
-
webhookSecret: field(z.string().optional(), {
|
|
481
|
+
webhookSecret: field$1(z.string().optional(), {
|
|
481
482
|
env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
|
|
482
483
|
secret: true
|
|
483
484
|
})
|
|
484
485
|
},
|
|
485
|
-
cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
|
|
486
|
-
rateLimit: optional({
|
|
487
|
-
max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
|
|
488
|
-
loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
|
|
489
|
-
webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
|
|
486
|
+
cors: optional$1({ origin: field$1(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
|
|
487
|
+
rateLimit: optional$1({
|
|
488
|
+
max: field$1(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
|
|
489
|
+
loginMax: field$1(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
|
|
490
|
+
webhookMax: field$1(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
|
|
490
491
|
})
|
|
491
492
|
});
|
|
492
493
|
//#endregion
|
|
@@ -22846,6 +22847,7 @@ function Aa({ prompt: $, options: X }) {
|
|
|
22846
22847
|
}
|
|
22847
22848
|
//#endregion
|
|
22848
22849
|
//#region ../client/dist/index.mjs
|
|
22850
|
+
const FETCH_TIMEOUT_MS = 15e3;
|
|
22849
22851
|
var FirstTreeHubSDK = class {
|
|
22850
22852
|
_baseUrl;
|
|
22851
22853
|
_token;
|
|
@@ -22868,9 +22870,16 @@ var FirstTreeHubSDK = class {
|
|
|
22868
22870
|
agentId: agent.id,
|
|
22869
22871
|
inboxId: agent.inboxId,
|
|
22870
22872
|
status: agent.status,
|
|
22871
|
-
displayName: agent.displayName
|
|
22873
|
+
displayName: agent.displayName,
|
|
22874
|
+
type: agent.type,
|
|
22875
|
+
delegateMention: agent.delegateMention ?? null,
|
|
22876
|
+
metadata: agent.metadata ?? {}
|
|
22872
22877
|
};
|
|
22873
22878
|
}
|
|
22879
|
+
/** Fetch Context Tree configuration from the server. */
|
|
22880
|
+
async getContextTreeConfig() {
|
|
22881
|
+
return this.requestJson("/api/v1/agent/context-tree");
|
|
22882
|
+
}
|
|
22874
22883
|
/** Fetch pending inbox entries. */
|
|
22875
22884
|
async pull(limit = 10) {
|
|
22876
22885
|
return { entries: await this.requestJson(`/api/v1/agent/inbox?limit=${limit}`) };
|
|
@@ -22925,9 +22934,12 @@ var FirstTreeHubSDK = class {
|
|
|
22925
22934
|
const url = `${this._baseUrl}${path}`;
|
|
22926
22935
|
const headers = { Authorization: `Bearer ${this._token}` };
|
|
22927
22936
|
if (init?.body) headers["Content-Type"] = "application/json";
|
|
22937
|
+
const timeout = AbortSignal.timeout(FETCH_TIMEOUT_MS);
|
|
22938
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, timeout]) : timeout;
|
|
22928
22939
|
return fetch(url, {
|
|
22929
22940
|
...init,
|
|
22930
|
-
headers
|
|
22941
|
+
headers,
|
|
22942
|
+
signal
|
|
22931
22943
|
});
|
|
22932
22944
|
}
|
|
22933
22945
|
async toSdkError(response) {
|
|
@@ -22952,12 +22964,16 @@ const DEFAULT_POLLING_INTERVAL = 5e3;
|
|
|
22952
22964
|
const DEFAULT_PULL_LIMIT = 10;
|
|
22953
22965
|
const RECONNECT_BASE_MS = 1e3;
|
|
22954
22966
|
const RECONNECT_MAX_MS = 3e4;
|
|
22967
|
+
const WS_CONNECT_TIMEOUT_MS = 1e4;
|
|
22968
|
+
const WS_PING_INTERVAL_MS = 3e3;
|
|
22955
22969
|
var AgentConnection = class extends EventEmitter {
|
|
22956
22970
|
sdk;
|
|
22957
22971
|
_state = "disconnected";
|
|
22958
22972
|
_agent = null;
|
|
22959
22973
|
handler = null;
|
|
22960
22974
|
ws = null;
|
|
22975
|
+
wsConnectTimer = null;
|
|
22976
|
+
wsPingTimer = null;
|
|
22961
22977
|
pollingTimer = null;
|
|
22962
22978
|
reconnectTimer = null;
|
|
22963
22979
|
reconnectAttempt = 0;
|
|
@@ -22968,6 +22984,7 @@ var AgentConnection = class extends EventEmitter {
|
|
|
22968
22984
|
pollingInterval;
|
|
22969
22985
|
pullLimit;
|
|
22970
22986
|
serverUrl;
|
|
22987
|
+
rateLimitedUntil = 0;
|
|
22971
22988
|
constructor(config) {
|
|
22972
22989
|
super();
|
|
22973
22990
|
this.serverUrl = config.serverUrl.replace(/\/+$/, "");
|
|
@@ -23010,10 +23027,19 @@ var AgentConnection = class extends EventEmitter {
|
|
|
23010
23027
|
}
|
|
23011
23028
|
openWebSocket() {
|
|
23012
23029
|
const ws = new WebSocket$1(`${this.serverUrl.replace(/^http/, "ws")}/api/v1/agent/ws/inbox`, { headers: { Authorization: `Bearer ${this.token}` } });
|
|
23030
|
+
this.wsConnectTimer = setTimeout(() => {
|
|
23031
|
+
this.wsConnectTimer = null;
|
|
23032
|
+
if (ws.readyState === WebSocket$1.CONNECTING) ws.terminate();
|
|
23033
|
+
}, WS_CONNECT_TIMEOUT_MS);
|
|
23013
23034
|
ws.on("open", () => {
|
|
23035
|
+
if (this.wsConnectTimer) {
|
|
23036
|
+
clearTimeout(this.wsConnectTimer);
|
|
23037
|
+
this.wsConnectTimer = null;
|
|
23038
|
+
}
|
|
23014
23039
|
this.reconnectAttempt = 0;
|
|
23015
23040
|
this._state = "connected";
|
|
23016
23041
|
this.emit("connected");
|
|
23042
|
+
this.startPing();
|
|
23017
23043
|
this.startPolling();
|
|
23018
23044
|
this.pullAndDispatch();
|
|
23019
23045
|
});
|
|
@@ -23023,14 +23049,17 @@ var AgentConnection = class extends EventEmitter {
|
|
|
23023
23049
|
} catch {}
|
|
23024
23050
|
});
|
|
23025
23051
|
ws.on("close", () => {
|
|
23052
|
+
this.stopPing();
|
|
23026
23053
|
if (!this.closing) this.scheduleReconnect();
|
|
23027
23054
|
});
|
|
23028
23055
|
ws.on("error", (err) => {
|
|
23056
|
+
if (this.handleRateLimit(err)) return;
|
|
23029
23057
|
this.emit("error", err);
|
|
23030
23058
|
});
|
|
23031
23059
|
this.ws = ws;
|
|
23032
23060
|
}
|
|
23033
23061
|
scheduleReconnect() {
|
|
23062
|
+
if (Date.now() < this.rateLimitedUntil) return;
|
|
23034
23063
|
this._state = "reconnecting";
|
|
23035
23064
|
this.reconnectAttempt++;
|
|
23036
23065
|
this.emit("reconnecting", this.reconnectAttempt);
|
|
@@ -23041,6 +23070,18 @@ var AgentConnection = class extends EventEmitter {
|
|
|
23041
23070
|
if (!this.closing) this.openWebSocket();
|
|
23042
23071
|
}, delay);
|
|
23043
23072
|
}
|
|
23073
|
+
startPing() {
|
|
23074
|
+
this.stopPing();
|
|
23075
|
+
this.wsPingTimer = setInterval(() => {
|
|
23076
|
+
if (this.ws?.readyState === WebSocket$1.OPEN) this.ws.ping();
|
|
23077
|
+
}, WS_PING_INTERVAL_MS);
|
|
23078
|
+
}
|
|
23079
|
+
stopPing() {
|
|
23080
|
+
if (this.wsPingTimer) {
|
|
23081
|
+
clearInterval(this.wsPingTimer);
|
|
23082
|
+
this.wsPingTimer = null;
|
|
23083
|
+
}
|
|
23084
|
+
}
|
|
23044
23085
|
startPolling() {
|
|
23045
23086
|
if (this.pollingTimer) return;
|
|
23046
23087
|
this.pollingTimer = setInterval(() => {
|
|
@@ -23055,6 +23096,7 @@ var AgentConnection = class extends EventEmitter {
|
|
|
23055
23096
|
}
|
|
23056
23097
|
async pullAndDispatch() {
|
|
23057
23098
|
if (this.closing || !this.handler) return;
|
|
23099
|
+
if (Date.now() < this.rateLimitedUntil) return;
|
|
23058
23100
|
if (this.isPulling) {
|
|
23059
23101
|
this.pullAgain = true;
|
|
23060
23102
|
return;
|
|
@@ -23074,13 +23116,42 @@ var AgentConnection = class extends EventEmitter {
|
|
|
23074
23116
|
}
|
|
23075
23117
|
} while (this.pullAgain && !this.closing);
|
|
23076
23118
|
} catch (err) {
|
|
23119
|
+
if (this.handleRateLimit(err)) return;
|
|
23077
23120
|
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
23078
23121
|
} finally {
|
|
23079
23122
|
this.isPulling = false;
|
|
23080
23123
|
}
|
|
23081
23124
|
}
|
|
23125
|
+
/** Detect 429 responses and pause all activity. Returns true if rate-limited. */
|
|
23126
|
+
handleRateLimit(err) {
|
|
23127
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23128
|
+
if (!msg.includes("429") && !msg.toLowerCase().includes("rate limit")) return false;
|
|
23129
|
+
const backoff = 6e4;
|
|
23130
|
+
this.rateLimitedUntil = Date.now() + backoff;
|
|
23131
|
+
this.emit("error", /* @__PURE__ */ new Error(`Rate limited, pausing for ${backoff / 1e3}s`));
|
|
23132
|
+
this.stopPolling();
|
|
23133
|
+
if (this.reconnectTimer) {
|
|
23134
|
+
clearTimeout(this.reconnectTimer);
|
|
23135
|
+
this.reconnectTimer = null;
|
|
23136
|
+
}
|
|
23137
|
+
setTimeout(() => {
|
|
23138
|
+
if (this.closing) return;
|
|
23139
|
+
this.rateLimitedUntil = 0;
|
|
23140
|
+
this.startPolling();
|
|
23141
|
+
if (!this.ws || this.ws.readyState !== WebSocket$1.OPEN) {
|
|
23142
|
+
this.reconnectAttempt = 0;
|
|
23143
|
+
this.openWebSocket();
|
|
23144
|
+
}
|
|
23145
|
+
}, backoff);
|
|
23146
|
+
return true;
|
|
23147
|
+
}
|
|
23082
23148
|
clearTimers() {
|
|
23083
23149
|
this.stopPolling();
|
|
23150
|
+
this.stopPing();
|
|
23151
|
+
if (this.wsConnectTimer) {
|
|
23152
|
+
clearTimeout(this.wsConnectTimer);
|
|
23153
|
+
this.wsConnectTimer = null;
|
|
23154
|
+
}
|
|
23084
23155
|
if (this.reconnectTimer) {
|
|
23085
23156
|
clearTimeout(this.reconnectTimer);
|
|
23086
23157
|
this.reconnectTimer = null;
|
|
@@ -23102,6 +23173,301 @@ function getHandlerFactory(type) {
|
|
|
23102
23173
|
}
|
|
23103
23174
|
return factory;
|
|
23104
23175
|
}
|
|
23176
|
+
/** Declare a config field with a Zod schema and optional metadata. */
|
|
23177
|
+
function field(schema, options) {
|
|
23178
|
+
return {
|
|
23179
|
+
_tag: "field",
|
|
23180
|
+
_type: void 0,
|
|
23181
|
+
schema,
|
|
23182
|
+
options: options ?? {}
|
|
23183
|
+
};
|
|
23184
|
+
}
|
|
23185
|
+
/** Mark a config group as optional — present only when at least one field has an explicit value. */
|
|
23186
|
+
function optional(shape) {
|
|
23187
|
+
return {
|
|
23188
|
+
_tag: "optional",
|
|
23189
|
+
shape
|
|
23190
|
+
};
|
|
23191
|
+
}
|
|
23192
|
+
/** Define a config shape. Identity function used for type inference. */
|
|
23193
|
+
function defineConfig(shape) {
|
|
23194
|
+
return shape;
|
|
23195
|
+
}
|
|
23196
|
+
defineConfig({
|
|
23197
|
+
token: field(z.string(), { secret: true }),
|
|
23198
|
+
type: field(z.string().default("claude-code")),
|
|
23199
|
+
concurrency: field(z.number().int().positive().default(5)),
|
|
23200
|
+
session: {
|
|
23201
|
+
idle_timeout: field(z.number().int().positive().default(300)),
|
|
23202
|
+
max_sessions: field(z.number().int().positive().default(10))
|
|
23203
|
+
}
|
|
23204
|
+
});
|
|
23205
|
+
defineConfig({
|
|
23206
|
+
server: { url: field(z.string(), {
|
|
23207
|
+
env: "FIRST_TREE_HUB_SERVER_URL",
|
|
23208
|
+
prompt: {
|
|
23209
|
+
message: "Server URL:",
|
|
23210
|
+
default: "http://localhost:8000"
|
|
23211
|
+
}
|
|
23212
|
+
}) },
|
|
23213
|
+
logLevel: field(z.enum([
|
|
23214
|
+
"debug",
|
|
23215
|
+
"info",
|
|
23216
|
+
"warn",
|
|
23217
|
+
"error"
|
|
23218
|
+
]).default("info"), { env: "FIRST_TREE_HUB_LOG_LEVEL" })
|
|
23219
|
+
});
|
|
23220
|
+
const DEFAULT_HOME_DIR = join(homedir(), ".first-tree-hub");
|
|
23221
|
+
join(DEFAULT_HOME_DIR, "config");
|
|
23222
|
+
const DEFAULT_DATA_DIR = join(DEFAULT_HOME_DIR, "data");
|
|
23223
|
+
defineConfig({
|
|
23224
|
+
database: {
|
|
23225
|
+
url: field(z.string(), {
|
|
23226
|
+
env: "FIRST_TREE_HUB_DATABASE_URL",
|
|
23227
|
+
auto: "docker-pg",
|
|
23228
|
+
prompt: {
|
|
23229
|
+
message: "PostgreSQL:",
|
|
23230
|
+
type: "select",
|
|
23231
|
+
choices: [{
|
|
23232
|
+
name: "Auto-provision via Docker",
|
|
23233
|
+
value: "__auto__"
|
|
23234
|
+
}, {
|
|
23235
|
+
name: "Provide connection URL",
|
|
23236
|
+
value: "__input__"
|
|
23237
|
+
}]
|
|
23238
|
+
}
|
|
23239
|
+
}),
|
|
23240
|
+
provider: field(z.enum(["docker", "external"]).default("docker"))
|
|
23241
|
+
},
|
|
23242
|
+
server: {
|
|
23243
|
+
port: field(z.number().default(8e3), { env: "FIRST_TREE_HUB_PORT" }),
|
|
23244
|
+
host: field(z.string().default("127.0.0.1"), { env: "FIRST_TREE_HUB_HOST" })
|
|
23245
|
+
},
|
|
23246
|
+
secrets: {
|
|
23247
|
+
jwtSecret: field(z.string(), {
|
|
23248
|
+
env: "FIRST_TREE_HUB_JWT_SECRET",
|
|
23249
|
+
auto: "random:base64url:32",
|
|
23250
|
+
secret: true
|
|
23251
|
+
}),
|
|
23252
|
+
encryptionKey: field(z.string(), {
|
|
23253
|
+
env: "FIRST_TREE_HUB_ENCRYPTION_KEY",
|
|
23254
|
+
auto: "random:hex:32",
|
|
23255
|
+
secret: true
|
|
23256
|
+
})
|
|
23257
|
+
},
|
|
23258
|
+
contextTree: {
|
|
23259
|
+
repo: field(z.string(), {
|
|
23260
|
+
env: "FIRST_TREE_HUB_CONTEXT_TREE_REPO",
|
|
23261
|
+
prompt: { message: "Context Tree repo URL (e.g. https://github.com/org/first-tree):" }
|
|
23262
|
+
}),
|
|
23263
|
+
branch: field(z.string().default("main")),
|
|
23264
|
+
syncInterval: field(z.number().default(60))
|
|
23265
|
+
},
|
|
23266
|
+
github: {
|
|
23267
|
+
token: field(z.string(), {
|
|
23268
|
+
env: "FIRST_TREE_HUB_GITHUB_TOKEN",
|
|
23269
|
+
secret: true,
|
|
23270
|
+
prompt: {
|
|
23271
|
+
message: "GitHub token (create at https://github.com/settings/tokens → repo scope):",
|
|
23272
|
+
type: "password"
|
|
23273
|
+
}
|
|
23274
|
+
}),
|
|
23275
|
+
webhookSecret: field(z.string().optional(), {
|
|
23276
|
+
env: "FIRST_TREE_HUB_GITHUB_WEBHOOK_SECRET",
|
|
23277
|
+
secret: true
|
|
23278
|
+
})
|
|
23279
|
+
},
|
|
23280
|
+
cors: optional({ origin: field(z.string(), { env: "FIRST_TREE_HUB_CORS_ORIGIN" }) }),
|
|
23281
|
+
rateLimit: optional({
|
|
23282
|
+
max: field(z.number().default(100), { env: "FIRST_TREE_HUB_RATE_LIMIT_MAX" }),
|
|
23283
|
+
loginMax: field(z.number().default(5), { env: "FIRST_TREE_HUB_RATE_LIMIT_LOGIN_MAX" }),
|
|
23284
|
+
webhookMax: field(z.number().default(60), { env: "FIRST_TREE_HUB_RATE_LIMIT_WEBHOOK_MAX" })
|
|
23285
|
+
})
|
|
23286
|
+
});
|
|
23287
|
+
const CONTEXT_TREE_DIR = join(DEFAULT_DATA_DIR, "context-tree");
|
|
23288
|
+
/**
|
|
23289
|
+
* Sync the shared Context Tree git clone.
|
|
23290
|
+
*
|
|
23291
|
+
* Clones on first run, pulls on subsequent runs.
|
|
23292
|
+
* Returns the clone path on success, null on failure (graceful degradation).
|
|
23293
|
+
*/
|
|
23294
|
+
async function syncContextTree(serverUrl, token, log) {
|
|
23295
|
+
try {
|
|
23296
|
+
execFileSync("git", ["--version"], { stdio: "ignore" });
|
|
23297
|
+
} catch {
|
|
23298
|
+
log("Context Tree sync skipped: git is not installed");
|
|
23299
|
+
return null;
|
|
23300
|
+
}
|
|
23301
|
+
let repo;
|
|
23302
|
+
let branch;
|
|
23303
|
+
try {
|
|
23304
|
+
const config = await new FirstTreeHubSDK({
|
|
23305
|
+
serverUrl,
|
|
23306
|
+
token
|
|
23307
|
+
}).getContextTreeConfig();
|
|
23308
|
+
repo = config.repo;
|
|
23309
|
+
branch = config.branch;
|
|
23310
|
+
} catch (err) {
|
|
23311
|
+
log(`Context Tree sync skipped: failed to fetch config from server (${err instanceof Error ? err.message : String(err)})`);
|
|
23312
|
+
return null;
|
|
23313
|
+
}
|
|
23314
|
+
try {
|
|
23315
|
+
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
23316
|
+
if (execFileSync("git", [
|
|
23317
|
+
"rev-parse",
|
|
23318
|
+
"--abbrev-ref",
|
|
23319
|
+
"HEAD"
|
|
23320
|
+
], {
|
|
23321
|
+
cwd: CONTEXT_TREE_DIR,
|
|
23322
|
+
encoding: "utf-8",
|
|
23323
|
+
timeout: 5e3
|
|
23324
|
+
}).trim() !== branch) {
|
|
23325
|
+
execFileSync("git", ["checkout", branch], {
|
|
23326
|
+
cwd: CONTEXT_TREE_DIR,
|
|
23327
|
+
stdio: "pipe",
|
|
23328
|
+
timeout: 1e4
|
|
23329
|
+
});
|
|
23330
|
+
log(`Context Tree switched to branch ${branch}`);
|
|
23331
|
+
}
|
|
23332
|
+
execFileSync("git", ["pull", "--ff-only"], {
|
|
23333
|
+
cwd: CONTEXT_TREE_DIR,
|
|
23334
|
+
stdio: "pipe",
|
|
23335
|
+
timeout: 3e4
|
|
23336
|
+
});
|
|
23337
|
+
log(`Context Tree updated (pull)`);
|
|
23338
|
+
} else {
|
|
23339
|
+
mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
|
|
23340
|
+
execFileSync("git", [
|
|
23341
|
+
"clone",
|
|
23342
|
+
"--branch",
|
|
23343
|
+
branch,
|
|
23344
|
+
"--single-branch",
|
|
23345
|
+
repo,
|
|
23346
|
+
CONTEXT_TREE_DIR
|
|
23347
|
+
], {
|
|
23348
|
+
stdio: "pipe",
|
|
23349
|
+
timeout: 6e4
|
|
23350
|
+
});
|
|
23351
|
+
log(`Context Tree cloned from ${repo} (branch: ${branch})`);
|
|
23352
|
+
}
|
|
23353
|
+
return CONTEXT_TREE_DIR;
|
|
23354
|
+
} catch (err) {
|
|
23355
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23356
|
+
log(`Context Tree sync failed: ${msg}`);
|
|
23357
|
+
log("Check that git credentials (SSH key or credential helper) are configured for this repo");
|
|
23358
|
+
if ((msg.includes("cannot fast-forward") || msg.includes("not possible to fast-forward") || msg.includes("CONFLICT")) && existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
23359
|
+
log("Diverged history detected, attempting fresh clone...");
|
|
23360
|
+
try {
|
|
23361
|
+
rmSync(CONTEXT_TREE_DIR, {
|
|
23362
|
+
recursive: true,
|
|
23363
|
+
force: true
|
|
23364
|
+
});
|
|
23365
|
+
mkdirSync(CONTEXT_TREE_DIR, { recursive: true });
|
|
23366
|
+
execFileSync("git", [
|
|
23367
|
+
"clone",
|
|
23368
|
+
"--branch",
|
|
23369
|
+
branch,
|
|
23370
|
+
"--single-branch",
|
|
23371
|
+
repo,
|
|
23372
|
+
CONTEXT_TREE_DIR
|
|
23373
|
+
], {
|
|
23374
|
+
stdio: "pipe",
|
|
23375
|
+
timeout: 6e4
|
|
23376
|
+
});
|
|
23377
|
+
log("Context Tree re-cloned successfully");
|
|
23378
|
+
return CONTEXT_TREE_DIR;
|
|
23379
|
+
} catch {
|
|
23380
|
+
log("Context Tree re-clone also failed, continuing without context");
|
|
23381
|
+
}
|
|
23382
|
+
}
|
|
23383
|
+
if (existsSync(join(CONTEXT_TREE_DIR, ".git"))) {
|
|
23384
|
+
log("Using existing Context Tree clone despite sync failure");
|
|
23385
|
+
return CONTEXT_TREE_DIR;
|
|
23386
|
+
}
|
|
23387
|
+
return null;
|
|
23388
|
+
}
|
|
23389
|
+
}
|
|
23390
|
+
/**
|
|
23391
|
+
* Bootstrap a workspace with .agent/ directory files.
|
|
23392
|
+
*
|
|
23393
|
+
* Writes identity.json, context/self.md (if context tree available), and tools.md.
|
|
23394
|
+
* Designed to be called on every handler start() and conditionally on resume().
|
|
23395
|
+
*/
|
|
23396
|
+
function bootstrapWorkspace(options) {
|
|
23397
|
+
const { workspacePath, identity, contextTreePath, serverUrl, chatId } = options;
|
|
23398
|
+
const agentDir = join(workspacePath, ".agent");
|
|
23399
|
+
const contextDir = join(agentDir, "context");
|
|
23400
|
+
if (existsSync(contextDir)) rmSync(contextDir, {
|
|
23401
|
+
recursive: true,
|
|
23402
|
+
force: true
|
|
23403
|
+
});
|
|
23404
|
+
mkdirSync(contextDir, { recursive: true });
|
|
23405
|
+
const identityData = {
|
|
23406
|
+
agentId: identity.agentId,
|
|
23407
|
+
displayName: identity.displayName,
|
|
23408
|
+
type: identity.type,
|
|
23409
|
+
delegateMention: identity.delegateMention,
|
|
23410
|
+
metadata: identity.metadata,
|
|
23411
|
+
chatId,
|
|
23412
|
+
serverUrl,
|
|
23413
|
+
contextTreePath
|
|
23414
|
+
};
|
|
23415
|
+
writeFileSync(join(agentDir, "identity.json"), JSON.stringify(identityData, null, 2), "utf-8");
|
|
23416
|
+
if (contextTreePath) {
|
|
23417
|
+
const selfNodePath = join(contextTreePath, "members", identity.agentId, "NODE.md");
|
|
23418
|
+
if (existsSync(selfNodePath)) copyFileSync(selfNodePath, join(contextDir, "self.md"));
|
|
23419
|
+
const agentMdPath = join(contextTreePath, "AGENT.md");
|
|
23420
|
+
if (existsSync(agentMdPath)) copyFileSync(agentMdPath, join(contextDir, "agent-instructions.md"));
|
|
23421
|
+
const rootNodePath = join(contextTreePath, "NODE.md");
|
|
23422
|
+
if (existsSync(rootNodePath)) copyFileSync(rootNodePath, join(contextDir, "domain-map.md"));
|
|
23423
|
+
} else writeFileSync(join(contextDir, "degraded.md"), "Context Tree is not available for this session.\nOrganizational context, domain structure, and ownership information are not loaded.\n", "utf-8");
|
|
23424
|
+
writeFileSync(join(agentDir, "tools.md"), generateToolsDoc(), "utf-8");
|
|
23425
|
+
}
|
|
23426
|
+
function generateToolsDoc() {
|
|
23427
|
+
return `# Agent Hub SDK
|
|
23428
|
+
|
|
23429
|
+
## How You Communicate
|
|
23430
|
+
|
|
23431
|
+
You are running inside **Agent Hub**, a messaging platform for agent teams.
|
|
23432
|
+
|
|
23433
|
+
- Messages from other team members arrive as your prompt input
|
|
23434
|
+
- Each message includes a \`[From: sender-id]\` header so you know who sent it
|
|
23435
|
+
- **Your final text response is automatically delivered** to the chat — just respond normally
|
|
23436
|
+
- For **proactive communication** (sending to other agents, other chats, or structured data),
|
|
23437
|
+
use the curl API endpoints below
|
|
23438
|
+
- **Use your judgment about when to respond.** Not every message requires a reply.
|
|
23439
|
+
Your role and responsibilities (defined in your profile above) guide your behavior
|
|
23440
|
+
|
|
23441
|
+
## Environment Variables
|
|
23442
|
+
|
|
23443
|
+
These are injected automatically when the agent process starts:
|
|
23444
|
+
|
|
23445
|
+
| Variable | Description |
|
|
23446
|
+
|----------|-------------|
|
|
23447
|
+
| \`FIRST_TREE_HUB_SERVER_URL\` | Server address for API calls |
|
|
23448
|
+
| \`FIRST_TREE_HUB_AGENT_TOKEN\` | Bearer token for authentication |
|
|
23449
|
+
| \`FIRST_TREE_HUB_CHAT_ID\` | Current chat context ID |
|
|
23450
|
+
| \`FIRST_TREE_HUB_AGENT_ID\` | Your agent ID |
|
|
23451
|
+
|
|
23452
|
+
## Sending Messages
|
|
23453
|
+
|
|
23454
|
+
Use curl or any HTTP client with the bearer token:
|
|
23455
|
+
|
|
23456
|
+
\`\`\`bash
|
|
23457
|
+
# Reply in current chat
|
|
23458
|
+
curl -X POST "$FIRST_TREE_HUB_SERVER_URL/api/v1/agent/chats/$FIRST_TREE_HUB_CHAT_ID/messages" \\
|
|
23459
|
+
-H "Authorization: Bearer $FIRST_TREE_HUB_AGENT_TOKEN" \\
|
|
23460
|
+
-H "Content-Type: application/json" \\
|
|
23461
|
+
-d '{"format": "text", "content": "your message"}'
|
|
23462
|
+
|
|
23463
|
+
# Send to another agent directly
|
|
23464
|
+
curl -X POST "$FIRST_TREE_HUB_SERVER_URL/api/v1/agent/agents/{agentId}/messages" \\
|
|
23465
|
+
-H "Authorization: Bearer $FIRST_TREE_HUB_AGENT_TOKEN" \\
|
|
23466
|
+
-H "Content-Type: application/json" \\
|
|
23467
|
+
-d '{"format": "text", "content": "your message"}'
|
|
23468
|
+
\`\`\`
|
|
23469
|
+
`;
|
|
23470
|
+
}
|
|
23105
23471
|
/**
|
|
23106
23472
|
* InputController — push-based async iterable bridge.
|
|
23107
23473
|
*
|
|
@@ -23156,6 +23522,46 @@ var InputController = class {
|
|
|
23156
23522
|
});
|
|
23157
23523
|
}
|
|
23158
23524
|
};
|
|
23525
|
+
const DEFAULT_WORKSPACE_TTL_MS = 10080 * 60 * 1e3;
|
|
23526
|
+
/**
|
|
23527
|
+
* Acquire a per-chat workspace directory.
|
|
23528
|
+
* Creates the directory if it does not exist; returns the path if it does.
|
|
23529
|
+
*/
|
|
23530
|
+
function acquireWorkspace(workspaceRoot, chatId) {
|
|
23531
|
+
const dir = join(workspaceRoot, chatId);
|
|
23532
|
+
mkdirSync(dir, { recursive: true });
|
|
23533
|
+
return dir;
|
|
23534
|
+
}
|
|
23535
|
+
/**
|
|
23536
|
+
* Clean stale workspace directories for an agent.
|
|
23537
|
+
*
|
|
23538
|
+
* A workspace is considered stale when:
|
|
23539
|
+
* 1. Its mtime is older than `ttlMs`
|
|
23540
|
+
* 2. Its chatId is NOT in the `activeChatIds` set
|
|
23541
|
+
*
|
|
23542
|
+
* Returns the list of removed chatIds.
|
|
23543
|
+
*/
|
|
23544
|
+
function cleanWorkspaces(workspaceRoot, activeChatIds, ttlMs = DEFAULT_WORKSPACE_TTL_MS) {
|
|
23545
|
+
if (!existsSync(workspaceRoot)) return [];
|
|
23546
|
+
const now = Date.now();
|
|
23547
|
+
const removed = [];
|
|
23548
|
+
for (const entry of readdirSync(workspaceRoot)) {
|
|
23549
|
+
if (activeChatIds.has(entry)) continue;
|
|
23550
|
+
const entryPath = join(workspaceRoot, entry);
|
|
23551
|
+
try {
|
|
23552
|
+
const stat = statSync(entryPath);
|
|
23553
|
+
if (!stat.isDirectory()) continue;
|
|
23554
|
+
if (now - stat.mtimeMs > ttlMs) {
|
|
23555
|
+
rmSync(entryPath, {
|
|
23556
|
+
recursive: true,
|
|
23557
|
+
force: true
|
|
23558
|
+
});
|
|
23559
|
+
removed.push(entry);
|
|
23560
|
+
}
|
|
23561
|
+
} catch {}
|
|
23562
|
+
}
|
|
23563
|
+
return removed;
|
|
23564
|
+
}
|
|
23159
23565
|
const MAX_RETRIES = 2;
|
|
23160
23566
|
/**
|
|
23161
23567
|
* Claude Code Handler — session-oriented handler using the Agent SDK.
|
|
@@ -23165,7 +23571,8 @@ const MAX_RETRIES = 2;
|
|
|
23165
23571
|
* and session resume from disk for idle reclaim recovery.
|
|
23166
23572
|
*/
|
|
23167
23573
|
const createClaudeCodeHandler = (config) => {
|
|
23168
|
-
const
|
|
23574
|
+
const workspaceRoot = config.workspaceRoot;
|
|
23575
|
+
let cwd = null;
|
|
23169
23576
|
let claudeSessionId = null;
|
|
23170
23577
|
let currentQuery = null;
|
|
23171
23578
|
let inputController = null;
|
|
@@ -23174,19 +23581,35 @@ const createClaudeCodeHandler = (config) => {
|
|
|
23174
23581
|
let retryCount = 0;
|
|
23175
23582
|
let ctx = null;
|
|
23176
23583
|
function toSDKUserMessage(message, sessionId) {
|
|
23584
|
+
const rawContent = typeof message.content === "string" ? message.content : JSON.stringify(message.content);
|
|
23177
23585
|
return {
|
|
23178
23586
|
type: "user",
|
|
23179
23587
|
message: {
|
|
23180
23588
|
role: "user",
|
|
23181
|
-
content:
|
|
23589
|
+
content: message.senderId ? `[From: ${message.senderId}]\n\n${rawContent}` : rawContent
|
|
23182
23590
|
},
|
|
23183
23591
|
parent_tool_use_id: null,
|
|
23184
23592
|
session_id: sessionId
|
|
23185
23593
|
};
|
|
23186
23594
|
}
|
|
23595
|
+
/**
|
|
23596
|
+
* Build env for the child Claude Code process.
|
|
23597
|
+
*
|
|
23598
|
+
* When the client runtime runs inside a Claude Code session (nested env),
|
|
23599
|
+
* process.env contains internal markers (CLAUDECODE, CLAUDE_CODE_ENTRYPOINT,
|
|
23600
|
+
* CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS, npm_lifecycle_script) that cause the
|
|
23601
|
+
* child to enable Agent Teams infrastructure and use wrong init paths,
|
|
23602
|
+
* resulting in ~90s cold start vs ~17s standalone. Strip these so the child
|
|
23603
|
+
* starts clean; the SDK sets its own CLAUDE_CODE_ENTRYPOINT="sdk-ts".
|
|
23604
|
+
*/
|
|
23187
23605
|
function buildEnv(sessionCtx) {
|
|
23606
|
+
const env = { ...process.env };
|
|
23607
|
+
delete env.CLAUDECODE;
|
|
23608
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
23609
|
+
delete env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS;
|
|
23610
|
+
delete env.npm_lifecycle_script;
|
|
23188
23611
|
return {
|
|
23189
|
-
...
|
|
23612
|
+
...env,
|
|
23190
23613
|
FIRST_TREE_HUB_SERVER_URL: sessionCtx.sdk.serverUrl,
|
|
23191
23614
|
FIRST_TREE_HUB_AGENT_TOKEN: sessionCtx.sdk.agentToken,
|
|
23192
23615
|
FIRST_TREE_HUB_CHAT_ID: sessionCtx.chatId,
|
|
@@ -23211,7 +23634,7 @@ const createClaudeCodeHandler = (config) => {
|
|
|
23211
23634
|
options: {
|
|
23212
23635
|
sessionId: resume ? void 0 : sessionId,
|
|
23213
23636
|
resume,
|
|
23214
|
-
cwd,
|
|
23637
|
+
cwd: cwd ?? void 0,
|
|
23215
23638
|
persistSession: true,
|
|
23216
23639
|
abortController,
|
|
23217
23640
|
permissionMode,
|
|
@@ -23228,10 +23651,15 @@ const createClaudeCodeHandler = (config) => {
|
|
|
23228
23651
|
sessionCtx.touch();
|
|
23229
23652
|
if (message.type === "result") {
|
|
23230
23653
|
const result = message;
|
|
23231
|
-
if (result.subtype === "success")
|
|
23232
|
-
|
|
23654
|
+
if (result.subtype === "success") {
|
|
23655
|
+
retryCount = 0;
|
|
23656
|
+
if (result.result && sessionCtx.chatId) sessionCtx.sdk.sendMessage(sessionCtx.chatId, {
|
|
23657
|
+
format: "text",
|
|
23658
|
+
content: result.result
|
|
23659
|
+
}).then(() => sessionCtx.log("Result forwarded to chat")).catch((err) => sessionCtx.log(`Failed to forward result: ${err instanceof Error ? err.message : String(err)}`));
|
|
23660
|
+
} else {
|
|
23233
23661
|
const errors = result.errors ? result.errors.join("; ") : result.subtype;
|
|
23234
|
-
sessionCtx.log(`Query result error: ${errors}`);
|
|
23662
|
+
sessionCtx.log(`Query result error: ${errors} (subtype=${result.subtype}, turns=${result.num_turns ?? "?"}, duration=${result.duration_ms ?? "?"}ms)`);
|
|
23235
23663
|
}
|
|
23236
23664
|
}
|
|
23237
23665
|
}
|
|
@@ -23239,6 +23667,13 @@ const createClaudeCodeHandler = (config) => {
|
|
|
23239
23667
|
} catch (err) {
|
|
23240
23668
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
23241
23669
|
sessionCtx.log(`Query error: ${errMsg}`);
|
|
23670
|
+
if (err instanceof Error) {
|
|
23671
|
+
if (err.cause) sessionCtx.log(` cause: ${err.cause instanceof Error ? err.cause.message : String(err.cause)}`);
|
|
23672
|
+
if ("exitCode" in err) sessionCtx.log(` exitCode: ${err.exitCode}`);
|
|
23673
|
+
if ("stderr" in err) sessionCtx.log(` stderr: ${err.stderr}`);
|
|
23674
|
+
if ("code" in err) sessionCtx.log(` code: ${err.code}`);
|
|
23675
|
+
if (err.stack) sessionCtx.log(` stack: ${err.stack.split("\n").slice(1, 4).join(" | ")}`);
|
|
23676
|
+
}
|
|
23242
23677
|
if (retryCount >= MAX_RETRIES || !claudeSessionId) {
|
|
23243
23678
|
sessionCtx.log("Exhausted retries, session will be suspended");
|
|
23244
23679
|
return;
|
|
@@ -23254,12 +23689,28 @@ const createClaudeCodeHandler = (config) => {
|
|
|
23254
23689
|
}
|
|
23255
23690
|
}
|
|
23256
23691
|
}
|
|
23692
|
+
const contextTreePath = config.contextTreePath ?? null;
|
|
23693
|
+
/** Bootstrap workspace and generate CLAUDE.md. */
|
|
23694
|
+
function runBootstrap(workspace, sessionCtx) {
|
|
23695
|
+
bootstrapWorkspace({
|
|
23696
|
+
workspacePath: workspace,
|
|
23697
|
+
identity: sessionCtx.agent,
|
|
23698
|
+
contextTreePath,
|
|
23699
|
+
serverUrl: sessionCtx.sdk.serverUrl,
|
|
23700
|
+
chatId: sessionCtx.chatId
|
|
23701
|
+
});
|
|
23702
|
+
generateClaudeMd(workspace, sessionCtx.agent, contextTreePath);
|
|
23703
|
+
}
|
|
23257
23704
|
const handler = {
|
|
23258
23705
|
async start(message, sessionCtx) {
|
|
23259
23706
|
ctx = sessionCtx;
|
|
23260
23707
|
claudeSessionId = randomUUID();
|
|
23708
|
+
cwd = acquireWorkspace(workspaceRoot, sessionCtx.chatId);
|
|
23709
|
+
runBootstrap(cwd, sessionCtx);
|
|
23710
|
+
sessionCtx.log(`Starting session (${claudeSessionId}), cwd=${cwd}, permissionMode=${config.permissionMode ?? "bypassPermissions"}`);
|
|
23261
23711
|
spawnQuery(claudeSessionId, sessionCtx);
|
|
23262
|
-
|
|
23712
|
+
const sdkMsg = toSDKUserMessage(message, claudeSessionId);
|
|
23713
|
+
inputController?.push(sdkMsg);
|
|
23263
23714
|
sessionCtx.log(`Session started (${claudeSessionId})`);
|
|
23264
23715
|
return claudeSessionId;
|
|
23265
23716
|
},
|
|
@@ -23267,6 +23718,9 @@ const createClaudeCodeHandler = (config) => {
|
|
|
23267
23718
|
ctx = sessionCtx;
|
|
23268
23719
|
claudeSessionId = sessionId;
|
|
23269
23720
|
retryCount = 0;
|
|
23721
|
+
cwd = acquireWorkspace(workspaceRoot, sessionCtx.chatId);
|
|
23722
|
+
if (!existsSync(join(cwd, ".agent", "identity.json"))) runBootstrap(cwd, sessionCtx);
|
|
23723
|
+
sessionCtx.log(`Resuming session (${sessionId}), cwd=${cwd}`);
|
|
23270
23724
|
spawnQuery(sessionId, sessionCtx, sessionId);
|
|
23271
23725
|
inputController?.push(toSDKUserMessage(message, sessionId));
|
|
23272
23726
|
sessionCtx.log(`Session resumed (${sessionId})`);
|
|
@@ -23301,6 +23755,50 @@ const createClaudeCodeHandler = (config) => {
|
|
|
23301
23755
|
};
|
|
23302
23756
|
return handler;
|
|
23303
23757
|
};
|
|
23758
|
+
/**
|
|
23759
|
+
* Generate a CLAUDE.md file from .agent/ bootstrap data.
|
|
23760
|
+
*
|
|
23761
|
+
* Layered Bootstrap:
|
|
23762
|
+
* Layer 1 (always): Agent identity + member profile + AGENT.md operating instructions
|
|
23763
|
+
* Layer 2 (if available): Organization domain map from root NODE.md
|
|
23764
|
+
* Layer 3 (on-demand): Agent reads specific domain nodes via contextTreePath
|
|
23765
|
+
*/
|
|
23766
|
+
function generateClaudeMd(workspacePath, identity, contextTreePath) {
|
|
23767
|
+
const sections = [];
|
|
23768
|
+
const contextDir = join(workspacePath, ".agent", "context");
|
|
23769
|
+
const name = identity.displayName ?? identity.agentId;
|
|
23770
|
+
if (identity.type === "personal_assistant") sections.push(`# Agent Identity\n\nYou are ${name}, a personal assistant agent.\n`);
|
|
23771
|
+
else sections.push(`# Agent Identity\n\nYou are ${name}, an autonomous agent.\n`);
|
|
23772
|
+
const selfMdPath = join(contextDir, "self.md");
|
|
23773
|
+
if (existsSync(selfMdPath)) {
|
|
23774
|
+
const selfContent = readFileSync(selfMdPath, "utf-8");
|
|
23775
|
+
sections.push(`## Your Profile\n\n${selfContent}\n`);
|
|
23776
|
+
} else sections.push("## Your Profile\n\nNo member profile available. Your responsibilities are not loaded from the Context Tree.\n");
|
|
23777
|
+
const agentInstructionsPath = join(contextDir, "agent-instructions.md");
|
|
23778
|
+
if (existsSync(agentInstructionsPath)) {
|
|
23779
|
+
const instructions = readFileSync(agentInstructionsPath, "utf-8");
|
|
23780
|
+
sections.push(`## Context Tree Operating Instructions\n\n${instructions}\n`);
|
|
23781
|
+
} else sections.push("## Context Tree Operating Instructions\n\nContext Tree instructions unavailable. Organizational context is not loaded for this session.\n");
|
|
23782
|
+
const domainMapPath = join(contextDir, "domain-map.md");
|
|
23783
|
+
if (existsSync(domainMapPath)) {
|
|
23784
|
+
const domainMap = readFileSync(domainMapPath, "utf-8");
|
|
23785
|
+
sections.push(`## Organization Domain Map\n\n${domainMap}\n`);
|
|
23786
|
+
}
|
|
23787
|
+
if (contextTreePath) sections.push(`## Context Tree Location\n\nThe full Context Tree is available at: \`${contextTreePath}\`\n\nRead specific domain nodes as needed following the operating instructions above.\n`);
|
|
23788
|
+
else {
|
|
23789
|
+
const degradedPath = join(contextDir, "degraded.md");
|
|
23790
|
+
if (existsSync(degradedPath)) {
|
|
23791
|
+
const degradedMsg = readFileSync(degradedPath, "utf-8");
|
|
23792
|
+
sections.push(`## Context Tree Location\n\nWARNING: ${degradedMsg}\nYou can still use the SDK tools below, but you lack organizational context for decisions.\n`);
|
|
23793
|
+
}
|
|
23794
|
+
}
|
|
23795
|
+
const toolsPath = join(workspacePath, ".agent", "tools.md");
|
|
23796
|
+
if (existsSync(toolsPath)) {
|
|
23797
|
+
const toolsContent = readFileSync(toolsPath, "utf-8");
|
|
23798
|
+
sections.push(toolsContent);
|
|
23799
|
+
}
|
|
23800
|
+
writeFileSync(join(workspacePath, "CLAUDE.md"), sections.join("\n"), "utf-8");
|
|
23801
|
+
}
|
|
23304
23802
|
/** Register all built-in handlers. Call once at startup. */
|
|
23305
23803
|
function registerBuiltinHandlers() {
|
|
23306
23804
|
registerHandler("claude-code", createClaudeCodeHandler);
|
|
@@ -23700,18 +24198,24 @@ var AgentSlot = class {
|
|
|
23700
24198
|
this.connection.on("reconnecting", (attempt) => this.logFn(`Reconnecting (attempt ${attempt})...`));
|
|
23701
24199
|
this.connection.on("error", (err) => this.logFn(`Error: ${err.message}`));
|
|
23702
24200
|
}
|
|
23703
|
-
async start() {
|
|
24201
|
+
async start(contextTreePath) {
|
|
23704
24202
|
const agent = await this.connection.connect();
|
|
23705
24203
|
this.logFn(`Registered as ${agent.displayName ?? agent.agentId} (${agent.agentId})`);
|
|
23706
|
-
const registryPath = join(
|
|
24204
|
+
const registryPath = join(DEFAULT_DATA_DIR, "sessions", `${this.config.name}.json`);
|
|
23707
24205
|
this.sessionManager = new SessionManager({
|
|
23708
24206
|
session: this.config.session,
|
|
23709
24207
|
concurrency: this.config.concurrency,
|
|
23710
24208
|
handlerFactory: this.config.handlerFactory,
|
|
23711
|
-
handlerConfig: {
|
|
24209
|
+
handlerConfig: {
|
|
24210
|
+
workspaceRoot: join(DEFAULT_DATA_DIR, "workspaces", this.config.name),
|
|
24211
|
+
contextTreePath: contextTreePath ?? void 0
|
|
24212
|
+
},
|
|
23712
24213
|
agentIdentity: {
|
|
23713
24214
|
agentId: agent.agentId,
|
|
23714
|
-
displayName: agent.displayName
|
|
24215
|
+
displayName: agent.displayName,
|
|
24216
|
+
type: agent.type,
|
|
24217
|
+
delegateMention: agent.delegateMention,
|
|
24218
|
+
metadata: agent.metadata
|
|
23715
24219
|
},
|
|
23716
24220
|
sdk: this.connection.sdk,
|
|
23717
24221
|
log: this.logFn,
|
|
@@ -23735,7 +24239,6 @@ const sessionConfigSchema = z.object({
|
|
|
23735
24239
|
const agentSlotConfigSchema = z.object({
|
|
23736
24240
|
token: z.string().min(1),
|
|
23737
24241
|
type: z.string().min(1),
|
|
23738
|
-
cwd: z.string().optional(),
|
|
23739
24242
|
session: sessionConfigSchema.default({}),
|
|
23740
24243
|
concurrency: z.number().int().positive().default(5)
|
|
23741
24244
|
});
|
|
@@ -23767,30 +24270,34 @@ function loadRuntimeConfig(configPath) {
|
|
|
23767
24270
|
const DEFAULT_SHUTDOWN_TIMEOUT = 3e4;
|
|
23768
24271
|
var AgentRuntime = class {
|
|
23769
24272
|
slots = [];
|
|
24273
|
+
config;
|
|
23770
24274
|
shutdownTimeout;
|
|
23771
24275
|
stopping = false;
|
|
23772
24276
|
constructor(options) {
|
|
23773
|
-
|
|
24277
|
+
this.config = options.config;
|
|
23774
24278
|
this.shutdownTimeout = options.shutdownTimeout ?? DEFAULT_SHUTDOWN_TIMEOUT;
|
|
23775
|
-
for (const [name, agentConfig] of Object.entries(config.agents)) {
|
|
24279
|
+
for (const [name, agentConfig] of Object.entries(this.config.agents)) {
|
|
23776
24280
|
const handlerFactory = getHandlerFactory(agentConfig.type);
|
|
23777
24281
|
this.slots.push(new AgentSlot({
|
|
23778
24282
|
name,
|
|
23779
|
-
serverUrl: config.server,
|
|
24283
|
+
serverUrl: this.config.server,
|
|
23780
24284
|
token: agentConfig.token,
|
|
23781
24285
|
type: agentConfig.type,
|
|
23782
24286
|
handlerFactory,
|
|
23783
24287
|
session: agentConfig.session,
|
|
23784
|
-
concurrency: agentConfig.concurrency
|
|
23785
|
-
cwd: agentConfig.cwd
|
|
24288
|
+
concurrency: agentConfig.concurrency
|
|
23786
24289
|
}));
|
|
23787
24290
|
}
|
|
23788
24291
|
}
|
|
23789
24292
|
/** Start all agent slots and block until shutdown signal. */
|
|
23790
24293
|
async start() {
|
|
23791
24294
|
const log = (msg) => process.stderr.write(`[runtime] ${msg}\n`);
|
|
24295
|
+
const firstToken = Object.values(this.config.agents)[0]?.token;
|
|
24296
|
+
let contextTreePath = null;
|
|
24297
|
+
if (firstToken) contextTreePath = await syncContextTree(this.config.server, firstToken, log);
|
|
24298
|
+
if (!contextTreePath) log("WARNING: Context Tree sync failed — agents will start without organizational context");
|
|
23792
24299
|
log(`Starting ${this.slots.length} agent(s)...`);
|
|
23793
|
-
const results = await Promise.allSettled(this.slots.map((slot) => slot.start()));
|
|
24300
|
+
const results = await Promise.allSettled(this.slots.map((slot) => slot.start(contextTreePath)));
|
|
23794
24301
|
let failed = 0;
|
|
23795
24302
|
for (const result of results) if (result.status === "rejected") {
|
|
23796
24303
|
log(`Failed to start agent: ${result.reason instanceof Error ? result.reason.message : result.reason}`);
|
|
@@ -24705,6 +25212,7 @@ z.object({
|
|
|
24705
25212
|
type: agentTypeSchema,
|
|
24706
25213
|
displayName: z.string().nullable(),
|
|
24707
25214
|
delegateMention: z.string().nullable(),
|
|
25215
|
+
treePath: z.string().nullable(),
|
|
24708
25216
|
inboxId: z.string(),
|
|
24709
25217
|
status: z.string(),
|
|
24710
25218
|
metadata: z.record(z.unknown()),
|
|
@@ -24832,7 +25340,7 @@ const SYSTEM_CONFIG_DEFAULTS = {
|
|
|
24832
25340
|
[SYSTEM_CONFIG_KEYS.PRESENCE_CLEANUP_SECONDS]: 60
|
|
24833
25341
|
};
|
|
24834
25342
|
//#endregion
|
|
24835
|
-
//#region ../server/dist/app-
|
|
25343
|
+
//#region ../server/dist/app-DtKgrri9.mjs
|
|
24836
25344
|
var __defProp = Object.defineProperty;
|
|
24837
25345
|
var __exportAll = (all, no_symbols) => {
|
|
24838
25346
|
let target = {};
|
|
@@ -24850,6 +25358,7 @@ const agents = pgTable("agents", {
|
|
|
24850
25358
|
type: text("type").notNull(),
|
|
24851
25359
|
displayName: text("display_name"),
|
|
24852
25360
|
delegateMention: text("delegate_mention"),
|
|
25361
|
+
treePath: text("tree_path"),
|
|
24853
25362
|
inboxId: text("inbox_id").unique().notNull(),
|
|
24854
25363
|
status: text("status").notNull().default("active"),
|
|
24855
25364
|
metadata: jsonb("metadata").$type().notNull().default({}),
|
|
@@ -25331,6 +25840,7 @@ async function adminAdapterRoutes(app) {
|
|
|
25331
25840
|
});
|
|
25332
25841
|
}
|
|
25333
25842
|
const GRAPHQL_URL = "https://api.github.com/graphql";
|
|
25843
|
+
const REST_API_URL = "https://api.github.com";
|
|
25334
25844
|
/** Parse "owner/repo" or "https://github.com/owner/repo" into { owner, name }. */
|
|
25335
25845
|
function parseRepo(input) {
|
|
25336
25846
|
const urlMatch = /github\.com\/([^/]+)\/([^/.]+)/.exec(input);
|
|
@@ -25344,31 +25854,13 @@ function parseRepo(input) {
|
|
|
25344
25854
|
name: parts[1] ?? ""
|
|
25345
25855
|
};
|
|
25346
25856
|
}
|
|
25347
|
-
/**
|
|
25348
|
-
|
|
25349
|
-
* Single request regardless of member count.
|
|
25350
|
-
*/
|
|
25351
|
-
async function fetchMembers(repo, branch, token) {
|
|
25352
|
-
const { owner, name } = parseRepo(repo);
|
|
25353
|
-
if (!owner || !name) throw new Error(`Invalid repo format: "${repo}" — expected "owner/repo" or a GitHub URL`);
|
|
25857
|
+
/** Step 1: Get the tree OID of the members/ directory via GraphQL. */
|
|
25858
|
+
async function fetchMembersTreeOid(owner, name, branch, token) {
|
|
25354
25859
|
const query = `
|
|
25355
25860
|
query($owner: String!, $name: String!, $expr: String!) {
|
|
25356
25861
|
repository(owner: $owner, name: $name) {
|
|
25357
25862
|
object(expression: $expr) {
|
|
25358
|
-
... on Tree {
|
|
25359
|
-
entries {
|
|
25360
|
-
name
|
|
25361
|
-
type
|
|
25362
|
-
object {
|
|
25363
|
-
... on Tree {
|
|
25364
|
-
entries {
|
|
25365
|
-
name
|
|
25366
|
-
object { ... on Blob { text } }
|
|
25367
|
-
}
|
|
25368
|
-
}
|
|
25369
|
-
}
|
|
25370
|
-
}
|
|
25371
|
-
}
|
|
25863
|
+
... on Tree { oid }
|
|
25372
25864
|
}
|
|
25373
25865
|
}
|
|
25374
25866
|
}
|
|
@@ -25391,36 +25883,128 @@ async function fetchMembers(repo, branch, token) {
|
|
|
25391
25883
|
if (!res.ok) throw new Error(`GitHub API returned ${res.status}: ${await res.text()}`);
|
|
25392
25884
|
const json = await res.json();
|
|
25393
25885
|
if (json.errors) throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
|
|
25394
|
-
|
|
25395
|
-
|
|
25396
|
-
|
|
25397
|
-
|
|
25398
|
-
|
|
25399
|
-
|
|
25400
|
-
|
|
25401
|
-
|
|
25402
|
-
|
|
25403
|
-
}
|
|
25404
|
-
|
|
25886
|
+
return json.data?.repository?.object?.oid ?? null;
|
|
25887
|
+
}
|
|
25888
|
+
/** Step 2: Recursively list all entries under the members/ tree via REST API. */
|
|
25889
|
+
async function fetchRecursiveTree(owner, name, treeSha, token) {
|
|
25890
|
+
const url = `${REST_API_URL}/repos/${owner}/${name}/git/trees/${treeSha}?recursive=1`;
|
|
25891
|
+
const res = await fetch(url, { headers: {
|
|
25892
|
+
Authorization: `Bearer ${token}`,
|
|
25893
|
+
Accept: "application/vnd.github+json"
|
|
25894
|
+
} });
|
|
25895
|
+
if (!res.ok) throw new Error(`GitHub REST API returned ${res.status}: ${await res.text()}`);
|
|
25896
|
+
const json = await res.json();
|
|
25897
|
+
if (json.truncated) throw new Error("[context-tree-sync] GitHub REST tree API returned truncated response — members/ subtree is too large. Sync aborted to prevent incorrect agent suspension from partial data.");
|
|
25898
|
+
return json.tree;
|
|
25405
25899
|
}
|
|
25406
|
-
/**
|
|
25407
|
-
|
|
25408
|
-
|
|
25409
|
-
|
|
25410
|
-
|
|
25411
|
-
|
|
25412
|
-
|
|
25413
|
-
|
|
25414
|
-
|
|
25415
|
-
|
|
25416
|
-
const
|
|
25417
|
-
return
|
|
25418
|
-
}
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25900
|
+
/**
|
|
25901
|
+
* Step 3: Batch-fetch NODE.md content for all member directories via GraphQL aliases.
|
|
25902
|
+
* Each alias fetches one NODE.md file by expression.
|
|
25903
|
+
*/
|
|
25904
|
+
async function batchFetchNodeMd(owner, name, branch, memberPaths, token) {
|
|
25905
|
+
if (memberPaths.length === 0) return /* @__PURE__ */ new Map();
|
|
25906
|
+
const query = `
|
|
25907
|
+
query($owner: String!, $name: String!) {
|
|
25908
|
+
repository(owner: $owner, name: $name) {
|
|
25909
|
+
${memberPaths.map((p, i) => {
|
|
25910
|
+
const expr = `${branch}:members/${p}/NODE.md`;
|
|
25911
|
+
return `m${i}: object(expression: ${JSON.stringify(expr)}) { ... on Blob { text } }`;
|
|
25912
|
+
}).join("\n ")}
|
|
25913
|
+
}
|
|
25914
|
+
}
|
|
25915
|
+
`;
|
|
25916
|
+
const res = await fetch(GRAPHQL_URL, {
|
|
25917
|
+
method: "POST",
|
|
25918
|
+
headers: {
|
|
25919
|
+
Authorization: `Bearer ${token}`,
|
|
25920
|
+
"Content-Type": "application/json"
|
|
25921
|
+
},
|
|
25922
|
+
body: JSON.stringify({
|
|
25923
|
+
query,
|
|
25924
|
+
variables: {
|
|
25925
|
+
owner,
|
|
25926
|
+
name
|
|
25927
|
+
}
|
|
25928
|
+
})
|
|
25929
|
+
});
|
|
25930
|
+
if (!res.ok) throw new Error(`GitHub API returned ${res.status}: ${await res.text()}`);
|
|
25931
|
+
const json = await res.json();
|
|
25932
|
+
if (json.errors) throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(", ")}`);
|
|
25933
|
+
const repo = json.data?.repository ?? {};
|
|
25934
|
+
const result = /* @__PURE__ */ new Map();
|
|
25935
|
+
for (let i = 0; i < memberPaths.length; i++) {
|
|
25936
|
+
const blob = repo[`m${i}`];
|
|
25937
|
+
const path = memberPaths[i];
|
|
25938
|
+
if (blob?.text && path) result.set(path, blob.text);
|
|
25939
|
+
}
|
|
25940
|
+
return result;
|
|
25941
|
+
}
|
|
25942
|
+
/**
|
|
25943
|
+
* Extract member directory paths from a recursive tree listing.
|
|
25944
|
+
* A directory is a member if it contains a NODE.md blob.
|
|
25945
|
+
*/
|
|
25946
|
+
function extractMemberDirs(treeEntries) {
|
|
25947
|
+
const nodeMdPaths = /* @__PURE__ */ new Set();
|
|
25948
|
+
for (const entry of treeEntries) if (entry.type === "blob" && entry.path.endsWith("/NODE.md")) nodeMdPaths.add(entry.path);
|
|
25949
|
+
const memberDirs = [];
|
|
25950
|
+
for (const entry of treeEntries) {
|
|
25951
|
+
if (entry.type !== "tree") continue;
|
|
25952
|
+
if (nodeMdPaths.has(`${entry.path}/NODE.md`)) memberDirs.push(entry.path);
|
|
25953
|
+
}
|
|
25954
|
+
return memberDirs.sort();
|
|
25955
|
+
}
|
|
25956
|
+
/**
|
|
25957
|
+
* Fetch all members from a Context Tree repo via GitHub API.
|
|
25958
|
+
* Uses 3 API calls:
|
|
25959
|
+
* 1. GraphQL: get members/ tree OID
|
|
25960
|
+
* 2. REST: recursive tree listing (scoped to members/ only)
|
|
25961
|
+
* 3. GraphQL: batch-fetch all NODE.md contents via aliases
|
|
25962
|
+
*/
|
|
25963
|
+
async function fetchMembers(repo, branch, token) {
|
|
25964
|
+
const { owner, name } = parseRepo(repo);
|
|
25965
|
+
if (!owner || !name) throw new Error(`Invalid repo format: "${repo}" — expected "owner/repo" or a GitHub URL`);
|
|
25966
|
+
const treeOid = await fetchMembersTreeOid(owner, name, branch, token);
|
|
25967
|
+
if (!treeOid) {
|
|
25968
|
+
console.warn("[context-tree-sync] members/ directory not found in repo");
|
|
25969
|
+
return [];
|
|
25970
|
+
}
|
|
25971
|
+
const memberDirs = extractMemberDirs(await fetchRecursiveTree(owner, name, treeOid, token));
|
|
25972
|
+
if (memberDirs.length === 0) {
|
|
25973
|
+
console.warn("[context-tree-sync] No member directories with NODE.md found");
|
|
25974
|
+
return [];
|
|
25975
|
+
}
|
|
25976
|
+
const nameMap = /* @__PURE__ */ new Map();
|
|
25977
|
+
for (const dir of memberDirs) {
|
|
25978
|
+
const dirName = dir.split("/").pop() ?? dir;
|
|
25979
|
+
const existing = nameMap.get(dirName);
|
|
25980
|
+
if (existing) throw new Error(`[context-tree-sync] Duplicate member directory name '${dirName}' found at 'members/${existing}' and 'members/${dir}' — directory names must be unique across all levels under members/. Fix this in the Context Tree repo.`);
|
|
25981
|
+
nameMap.set(dirName, dir);
|
|
25982
|
+
}
|
|
25983
|
+
const nodeContents = await batchFetchNodeMd(owner, name, branch, memberDirs, token);
|
|
25984
|
+
return memberDirs.map((dir) => ({
|
|
25985
|
+
name: dir.split("/").pop() ?? dir,
|
|
25986
|
+
treePath: dir,
|
|
25987
|
+
nodeContent: nodeContents.get(dir) ?? null
|
|
25988
|
+
}));
|
|
25989
|
+
}
|
|
25990
|
+
/** Parse NODE.md frontmatter for agent metadata. */
|
|
25991
|
+
function parseNodeMetadata(content) {
|
|
25992
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
25993
|
+
if (!match) return {
|
|
25994
|
+
type: "autonomous_agent",
|
|
25995
|
+
displayName: null,
|
|
25996
|
+
delegateMention: null
|
|
25997
|
+
};
|
|
25998
|
+
const frontmatter = match[1] ?? "";
|
|
25999
|
+
const getValue = (key) => {
|
|
26000
|
+
const lineMatch = new RegExp(`^${key}:\\s*(.+)$`, "m").exec(frontmatter);
|
|
26001
|
+
return lineMatch ? lineMatch[1]?.trim().replace(/^["']|["']$/g, "") ?? null : null;
|
|
26002
|
+
};
|
|
26003
|
+
return {
|
|
26004
|
+
type: getValue("type") ?? "autonomous_agent",
|
|
26005
|
+
displayName: getValue("display_name") ?? getValue("title") ?? getValue("name"),
|
|
26006
|
+
delegateMention: getValue("delegate_mention")
|
|
26007
|
+
};
|
|
25424
26008
|
}
|
|
25425
26009
|
/** Stored for the /status endpoint */
|
|
25426
26010
|
let _lastSyncResult;
|
|
@@ -25455,31 +26039,31 @@ async function syncFromGitHub(db, repo, branch, githubToken) {
|
|
|
25455
26039
|
displayName: null,
|
|
25456
26040
|
delegateMention: null
|
|
25457
26041
|
};
|
|
25458
|
-
const existing = await db.execute(sql`SELECT id, status, type, display_name, delegate_mention FROM agents WHERE id = ${member.name}`);
|
|
26042
|
+
const existing = await db.execute(sql`SELECT id, status, type, display_name, delegate_mention, tree_path FROM agents WHERE id = ${member.name}`);
|
|
25459
26043
|
if (existing.length === 0) {
|
|
25460
26044
|
await db.execute(sql`
|
|
25461
|
-
INSERT INTO agents (id, type, display_name, delegate_mention, status, inbox_id)
|
|
25462
|
-
VALUES (${member.name}, ${meta.type}, ${meta.displayName}, ${meta.delegateMention}, 'active', ${`inbox_${member.name}`})
|
|
26045
|
+
INSERT INTO agents (id, type, display_name, delegate_mention, tree_path, status, inbox_id)
|
|
26046
|
+
VALUES (${member.name}, ${meta.type}, ${meta.displayName}, ${meta.delegateMention}, ${member.treePath}, 'active', ${`inbox_${member.name}`})
|
|
25463
26047
|
`);
|
|
25464
26048
|
result.created++;
|
|
25465
26049
|
} else {
|
|
25466
26050
|
const agent = existing[0];
|
|
25467
26051
|
if (agent.status === "suspended") {
|
|
25468
26052
|
await db.execute(sql`
|
|
25469
|
-
UPDATE agents SET status = 'active', type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}
|
|
26053
|
+
UPDATE agents SET status = 'active', type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}
|
|
25470
26054
|
WHERE id = ${member.name}
|
|
25471
26055
|
`);
|
|
25472
26056
|
result.reactivated++;
|
|
25473
|
-
} else if (agent.type !== meta.type || agent.display_name !== meta.displayName || agent.delegate_mention !== meta.delegateMention) {
|
|
26057
|
+
} else if (agent.type !== meta.type || agent.display_name !== meta.displayName || agent.delegate_mention !== meta.delegateMention || agent.tree_path !== member.treePath) {
|
|
25474
26058
|
await db.execute(sql`
|
|
25475
|
-
UPDATE agents SET type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}
|
|
26059
|
+
UPDATE agents SET type = ${meta.type}, display_name = ${meta.displayName}, delegate_mention = ${meta.delegateMention}, tree_path = ${member.treePath}
|
|
25476
26060
|
WHERE id = ${member.name}
|
|
25477
26061
|
`);
|
|
25478
26062
|
result.updated++;
|
|
25479
26063
|
} else result.unchanged++;
|
|
25480
26064
|
}
|
|
25481
26065
|
} catch (err) {
|
|
25482
|
-
console.error(`[context-tree-sync] Failed to sync member "${member.name}":`, err);
|
|
26066
|
+
console.error(`[context-tree-sync] Failed to sync member "${member.name}" (path: members/${member.treePath}):`, err);
|
|
25483
26067
|
result.errors++;
|
|
25484
26068
|
}
|
|
25485
26069
|
try {
|
|
@@ -25583,6 +26167,7 @@ async function listAgents(db, limit, cursor) {
|
|
|
25583
26167
|
type: agents.type,
|
|
25584
26168
|
displayName: agents.displayName,
|
|
25585
26169
|
delegateMention: agents.delegateMention,
|
|
26170
|
+
treePath: agents.treePath,
|
|
25586
26171
|
inboxId: agents.inboxId,
|
|
25587
26172
|
status: agents.status,
|
|
25588
26173
|
metadata: agents.metadata,
|
|
@@ -25658,6 +26243,133 @@ async function revokeToken(db, agentId, tokenId) {
|
|
|
25658
26243
|
if (!token) throw new NotFoundError("Token not found or already revoked");
|
|
25659
26244
|
return token;
|
|
25660
26245
|
}
|
|
26246
|
+
async function createChat(db, creatorId, data) {
|
|
26247
|
+
const chatId = randomUUID();
|
|
26248
|
+
const allParticipantIds = new Set([creatorId, ...data.participantIds]);
|
|
26249
|
+
const existingAgents = await db.select({
|
|
26250
|
+
id: agents.id,
|
|
26251
|
+
organizationId: agents.organizationId
|
|
26252
|
+
}).from(agents).where(inArray(agents.id, [...allParticipantIds]));
|
|
26253
|
+
if (existingAgents.length !== allParticipantIds.size) {
|
|
26254
|
+
const found = new Set(existingAgents.map((a) => a.id));
|
|
26255
|
+
throw new BadRequestError(`Agents not found: ${[...allParticipantIds].filter((id) => !found.has(id)).join(", ")}`);
|
|
26256
|
+
}
|
|
26257
|
+
const creator = existingAgents.find((a) => a.id === creatorId);
|
|
26258
|
+
if (!creator) throw new Error("Unexpected: creator not in existingAgents");
|
|
26259
|
+
const orgId = creator.organizationId;
|
|
26260
|
+
const crossOrg = existingAgents.filter((a) => a.organizationId !== orgId);
|
|
26261
|
+
if (crossOrg.length > 0) throw new BadRequestError(`Cross-organization chat not allowed: ${crossOrg.map((a) => a.id).join(", ")}`);
|
|
26262
|
+
return db.transaction(async (tx) => {
|
|
26263
|
+
const [chat] = await tx.insert(chats).values({
|
|
26264
|
+
id: chatId,
|
|
26265
|
+
organizationId: orgId,
|
|
26266
|
+
type: data.type,
|
|
26267
|
+
topic: data.topic ?? null,
|
|
26268
|
+
metadata: data.metadata ?? {}
|
|
26269
|
+
}).returning();
|
|
26270
|
+
const participantRows = [...allParticipantIds].map((agentId) => ({
|
|
26271
|
+
chatId,
|
|
26272
|
+
agentId,
|
|
26273
|
+
role: agentId === creatorId ? "owner" : "member"
|
|
26274
|
+
}));
|
|
26275
|
+
await tx.insert(chatParticipants).values(participantRows);
|
|
26276
|
+
const participants = await tx.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
26277
|
+
if (!chat) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
26278
|
+
return {
|
|
26279
|
+
...chat,
|
|
26280
|
+
participants
|
|
26281
|
+
};
|
|
26282
|
+
});
|
|
26283
|
+
}
|
|
26284
|
+
async function getChat(db, chatId) {
|
|
26285
|
+
const [chat] = await db.select().from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
26286
|
+
if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
26287
|
+
return chat;
|
|
26288
|
+
}
|
|
26289
|
+
async function getChatDetail(db, chatId) {
|
|
26290
|
+
const chat = await getChat(db, chatId);
|
|
26291
|
+
const participants = await db.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
26292
|
+
return {
|
|
26293
|
+
...chat,
|
|
26294
|
+
participants
|
|
26295
|
+
};
|
|
26296
|
+
}
|
|
26297
|
+
async function listChats(db, agentId, limit, cursor) {
|
|
26298
|
+
const chatIds = (await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(eq(chatParticipants.agentId, agentId))).map((r) => r.chatId);
|
|
26299
|
+
if (chatIds.length === 0) return {
|
|
26300
|
+
items: [],
|
|
26301
|
+
nextCursor: null
|
|
26302
|
+
};
|
|
26303
|
+
const where = cursor ? and(inArray(chats.id, chatIds), lt(chats.updatedAt, new Date(cursor))) : inArray(chats.id, chatIds);
|
|
26304
|
+
const rows = await db.select().from(chats).where(where).orderBy(desc(chats.updatedAt)).limit(limit + 1);
|
|
26305
|
+
const hasMore = rows.length > limit;
|
|
26306
|
+
const items = hasMore ? rows.slice(0, limit) : rows;
|
|
26307
|
+
const last = items[items.length - 1];
|
|
26308
|
+
return {
|
|
26309
|
+
items,
|
|
26310
|
+
nextCursor: hasMore && last ? last.updatedAt.toISOString() : null
|
|
26311
|
+
};
|
|
26312
|
+
}
|
|
26313
|
+
async function assertParticipant(db, chatId, agentId) {
|
|
26314
|
+
const [row] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, agentId))).limit(1);
|
|
26315
|
+
if (!row) throw new ForbiddenError("Not a participant of this chat");
|
|
26316
|
+
}
|
|
26317
|
+
async function addParticipant(db, chatId, requesterId, data) {
|
|
26318
|
+
const chat = await getChat(db, chatId);
|
|
26319
|
+
await assertParticipant(db, chatId, requesterId);
|
|
26320
|
+
const [targetAgent] = await db.select({
|
|
26321
|
+
id: agents.id,
|
|
26322
|
+
organizationId: agents.organizationId
|
|
26323
|
+
}).from(agents).where(eq(agents.id, data.agentId)).limit(1);
|
|
26324
|
+
if (!targetAgent) throw new NotFoundError(`Agent "${data.agentId}" not found`);
|
|
26325
|
+
if (targetAgent.organizationId !== chat.organizationId) throw new BadRequestError("Cannot add agent from different organization");
|
|
26326
|
+
const [existing] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, data.agentId))).limit(1);
|
|
26327
|
+
if (existing) throw new ConflictError(`Agent "${data.agentId}" is already a participant`);
|
|
26328
|
+
await db.insert(chatParticipants).values({
|
|
26329
|
+
chatId,
|
|
26330
|
+
agentId: data.agentId,
|
|
26331
|
+
mode: data.mode ?? "full"
|
|
26332
|
+
});
|
|
26333
|
+
return db.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
26334
|
+
}
|
|
26335
|
+
async function removeParticipant(db, chatId, requesterId, targetAgentId) {
|
|
26336
|
+
await assertParticipant(db, chatId, requesterId);
|
|
26337
|
+
if (requesterId === targetAgentId) throw new BadRequestError("Cannot remove yourself from a chat");
|
|
26338
|
+
const [removed] = await db.delete(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, targetAgentId))).returning();
|
|
26339
|
+
if (!removed) throw new NotFoundError(`Agent "${targetAgentId}" is not a participant of this chat`);
|
|
26340
|
+
return db.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
26341
|
+
}
|
|
26342
|
+
async function findOrCreateDirectChat(db, agentAId, agentBId) {
|
|
26343
|
+
const aChats = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(eq(chatParticipants.agentId, agentAId));
|
|
26344
|
+
const bChats = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(eq(chatParticipants.agentId, agentBId));
|
|
26345
|
+
const bChatIds = new Set(bChats.map((r) => r.chatId));
|
|
26346
|
+
const commonChatIds = aChats.map((r) => r.chatId).filter((id) => bChatIds.has(id));
|
|
26347
|
+
if (commonChatIds.length > 0) {
|
|
26348
|
+
const directChats = await db.select().from(chats).where(and(inArray(chats.id, commonChatIds), eq(chats.type, "direct")));
|
|
26349
|
+
if (directChats.length > 0 && directChats[0]) return directChats[0];
|
|
26350
|
+
}
|
|
26351
|
+
const [agentA] = await db.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.id, agentAId)).limit(1);
|
|
26352
|
+
if (!agentA) throw new NotFoundError(`Agent "${agentAId}" not found`);
|
|
26353
|
+
const chatId = randomUUID();
|
|
26354
|
+
return db.transaction(async (tx) => {
|
|
26355
|
+
const [chat] = await tx.insert(chats).values({
|
|
26356
|
+
id: chatId,
|
|
26357
|
+
organizationId: agentA.organizationId,
|
|
26358
|
+
type: "direct"
|
|
26359
|
+
}).returning();
|
|
26360
|
+
await tx.insert(chatParticipants).values([{
|
|
26361
|
+
chatId,
|
|
26362
|
+
agentId: agentAId,
|
|
26363
|
+
role: "member"
|
|
26364
|
+
}, {
|
|
26365
|
+
chatId,
|
|
26366
|
+
agentId: agentBId,
|
|
26367
|
+
role: "member"
|
|
26368
|
+
}]);
|
|
26369
|
+
if (!chat) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
26370
|
+
return chat;
|
|
26371
|
+
});
|
|
26372
|
+
}
|
|
25661
26373
|
/** WS close code: agent already connected from another client. */
|
|
25662
26374
|
const WS_CLOSE_ALREADY_CONNECTED = 4009;
|
|
25663
26375
|
/** Track active WS connections per agentId. At most one entry per agent. */
|
|
@@ -25687,36 +26399,217 @@ function forceDisconnect(agentId) {
|
|
|
25687
26399
|
activeConnections.delete(agentId);
|
|
25688
26400
|
return true;
|
|
25689
26401
|
}
|
|
25690
|
-
/**
|
|
25691
|
-
const
|
|
25692
|
-
|
|
25693
|
-
|
|
25694
|
-
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
|
|
25699
|
-
|
|
25700
|
-
|
|
25701
|
-
|
|
25702
|
-
|
|
25703
|
-
|
|
25704
|
-
|
|
25705
|
-
|
|
25706
|
-
|
|
25707
|
-
|
|
25708
|
-
|
|
25709
|
-
|
|
26402
|
+
/** Delivery queue (envelope). One entry per recipient created during message fan-out. Uses SKIP LOCKED for concurrent-safe consumption. */
|
|
26403
|
+
const inboxEntries = pgTable("inbox_entries", {
|
|
26404
|
+
id: bigserial("id", { mode: "number" }).primaryKey(),
|
|
26405
|
+
inboxId: text("inbox_id").notNull(),
|
|
26406
|
+
messageId: text("message_id").notNull().references(() => messages.id),
|
|
26407
|
+
chatId: text("chat_id"),
|
|
26408
|
+
status: text("status").notNull().default("pending"),
|
|
26409
|
+
retryCount: integer("retry_count").notNull().default(0),
|
|
26410
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
26411
|
+
deliveredAt: timestamp("delivered_at", { withTimezone: true }),
|
|
26412
|
+
ackedAt: timestamp("acked_at", { withTimezone: true })
|
|
26413
|
+
}, (table) => [unique("uq_inbox_delivery").on(table.inboxId, table.messageId, table.chatId), index("idx_inbox_pending").on(table.inboxId, table.createdAt)]);
|
|
26414
|
+
async function sendMessage(db, chatId, senderId, data) {
|
|
26415
|
+
return db.transaction(async (tx) => {
|
|
26416
|
+
const messageId = randomUUID();
|
|
26417
|
+
const [msg] = await tx.insert(messages).values({
|
|
26418
|
+
id: messageId,
|
|
26419
|
+
chatId,
|
|
26420
|
+
senderId,
|
|
26421
|
+
format: data.format,
|
|
26422
|
+
content: data.content,
|
|
26423
|
+
metadata: data.metadata ?? {},
|
|
26424
|
+
replyToInbox: data.replyToInbox ?? null,
|
|
26425
|
+
replyToChat: data.replyToChat ?? null,
|
|
26426
|
+
inReplyTo: data.inReplyTo ?? null
|
|
26427
|
+
}).returning();
|
|
26428
|
+
const entries = (await tx.select({
|
|
26429
|
+
agentId: chatParticipants.agentId,
|
|
26430
|
+
inboxId: agents.inboxId
|
|
26431
|
+
}).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.id)).where(eq(chatParticipants.chatId, chatId))).filter((p) => p.agentId !== senderId).map((p) => ({
|
|
26432
|
+
inboxId: p.inboxId,
|
|
26433
|
+
messageId,
|
|
26434
|
+
chatId
|
|
26435
|
+
}));
|
|
26436
|
+
if (entries.length > 0) await tx.insert(inboxEntries).values(entries);
|
|
26437
|
+
const recipients = entries.map((e) => e.inboxId);
|
|
26438
|
+
if (data.inReplyTo) {
|
|
26439
|
+
const [original] = await tx.select({
|
|
26440
|
+
replyToInbox: messages.replyToInbox,
|
|
26441
|
+
replyToChat: messages.replyToChat
|
|
26442
|
+
}).from(messages).where(eq(messages.id, data.inReplyTo)).limit(1);
|
|
26443
|
+
if (original?.replyToInbox && original?.replyToChat) {
|
|
26444
|
+
await tx.insert(inboxEntries).values({
|
|
26445
|
+
inboxId: original.replyToInbox,
|
|
26446
|
+
messageId,
|
|
26447
|
+
chatId: original.replyToChat
|
|
26448
|
+
}).onConflictDoNothing();
|
|
26449
|
+
if (!recipients.includes(original.replyToInbox)) recipients.push(original.replyToInbox);
|
|
26450
|
+
}
|
|
25710
26451
|
}
|
|
25711
|
-
|
|
25712
|
-
|
|
25713
|
-
|
|
25714
|
-
|
|
26452
|
+
await tx.update(chats).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq(chats.id, chatId));
|
|
26453
|
+
if (!msg) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
26454
|
+
return {
|
|
26455
|
+
message: msg,
|
|
26456
|
+
recipients
|
|
26457
|
+
};
|
|
26458
|
+
});
|
|
26459
|
+
}
|
|
26460
|
+
async function sendToAgent(db, senderId, targetAgentId, data) {
|
|
26461
|
+
const [sender] = await db.select({
|
|
26462
|
+
id: agents.id,
|
|
26463
|
+
organizationId: agents.organizationId
|
|
26464
|
+
}).from(agents).where(eq(agents.id, senderId)).limit(1);
|
|
26465
|
+
if (!sender) throw new NotFoundError(`Agent "${senderId}" not found`);
|
|
26466
|
+
const [target] = await db.select({
|
|
26467
|
+
id: agents.id,
|
|
26468
|
+
organizationId: agents.organizationId
|
|
26469
|
+
}).from(agents).where(eq(agents.id, targetAgentId)).limit(1);
|
|
26470
|
+
if (!target) throw new NotFoundError(`Agent "${targetAgentId}" not found`);
|
|
26471
|
+
return sendMessage(db, (await findOrCreateDirectChat(db, senderId, targetAgentId)).id, senderId, {
|
|
26472
|
+
format: data.format,
|
|
26473
|
+
content: data.content,
|
|
26474
|
+
metadata: data.metadata,
|
|
26475
|
+
replyToInbox: data.replyToInbox,
|
|
26476
|
+
replyToChat: data.replyToChat
|
|
26477
|
+
});
|
|
26478
|
+
}
|
|
26479
|
+
async function editMessage(db, chatId, messageId, senderId, data) {
|
|
26480
|
+
const [msg] = await db.select().from(messages).where(eq(messages.id, messageId)).limit(1);
|
|
26481
|
+
if (!msg) throw new NotFoundError(`Message "${messageId}" not found`);
|
|
26482
|
+
if (msg.chatId !== chatId) throw new NotFoundError(`Message "${messageId}" not found in this chat`);
|
|
26483
|
+
if (msg.senderId !== senderId) throw new ForbiddenError("Only the sender can edit a message");
|
|
26484
|
+
const setClause = {};
|
|
26485
|
+
if (data.format !== void 0) setClause.format = data.format;
|
|
26486
|
+
if (data.content !== void 0) setClause.content = data.content;
|
|
26487
|
+
const meta = msg.metadata ?? {};
|
|
26488
|
+
meta.editedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
26489
|
+
setClause.metadata = meta;
|
|
26490
|
+
const [updated] = await db.update(messages).set(setClause).where(eq(messages.id, messageId)).returning();
|
|
26491
|
+
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
26492
|
+
return updated;
|
|
26493
|
+
}
|
|
26494
|
+
async function listMessages(db, chatId, limit, cursor) {
|
|
26495
|
+
const where = cursor ? and(eq(messages.chatId, chatId), lt(messages.createdAt, new Date(cursor))) : eq(messages.chatId, chatId);
|
|
26496
|
+
const rows = await db.select().from(messages).where(where).orderBy(desc(messages.createdAt)).limit(limit + 1);
|
|
26497
|
+
const hasMore = rows.length > limit;
|
|
26498
|
+
const items = hasMore ? rows.slice(0, limit) : rows;
|
|
26499
|
+
const last = items[items.length - 1];
|
|
26500
|
+
return {
|
|
26501
|
+
items,
|
|
26502
|
+
nextCursor: hasMore && last ? last.createdAt.toISOString() : null
|
|
26503
|
+
};
|
|
26504
|
+
}
|
|
26505
|
+
const INBOX_CHANNEL = "inbox_notifications";
|
|
26506
|
+
const CONFIG_CHANNEL = "config_changes";
|
|
26507
|
+
function createNotifier(listenClient) {
|
|
26508
|
+
const subscriptions = /* @__PURE__ */ new Map();
|
|
26509
|
+
const configChangeHandlers = [];
|
|
26510
|
+
let unlistenInboxFn = null;
|
|
26511
|
+
let unlistenConfigFn = null;
|
|
26512
|
+
function handleNotification(payload) {
|
|
26513
|
+
const sepIdx = payload.indexOf(":");
|
|
26514
|
+
if (sepIdx === -1) return;
|
|
26515
|
+
const inboxId = payload.slice(0, sepIdx);
|
|
26516
|
+
const messageId = payload.slice(sepIdx + 1);
|
|
26517
|
+
const sockets = subscriptions.get(inboxId);
|
|
26518
|
+
if (!sockets) return;
|
|
26519
|
+
const data = JSON.stringify({
|
|
26520
|
+
type: "new_message",
|
|
26521
|
+
inboxId,
|
|
26522
|
+
messageId
|
|
26523
|
+
});
|
|
26524
|
+
for (const ws of sockets) if (ws.readyState === ws.OPEN) ws.send(data);
|
|
26525
|
+
}
|
|
26526
|
+
return {
|
|
26527
|
+
subscribe(inboxId, ws) {
|
|
26528
|
+
let set = subscriptions.get(inboxId);
|
|
26529
|
+
if (!set) {
|
|
26530
|
+
set = /* @__PURE__ */ new Set();
|
|
26531
|
+
subscriptions.set(inboxId, set);
|
|
26532
|
+
}
|
|
26533
|
+
set.add(ws);
|
|
26534
|
+
},
|
|
26535
|
+
unsubscribe(inboxId, ws) {
|
|
26536
|
+
const set = subscriptions.get(inboxId);
|
|
26537
|
+
if (set) {
|
|
26538
|
+
set.delete(ws);
|
|
26539
|
+
if (set.size === 0) subscriptions.delete(inboxId);
|
|
26540
|
+
}
|
|
26541
|
+
},
|
|
26542
|
+
async notify(inboxId, messageId) {
|
|
26543
|
+
try {
|
|
26544
|
+
await listenClient`SELECT pg_notify(${INBOX_CHANNEL}, ${`${inboxId}:${messageId}`})`;
|
|
26545
|
+
} catch {}
|
|
26546
|
+
},
|
|
26547
|
+
async notifyConfigChange(configType) {
|
|
26548
|
+
try {
|
|
26549
|
+
await listenClient`SELECT pg_notify(${CONFIG_CHANNEL}, ${configType})`;
|
|
26550
|
+
} catch {}
|
|
26551
|
+
},
|
|
26552
|
+
onConfigChange(handler) {
|
|
26553
|
+
configChangeHandlers.push(handler);
|
|
26554
|
+
},
|
|
26555
|
+
async start() {
|
|
26556
|
+
unlistenInboxFn = (await listenClient.listen(INBOX_CHANNEL, (payload) => {
|
|
26557
|
+
if (payload) handleNotification(payload);
|
|
26558
|
+
})).unlisten;
|
|
26559
|
+
unlistenConfigFn = (await listenClient.listen(CONFIG_CHANNEL, (payload) => {
|
|
26560
|
+
if (payload) for (const handler of configChangeHandlers) handler(payload);
|
|
26561
|
+
})).unlisten;
|
|
26562
|
+
},
|
|
26563
|
+
async stop() {
|
|
26564
|
+
if (unlistenInboxFn) {
|
|
26565
|
+
await unlistenInboxFn();
|
|
26566
|
+
unlistenInboxFn = null;
|
|
26567
|
+
}
|
|
26568
|
+
if (unlistenConfigFn) {
|
|
26569
|
+
await unlistenConfigFn();
|
|
26570
|
+
unlistenConfigFn = null;
|
|
26571
|
+
}
|
|
26572
|
+
}
|
|
26573
|
+
};
|
|
26574
|
+
}
|
|
26575
|
+
/** Fire-and-forget: notify all recipients that a new message is available. */
|
|
26576
|
+
function notifyRecipients(notifier, recipients, messageId) {
|
|
26577
|
+
for (const inboxId of recipients) notifier.notify(inboxId, messageId).catch(() => {});
|
|
26578
|
+
}
|
|
26579
|
+
/** Server instance heartbeat. Used to detect crashed instances and clean up associated agent_presence records. */
|
|
26580
|
+
const serverInstances = pgTable("server_instances", {
|
|
26581
|
+
instanceId: text("instance_id").primaryKey(),
|
|
26582
|
+
lastHeartbeat: timestamp("last_heartbeat", { withTimezone: true }).notNull().defaultNow()
|
|
26583
|
+
});
|
|
26584
|
+
async function setOnline(db, agentId, instanceId) {
|
|
26585
|
+
const now = /* @__PURE__ */ new Date();
|
|
26586
|
+
await db.insert(agentPresence).values({
|
|
26587
|
+
agentId,
|
|
26588
|
+
status: "online",
|
|
26589
|
+
instanceId,
|
|
26590
|
+
connectedAt: now,
|
|
26591
|
+
lastSeenAt: now
|
|
26592
|
+
}).onConflictDoUpdate({
|
|
26593
|
+
target: agentPresence.agentId,
|
|
26594
|
+
set: {
|
|
26595
|
+
status: "online",
|
|
26596
|
+
instanceId,
|
|
26597
|
+
connectedAt: now,
|
|
26598
|
+
lastSeenAt: now
|
|
26599
|
+
}
|
|
26600
|
+
});
|
|
26601
|
+
}
|
|
26602
|
+
async function setOffline(db, agentId) {
|
|
26603
|
+
await db.update(agentPresence).set({
|
|
25715
26604
|
status: "offline",
|
|
25716
26605
|
instanceId: null,
|
|
25717
26606
|
lastSeenAt: /* @__PURE__ */ new Date()
|
|
25718
26607
|
}).where(eq(agentPresence.agentId, agentId));
|
|
25719
26608
|
}
|
|
26609
|
+
async function getPresence(db, agentId) {
|
|
26610
|
+
const [row] = await db.select().from(agentPresence).where(eq(agentPresence.agentId, agentId)).limit(1);
|
|
26611
|
+
return row ?? null;
|
|
26612
|
+
}
|
|
25720
26613
|
async function getOnlineCount(db) {
|
|
25721
26614
|
const [result] = await db.select({ count: sql`count(*)::int` }).from(agentPresence).where(eq(agentPresence.status, "online"));
|
|
25722
26615
|
return result?.count ?? 0;
|
|
@@ -25801,6 +26694,53 @@ async function adminAgentRoutes(app) {
|
|
|
25801
26694
|
await deleteAgent(app.db, request.params.agentId);
|
|
25802
26695
|
return reply.status(204).send();
|
|
25803
26696
|
});
|
|
26697
|
+
app.post("/:agentId/test", async (request, reply) => {
|
|
26698
|
+
const { agentId } = request.params;
|
|
26699
|
+
const [, presence] = await Promise.all([getAgent(app.db, agentId), getPresence(app.db, agentId)]);
|
|
26700
|
+
if (!presence || presence.status !== "online") return reply.status(200).send({
|
|
26701
|
+
status: "offline",
|
|
26702
|
+
message: "Agent is not connected. Start the client first."
|
|
26703
|
+
});
|
|
26704
|
+
const [owner] = await app.db.select({ id: agents.id }).from(agents).where(and(eq(agents.delegateMention, agentId), eq(agents.status, "active"))).limit(1);
|
|
26705
|
+
let senderId = owner?.id ?? null;
|
|
26706
|
+
if (!senderId) {
|
|
26707
|
+
const [other] = await app.db.select({ id: agents.id }).from(agents).where(and(ne(agents.id, agentId), eq(agents.status, "active"))).limit(1);
|
|
26708
|
+
senderId = other?.id ?? null;
|
|
26709
|
+
}
|
|
26710
|
+
if (!senderId) return reply.status(200).send({
|
|
26711
|
+
status: "error",
|
|
26712
|
+
message: "No suitable sender found. Need at least one other active agent."
|
|
26713
|
+
});
|
|
26714
|
+
const chat = await findOrCreateDirectChat(app.db, senderId, agentId);
|
|
26715
|
+
const testContent = `[System Test] Verify your connection. Respond with your identity and role. Time: ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
26716
|
+
const result = await sendMessage(app.db, chat.id, senderId, {
|
|
26717
|
+
format: "text",
|
|
26718
|
+
content: testContent
|
|
26719
|
+
});
|
|
26720
|
+
notifyRecipients(app.notifier, result.recipients, result.message.id);
|
|
26721
|
+
const POLL_TIMEOUT = 3e4;
|
|
26722
|
+
const POLL_INTERVAL = 1e3;
|
|
26723
|
+
const threshold = result.message.createdAt;
|
|
26724
|
+
const pollStart = Date.now();
|
|
26725
|
+
while (Date.now() - pollStart < POLL_TIMEOUT) {
|
|
26726
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
26727
|
+
const [response] = await app.db.select().from(messages).where(and(eq(messages.chatId, chat.id), eq(messages.senderId, agentId), gt(messages.createdAt, threshold))).limit(1);
|
|
26728
|
+
if (response) {
|
|
26729
|
+
const content = typeof response.content === "string" ? response.content.slice(0, 500) : JSON.stringify(response.content).slice(0, 500);
|
|
26730
|
+
return reply.status(200).send({
|
|
26731
|
+
status: "success",
|
|
26732
|
+
chatId: chat.id,
|
|
26733
|
+
responseContent: content,
|
|
26734
|
+
responseTime: response.createdAt.getTime() - threshold.getTime()
|
|
26735
|
+
});
|
|
26736
|
+
}
|
|
26737
|
+
}
|
|
26738
|
+
return reply.status(200).send({
|
|
26739
|
+
status: "timeout",
|
|
26740
|
+
chatId: chat.id,
|
|
26741
|
+
message: "Agent is connected but did not respond within 30 seconds."
|
|
26742
|
+
});
|
|
26743
|
+
});
|
|
25804
26744
|
}
|
|
25805
26745
|
/** Admin accounts. Passwords are stored as bcrypt hashes. */
|
|
25806
26746
|
const adminUsers = pgTable("admin_users", {
|
|
@@ -26039,8 +26979,10 @@ async function updateAdminUser(db, id, data) {
|
|
|
26039
26979
|
return toResponse(row);
|
|
26040
26980
|
}
|
|
26041
26981
|
async function deleteAdminUser(db, id) {
|
|
26042
|
-
const [
|
|
26043
|
-
if (!
|
|
26982
|
+
const [target] = await db.select().from(adminUsers).where(eq(adminUsers.id, id)).limit(1);
|
|
26983
|
+
if (!target) throw new NotFoundError(`Admin user "${id}" not found`);
|
|
26984
|
+
if (target.role === "super_admin") throw new ForbiddenError("Cannot delete a super_admin account");
|
|
26985
|
+
await db.delete(adminUsers).where(eq(adminUsers.id, id));
|
|
26044
26986
|
}
|
|
26045
26987
|
function serializeDate(d) {
|
|
26046
26988
|
return d ? d.toISOString() : null;
|
|
@@ -26087,133 +27029,6 @@ function requireAgent(request) {
|
|
|
26087
27029
|
if (!agent) throw new UnauthorizedError("Agent authentication required");
|
|
26088
27030
|
return agent;
|
|
26089
27031
|
}
|
|
26090
|
-
async function createChat(db, creatorId, data) {
|
|
26091
|
-
const chatId = randomUUID();
|
|
26092
|
-
const allParticipantIds = new Set([creatorId, ...data.participantIds]);
|
|
26093
|
-
const existingAgents = await db.select({
|
|
26094
|
-
id: agents.id,
|
|
26095
|
-
organizationId: agents.organizationId
|
|
26096
|
-
}).from(agents).where(inArray(agents.id, [...allParticipantIds]));
|
|
26097
|
-
if (existingAgents.length !== allParticipantIds.size) {
|
|
26098
|
-
const found = new Set(existingAgents.map((a) => a.id));
|
|
26099
|
-
throw new BadRequestError(`Agents not found: ${[...allParticipantIds].filter((id) => !found.has(id)).join(", ")}`);
|
|
26100
|
-
}
|
|
26101
|
-
const creator = existingAgents.find((a) => a.id === creatorId);
|
|
26102
|
-
if (!creator) throw new Error("Unexpected: creator not in existingAgents");
|
|
26103
|
-
const orgId = creator.organizationId;
|
|
26104
|
-
const crossOrg = existingAgents.filter((a) => a.organizationId !== orgId);
|
|
26105
|
-
if (crossOrg.length > 0) throw new BadRequestError(`Cross-organization chat not allowed: ${crossOrg.map((a) => a.id).join(", ")}`);
|
|
26106
|
-
return db.transaction(async (tx) => {
|
|
26107
|
-
const [chat] = await tx.insert(chats).values({
|
|
26108
|
-
id: chatId,
|
|
26109
|
-
organizationId: orgId,
|
|
26110
|
-
type: data.type,
|
|
26111
|
-
topic: data.topic ?? null,
|
|
26112
|
-
metadata: data.metadata ?? {}
|
|
26113
|
-
}).returning();
|
|
26114
|
-
const participantRows = [...allParticipantIds].map((agentId) => ({
|
|
26115
|
-
chatId,
|
|
26116
|
-
agentId,
|
|
26117
|
-
role: agentId === creatorId ? "owner" : "member"
|
|
26118
|
-
}));
|
|
26119
|
-
await tx.insert(chatParticipants).values(participantRows);
|
|
26120
|
-
const participants = await tx.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
26121
|
-
if (!chat) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
26122
|
-
return {
|
|
26123
|
-
...chat,
|
|
26124
|
-
participants
|
|
26125
|
-
};
|
|
26126
|
-
});
|
|
26127
|
-
}
|
|
26128
|
-
async function getChat(db, chatId) {
|
|
26129
|
-
const [chat] = await db.select().from(chats).where(eq(chats.id, chatId)).limit(1);
|
|
26130
|
-
if (!chat) throw new NotFoundError(`Chat "${chatId}" not found`);
|
|
26131
|
-
return chat;
|
|
26132
|
-
}
|
|
26133
|
-
async function getChatDetail(db, chatId) {
|
|
26134
|
-
const chat = await getChat(db, chatId);
|
|
26135
|
-
const participants = await db.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
26136
|
-
return {
|
|
26137
|
-
...chat,
|
|
26138
|
-
participants
|
|
26139
|
-
};
|
|
26140
|
-
}
|
|
26141
|
-
async function listChats(db, agentId, limit, cursor) {
|
|
26142
|
-
const chatIds = (await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(eq(chatParticipants.agentId, agentId))).map((r) => r.chatId);
|
|
26143
|
-
if (chatIds.length === 0) return {
|
|
26144
|
-
items: [],
|
|
26145
|
-
nextCursor: null
|
|
26146
|
-
};
|
|
26147
|
-
const where = cursor ? and(inArray(chats.id, chatIds), lt(chats.updatedAt, new Date(cursor))) : inArray(chats.id, chatIds);
|
|
26148
|
-
const rows = await db.select().from(chats).where(where).orderBy(desc(chats.updatedAt)).limit(limit + 1);
|
|
26149
|
-
const hasMore = rows.length > limit;
|
|
26150
|
-
const items = hasMore ? rows.slice(0, limit) : rows;
|
|
26151
|
-
const last = items[items.length - 1];
|
|
26152
|
-
return {
|
|
26153
|
-
items,
|
|
26154
|
-
nextCursor: hasMore && last ? last.updatedAt.toISOString() : null
|
|
26155
|
-
};
|
|
26156
|
-
}
|
|
26157
|
-
async function assertParticipant(db, chatId, agentId) {
|
|
26158
|
-
const [row] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, agentId))).limit(1);
|
|
26159
|
-
if (!row) throw new ForbiddenError("Not a participant of this chat");
|
|
26160
|
-
}
|
|
26161
|
-
async function addParticipant(db, chatId, requesterId, data) {
|
|
26162
|
-
const chat = await getChat(db, chatId);
|
|
26163
|
-
await assertParticipant(db, chatId, requesterId);
|
|
26164
|
-
const [targetAgent] = await db.select({
|
|
26165
|
-
id: agents.id,
|
|
26166
|
-
organizationId: agents.organizationId
|
|
26167
|
-
}).from(agents).where(eq(agents.id, data.agentId)).limit(1);
|
|
26168
|
-
if (!targetAgent) throw new NotFoundError(`Agent "${data.agentId}" not found`);
|
|
26169
|
-
if (targetAgent.organizationId !== chat.organizationId) throw new BadRequestError("Cannot add agent from different organization");
|
|
26170
|
-
const [existing] = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, data.agentId))).limit(1);
|
|
26171
|
-
if (existing) throw new ConflictError(`Agent "${data.agentId}" is already a participant`);
|
|
26172
|
-
await db.insert(chatParticipants).values({
|
|
26173
|
-
chatId,
|
|
26174
|
-
agentId: data.agentId,
|
|
26175
|
-
mode: data.mode ?? "full"
|
|
26176
|
-
});
|
|
26177
|
-
return db.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
26178
|
-
}
|
|
26179
|
-
async function removeParticipant(db, chatId, requesterId, targetAgentId) {
|
|
26180
|
-
await assertParticipant(db, chatId, requesterId);
|
|
26181
|
-
if (requesterId === targetAgentId) throw new BadRequestError("Cannot remove yourself from a chat");
|
|
26182
|
-
const [removed] = await db.delete(chatParticipants).where(and(eq(chatParticipants.chatId, chatId), eq(chatParticipants.agentId, targetAgentId))).returning();
|
|
26183
|
-
if (!removed) throw new NotFoundError(`Agent "${targetAgentId}" is not a participant of this chat`);
|
|
26184
|
-
return db.select().from(chatParticipants).where(eq(chatParticipants.chatId, chatId));
|
|
26185
|
-
}
|
|
26186
|
-
async function findOrCreateDirectChat(db, agentAId, agentBId) {
|
|
26187
|
-
const aChats = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(eq(chatParticipants.agentId, agentAId));
|
|
26188
|
-
const bChats = await db.select({ chatId: chatParticipants.chatId }).from(chatParticipants).where(eq(chatParticipants.agentId, agentBId));
|
|
26189
|
-
const bChatIds = new Set(bChats.map((r) => r.chatId));
|
|
26190
|
-
const commonChatIds = aChats.map((r) => r.chatId).filter((id) => bChatIds.has(id));
|
|
26191
|
-
if (commonChatIds.length > 0) {
|
|
26192
|
-
const directChats = await db.select().from(chats).where(and(inArray(chats.id, commonChatIds), eq(chats.type, "direct")));
|
|
26193
|
-
if (directChats.length > 0 && directChats[0]) return directChats[0];
|
|
26194
|
-
}
|
|
26195
|
-
const [agentA] = await db.select({ organizationId: agents.organizationId }).from(agents).where(eq(agents.id, agentAId)).limit(1);
|
|
26196
|
-
if (!agentA) throw new NotFoundError(`Agent "${agentAId}" not found`);
|
|
26197
|
-
const chatId = randomUUID();
|
|
26198
|
-
return db.transaction(async (tx) => {
|
|
26199
|
-
const [chat] = await tx.insert(chats).values({
|
|
26200
|
-
id: chatId,
|
|
26201
|
-
organizationId: agentA.organizationId,
|
|
26202
|
-
type: "direct"
|
|
26203
|
-
}).returning();
|
|
26204
|
-
await tx.insert(chatParticipants).values([{
|
|
26205
|
-
chatId,
|
|
26206
|
-
agentId: agentAId,
|
|
26207
|
-
role: "member"
|
|
26208
|
-
}, {
|
|
26209
|
-
chatId,
|
|
26210
|
-
agentId: agentBId,
|
|
26211
|
-
role: "member"
|
|
26212
|
-
}]);
|
|
26213
|
-
if (!chat) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
26214
|
-
return chat;
|
|
26215
|
-
});
|
|
26216
|
-
}
|
|
26217
27032
|
function serializeChat(chat) {
|
|
26218
27033
|
return {
|
|
26219
27034
|
...chat,
|
|
@@ -26270,18 +27085,16 @@ async function agentChatRoutes(app) {
|
|
|
26270
27085
|
return reply.status(204).send();
|
|
26271
27086
|
});
|
|
26272
27087
|
}
|
|
26273
|
-
|
|
26274
|
-
|
|
26275
|
-
|
|
26276
|
-
|
|
26277
|
-
|
|
26278
|
-
|
|
26279
|
-
|
|
26280
|
-
|
|
26281
|
-
|
|
26282
|
-
|
|
26283
|
-
ackedAt: timestamp("acked_at", { withTimezone: true })
|
|
26284
|
-
}, (table) => [unique("uq_inbox_delivery").on(table.inboxId, table.messageId, table.chatId), index("idx_inbox_pending").on(table.inboxId, table.createdAt)]);
|
|
27088
|
+
async function agentContextTreeRoutes(app) {
|
|
27089
|
+
app.get("/", async (_request, reply) => {
|
|
27090
|
+
const { repo, branch } = app.config.contextTree;
|
|
27091
|
+
if (!repo) return reply.status(404).send({ error: "Context Tree not configured" });
|
|
27092
|
+
return reply.send({
|
|
27093
|
+
repo,
|
|
27094
|
+
branch
|
|
27095
|
+
});
|
|
27096
|
+
});
|
|
27097
|
+
}
|
|
26285
27098
|
const DEFAULT_INBOX_TIMEOUT_SECONDS = 300;
|
|
26286
27099
|
const DEFAULT_MAX_RETRY_COUNT = 3;
|
|
26287
27100
|
async function pollInbox(db, inboxId, limit) {
|
|
@@ -26394,171 +27207,6 @@ async function agentMeRoutes(app) {
|
|
|
26394
27207
|
};
|
|
26395
27208
|
});
|
|
26396
27209
|
}
|
|
26397
|
-
async function sendMessage(db, chatId, senderId, data) {
|
|
26398
|
-
return db.transaction(async (tx) => {
|
|
26399
|
-
const messageId = randomUUID();
|
|
26400
|
-
const [msg] = await tx.insert(messages).values({
|
|
26401
|
-
id: messageId,
|
|
26402
|
-
chatId,
|
|
26403
|
-
senderId,
|
|
26404
|
-
format: data.format,
|
|
26405
|
-
content: data.content,
|
|
26406
|
-
metadata: data.metadata ?? {},
|
|
26407
|
-
replyToInbox: data.replyToInbox ?? null,
|
|
26408
|
-
replyToChat: data.replyToChat ?? null,
|
|
26409
|
-
inReplyTo: data.inReplyTo ?? null
|
|
26410
|
-
}).returning();
|
|
26411
|
-
const entries = (await tx.select({
|
|
26412
|
-
agentId: chatParticipants.agentId,
|
|
26413
|
-
inboxId: agents.inboxId
|
|
26414
|
-
}).from(chatParticipants).innerJoin(agents, eq(chatParticipants.agentId, agents.id)).where(eq(chatParticipants.chatId, chatId))).filter((p) => p.agentId !== senderId).map((p) => ({
|
|
26415
|
-
inboxId: p.inboxId,
|
|
26416
|
-
messageId,
|
|
26417
|
-
chatId
|
|
26418
|
-
}));
|
|
26419
|
-
if (entries.length > 0) await tx.insert(inboxEntries).values(entries);
|
|
26420
|
-
const recipients = entries.map((e) => e.inboxId);
|
|
26421
|
-
if (data.inReplyTo) {
|
|
26422
|
-
const [original] = await tx.select({
|
|
26423
|
-
replyToInbox: messages.replyToInbox,
|
|
26424
|
-
replyToChat: messages.replyToChat
|
|
26425
|
-
}).from(messages).where(eq(messages.id, data.inReplyTo)).limit(1);
|
|
26426
|
-
if (original?.replyToInbox && original?.replyToChat) {
|
|
26427
|
-
await tx.insert(inboxEntries).values({
|
|
26428
|
-
inboxId: original.replyToInbox,
|
|
26429
|
-
messageId,
|
|
26430
|
-
chatId: original.replyToChat
|
|
26431
|
-
}).onConflictDoNothing();
|
|
26432
|
-
if (!recipients.includes(original.replyToInbox)) recipients.push(original.replyToInbox);
|
|
26433
|
-
}
|
|
26434
|
-
}
|
|
26435
|
-
await tx.update(chats).set({ updatedAt: /* @__PURE__ */ new Date() }).where(eq(chats.id, chatId));
|
|
26436
|
-
if (!msg) throw new Error("Unexpected: INSERT RETURNING produced no row");
|
|
26437
|
-
return {
|
|
26438
|
-
message: msg,
|
|
26439
|
-
recipients
|
|
26440
|
-
};
|
|
26441
|
-
});
|
|
26442
|
-
}
|
|
26443
|
-
async function sendToAgent(db, senderId, targetAgentId, data) {
|
|
26444
|
-
const [sender] = await db.select({
|
|
26445
|
-
id: agents.id,
|
|
26446
|
-
organizationId: agents.organizationId
|
|
26447
|
-
}).from(agents).where(eq(agents.id, senderId)).limit(1);
|
|
26448
|
-
if (!sender) throw new NotFoundError(`Agent "${senderId}" not found`);
|
|
26449
|
-
const [target] = await db.select({
|
|
26450
|
-
id: agents.id,
|
|
26451
|
-
organizationId: agents.organizationId
|
|
26452
|
-
}).from(agents).where(eq(agents.id, targetAgentId)).limit(1);
|
|
26453
|
-
if (!target) throw new NotFoundError(`Agent "${targetAgentId}" not found`);
|
|
26454
|
-
return sendMessage(db, (await findOrCreateDirectChat(db, senderId, targetAgentId)).id, senderId, {
|
|
26455
|
-
format: data.format,
|
|
26456
|
-
content: data.content,
|
|
26457
|
-
metadata: data.metadata,
|
|
26458
|
-
replyToInbox: data.replyToInbox,
|
|
26459
|
-
replyToChat: data.replyToChat
|
|
26460
|
-
});
|
|
26461
|
-
}
|
|
26462
|
-
async function editMessage(db, chatId, messageId, senderId, data) {
|
|
26463
|
-
const [msg] = await db.select().from(messages).where(eq(messages.id, messageId)).limit(1);
|
|
26464
|
-
if (!msg) throw new NotFoundError(`Message "${messageId}" not found`);
|
|
26465
|
-
if (msg.chatId !== chatId) throw new NotFoundError(`Message "${messageId}" not found in this chat`);
|
|
26466
|
-
if (msg.senderId !== senderId) throw new ForbiddenError("Only the sender can edit a message");
|
|
26467
|
-
const setClause = {};
|
|
26468
|
-
if (data.format !== void 0) setClause.format = data.format;
|
|
26469
|
-
if (data.content !== void 0) setClause.content = data.content;
|
|
26470
|
-
const meta = msg.metadata ?? {};
|
|
26471
|
-
meta.editedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
26472
|
-
setClause.metadata = meta;
|
|
26473
|
-
const [updated] = await db.update(messages).set(setClause).where(eq(messages.id, messageId)).returning();
|
|
26474
|
-
if (!updated) throw new Error("Unexpected: UPDATE RETURNING produced no row");
|
|
26475
|
-
return updated;
|
|
26476
|
-
}
|
|
26477
|
-
async function listMessages(db, chatId, limit, cursor) {
|
|
26478
|
-
const where = cursor ? and(eq(messages.chatId, chatId), lt(messages.createdAt, new Date(cursor))) : eq(messages.chatId, chatId);
|
|
26479
|
-
const rows = await db.select().from(messages).where(where).orderBy(desc(messages.createdAt)).limit(limit + 1);
|
|
26480
|
-
const hasMore = rows.length > limit;
|
|
26481
|
-
const items = hasMore ? rows.slice(0, limit) : rows;
|
|
26482
|
-
const last = items[items.length - 1];
|
|
26483
|
-
return {
|
|
26484
|
-
items,
|
|
26485
|
-
nextCursor: hasMore && last ? last.createdAt.toISOString() : null
|
|
26486
|
-
};
|
|
26487
|
-
}
|
|
26488
|
-
const INBOX_CHANNEL = "inbox_notifications";
|
|
26489
|
-
const CONFIG_CHANNEL = "config_changes";
|
|
26490
|
-
function createNotifier(listenClient) {
|
|
26491
|
-
const subscriptions = /* @__PURE__ */ new Map();
|
|
26492
|
-
const configChangeHandlers = [];
|
|
26493
|
-
let unlistenInboxFn = null;
|
|
26494
|
-
let unlistenConfigFn = null;
|
|
26495
|
-
function handleNotification(payload) {
|
|
26496
|
-
const sepIdx = payload.indexOf(":");
|
|
26497
|
-
if (sepIdx === -1) return;
|
|
26498
|
-
const inboxId = payload.slice(0, sepIdx);
|
|
26499
|
-
const messageId = payload.slice(sepIdx + 1);
|
|
26500
|
-
const sockets = subscriptions.get(inboxId);
|
|
26501
|
-
if (!sockets) return;
|
|
26502
|
-
const data = JSON.stringify({
|
|
26503
|
-
type: "new_message",
|
|
26504
|
-
inboxId,
|
|
26505
|
-
messageId
|
|
26506
|
-
});
|
|
26507
|
-
for (const ws of sockets) if (ws.readyState === ws.OPEN) ws.send(data);
|
|
26508
|
-
}
|
|
26509
|
-
return {
|
|
26510
|
-
subscribe(inboxId, ws) {
|
|
26511
|
-
let set = subscriptions.get(inboxId);
|
|
26512
|
-
if (!set) {
|
|
26513
|
-
set = /* @__PURE__ */ new Set();
|
|
26514
|
-
subscriptions.set(inboxId, set);
|
|
26515
|
-
}
|
|
26516
|
-
set.add(ws);
|
|
26517
|
-
},
|
|
26518
|
-
unsubscribe(inboxId, ws) {
|
|
26519
|
-
const set = subscriptions.get(inboxId);
|
|
26520
|
-
if (set) {
|
|
26521
|
-
set.delete(ws);
|
|
26522
|
-
if (set.size === 0) subscriptions.delete(inboxId);
|
|
26523
|
-
}
|
|
26524
|
-
},
|
|
26525
|
-
async notify(inboxId, messageId) {
|
|
26526
|
-
try {
|
|
26527
|
-
await listenClient`SELECT pg_notify(${INBOX_CHANNEL}, ${`${inboxId}:${messageId}`})`;
|
|
26528
|
-
} catch {}
|
|
26529
|
-
},
|
|
26530
|
-
async notifyConfigChange(configType) {
|
|
26531
|
-
try {
|
|
26532
|
-
await listenClient`SELECT pg_notify(${CONFIG_CHANNEL}, ${configType})`;
|
|
26533
|
-
} catch {}
|
|
26534
|
-
},
|
|
26535
|
-
onConfigChange(handler) {
|
|
26536
|
-
configChangeHandlers.push(handler);
|
|
26537
|
-
},
|
|
26538
|
-
async start() {
|
|
26539
|
-
unlistenInboxFn = (await listenClient.listen(INBOX_CHANNEL, (payload) => {
|
|
26540
|
-
if (payload) handleNotification(payload);
|
|
26541
|
-
})).unlisten;
|
|
26542
|
-
unlistenConfigFn = (await listenClient.listen(CONFIG_CHANNEL, (payload) => {
|
|
26543
|
-
if (payload) for (const handler of configChangeHandlers) handler(payload);
|
|
26544
|
-
})).unlisten;
|
|
26545
|
-
},
|
|
26546
|
-
async stop() {
|
|
26547
|
-
if (unlistenInboxFn) {
|
|
26548
|
-
await unlistenInboxFn();
|
|
26549
|
-
unlistenInboxFn = null;
|
|
26550
|
-
}
|
|
26551
|
-
if (unlistenConfigFn) {
|
|
26552
|
-
await unlistenConfigFn();
|
|
26553
|
-
unlistenConfigFn = null;
|
|
26554
|
-
}
|
|
26555
|
-
}
|
|
26556
|
-
};
|
|
26557
|
-
}
|
|
26558
|
-
/** Fire-and-forget: notify all recipients that a new message is available. */
|
|
26559
|
-
function notifyRecipients(notifier, recipients, messageId) {
|
|
26560
|
-
for (const inboxId of recipients) notifier.notify(inboxId, messageId).catch(() => {});
|
|
26561
|
-
}
|
|
26562
27210
|
const editMessageSchema = z.object({
|
|
26563
27211
|
format: z.string().optional(),
|
|
26564
27212
|
content: z.unknown()
|
|
@@ -27752,6 +28400,7 @@ async function buildApp(config) {
|
|
|
27752
28400
|
await agentApp.register(agentMessageRoutes, { prefix: "/chats" });
|
|
27753
28401
|
await agentApp.register(agentSendToAgentRoutes, { prefix: "/agents" });
|
|
27754
28402
|
await agentApp.register(agentInboxRoutes, { prefix: "/inbox" });
|
|
28403
|
+
await agentApp.register(agentContextTreeRoutes, { prefix: "/context-tree" });
|
|
27755
28404
|
await agentApp.register(agentWsRoutes(notifier, config.instanceId), { prefix: "/ws" });
|
|
27756
28405
|
}, { prefix: "/agent" });
|
|
27757
28406
|
}, { prefix: "/api/v1" });
|
|
@@ -27911,4 +28560,4 @@ function resolveWebDist() {
|
|
|
27911
28560
|
} catch {}
|
|
27912
28561
|
}
|
|
27913
28562
|
//#endregion
|
|
27914
|
-
export {
|
|
28563
|
+
export { SessionRegistry as A, clientConfigSchema as B, stopPostgres as C, DEFAULT_WORKSPACE_TTL_MS as D, AgentSlot as E, createAdminUser$1 as F, resetConfig as G, initConfig as H, hasAdminUser as I, setConfigValue as J, resetConfigMeta as K, DEFAULT_CONFIG_DIR as L, getHandlerFactory as M, loadRuntimeConfig as N, FirstTreeHubSDK as O, registerBuiltinHandlers as P, DEFAULT_DATA_DIR$1 as R, isDockerAvailable as S, AgentRuntime as T, loadAgents as U, getConfigValue as V, readConfigFile as W, checkWebSocket as _, runMigrations as a, status as b, checkClientConfig as c, checkDocker as d, checkGitHubToken as f, checkServerReachable as g, checkServerHealth as h, promptMissingFields as i, cleanWorkspaces as j, SdkError as k, checkContextTreeRepo as l, checkServerConfig as m, isInteractive as n, checkAgentConfigs as o, checkNodeVersion as p, serverConfigSchema as q, promptAddAgent as r, checkAgentTokens as s, startServer as t, checkDatabase as u, printResults as v, ClientRuntime as w, ensurePostgres as x, blank as y, agentConfigSchema as z };
|