granclaw 0.0.1-beta.0 → 0.0.1-beta.3
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.
|
Binary file
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.main = main;
|
|
|
8
8
|
/* eslint-disable no-console */
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
11
12
|
const path_1 = __importDefault(require("path"));
|
|
12
13
|
const home_js_1 = require("./home.js");
|
|
13
14
|
// package.json is resolved at runtime; require() avoids rootDir complaints
|
|
@@ -15,6 +16,54 @@ const home_js_1 = require("./home.js");
|
|
|
15
16
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
16
17
|
const pkg = require('../package.json');
|
|
17
18
|
const DEFAULT_PORT = 8787;
|
|
19
|
+
// ANSI colour helpers — no external deps
|
|
20
|
+
const c = {
|
|
21
|
+
reset: '\x1b[0m',
|
|
22
|
+
bold: '\x1b[1m',
|
|
23
|
+
dim: '\x1b[2m',
|
|
24
|
+
cyan: '\x1b[36m',
|
|
25
|
+
magenta: '\x1b[35m',
|
|
26
|
+
green: '\x1b[32m',
|
|
27
|
+
yellow: '\x1b[33m',
|
|
28
|
+
white: '\x1b[97m',
|
|
29
|
+
gray: '\x1b[90m',
|
|
30
|
+
};
|
|
31
|
+
function col(code, text) {
|
|
32
|
+
return `${code}${text}${c.reset}`;
|
|
33
|
+
}
|
|
34
|
+
function printBanner(version, port, homeDir) {
|
|
35
|
+
const art = [
|
|
36
|
+
' ____ ____ _ ',
|
|
37
|
+
' / ___|_ __ __ _ _ __ / ___| | __ ___ __',
|
|
38
|
+
" | | _| '__/ _` | '_ \\| | | |/ _` \\ \\ /\\ / /",
|
|
39
|
+
' | |_| | | | (_| | | | | |___| | (_| |\\ V V / ',
|
|
40
|
+
' \\____|_| \\__,_|_| |_|\\____|_|\\__,_| \\_/\\_/ ',
|
|
41
|
+
];
|
|
42
|
+
console.log('');
|
|
43
|
+
for (const line of art) {
|
|
44
|
+
console.log(col(c.magenta + c.bold, line));
|
|
45
|
+
}
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log(col(c.gray, ' Multi-agent AI framework') +
|
|
48
|
+
col(c.gray, ' · ') +
|
|
49
|
+
col(c.dim, `v${version}`));
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(col(c.cyan, ' Dashboard ') + col(c.white + c.bold, `http://localhost:${port}`));
|
|
52
|
+
console.log(col(c.cyan, ' Home ') + col(c.white, homeDir));
|
|
53
|
+
console.log(col(c.cyan, ' Workspaces ') + col(c.white, path_1.default.join(homeDir, 'workspaces')));
|
|
54
|
+
console.log(col(c.cyan, ' Config ') + col(c.white, path_1.default.join(homeDir, 'agents.config.json')));
|
|
55
|
+
console.log(col(c.cyan, ' Logs ') + col(c.white, path_1.default.join(homeDir, 'data')));
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(col(c.green, ' Opening browser…'));
|
|
58
|
+
console.log('');
|
|
59
|
+
}
|
|
60
|
+
function openBrowser(url) {
|
|
61
|
+
const platform = os_1.default.platform();
|
|
62
|
+
const cmd = platform === 'darwin' ? `open "${url}"` :
|
|
63
|
+
platform === 'win32' ? `start "" "${url}"` :
|
|
64
|
+
`xdg-open "${url}"`;
|
|
65
|
+
(0, child_process_1.exec)(cmd, () => { });
|
|
66
|
+
}
|
|
18
67
|
/**
|
|
19
68
|
* Parse the CLI argv slice (sans `node` and the script path).
|
|
20
69
|
*
|
|
@@ -94,6 +143,91 @@ Install from https://claude.ai/download, then rerun.
|
|
|
94
143
|
process.exit(1);
|
|
95
144
|
}
|
|
96
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Return the path to a real system Chrome/Chromium install, or null if none
|
|
148
|
+
* is found. Deliberately excludes ~/.agent-browser/browsers/ (Chrome for
|
|
149
|
+
* Testing) — GranClaw requires a real browser installed on the system.
|
|
150
|
+
*
|
|
151
|
+
* Respects AGENT_BROWSER_EXECUTABLE_PATH for users who want a custom binary.
|
|
152
|
+
*/
|
|
153
|
+
function detectSystemChrome() {
|
|
154
|
+
const override = process.env.AGENT_BROWSER_EXECUTABLE_PATH;
|
|
155
|
+
if (override)
|
|
156
|
+
return fs_1.default.existsSync(override) ? override : null;
|
|
157
|
+
const platform = os_1.default.platform();
|
|
158
|
+
if (platform === 'darwin') {
|
|
159
|
+
const candidates = [
|
|
160
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
161
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
162
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
163
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
164
|
+
path_1.default.join(os_1.default.homedir(), 'Applications/Google Chrome.app/Contents/MacOS/Google Chrome'),
|
|
165
|
+
];
|
|
166
|
+
return candidates.find((p) => fs_1.default.existsSync(p)) ?? null;
|
|
167
|
+
}
|
|
168
|
+
if (platform === 'linux') {
|
|
169
|
+
const candidates = [
|
|
170
|
+
'/usr/bin/google-chrome',
|
|
171
|
+
'/usr/bin/google-chrome-stable',
|
|
172
|
+
'/usr/bin/chromium',
|
|
173
|
+
'/usr/bin/chromium-browser',
|
|
174
|
+
'/snap/bin/chromium',
|
|
175
|
+
'/usr/bin/brave-browser',
|
|
176
|
+
'/usr/bin/microsoft-edge',
|
|
177
|
+
];
|
|
178
|
+
return candidates.find((p) => fs_1.default.existsSync(p)) ?? null;
|
|
179
|
+
}
|
|
180
|
+
if (platform === 'win32') {
|
|
181
|
+
const local = process.env.LOCALAPPDATA ?? '';
|
|
182
|
+
const pf = process.env.ProgramFiles ?? 'C:\\Program Files';
|
|
183
|
+
const pf86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)';
|
|
184
|
+
const candidates = [
|
|
185
|
+
path_1.default.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
186
|
+
path_1.default.join(pf, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
187
|
+
path_1.default.join(pf86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
|
|
188
|
+
path_1.default.join(local, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
|
|
189
|
+
path_1.default.join(pf, 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
|
|
190
|
+
path_1.default.join(local, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
191
|
+
path_1.default.join(pf, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
|
|
192
|
+
];
|
|
193
|
+
return candidates.find((p) => fs_1.default.existsSync(p)) ?? null;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
function requireAgentBrowser() {
|
|
198
|
+
try {
|
|
199
|
+
(0, child_process_1.execSync)('agent-browser --version', { stdio: 'ignore' });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
console.error(`
|
|
203
|
+
error: agent-browser not found.
|
|
204
|
+
|
|
205
|
+
GranClaw requires \`agent-browser\` for browser automation.
|
|
206
|
+
|
|
207
|
+
npm install -g agent-browser
|
|
208
|
+
`);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
const chrome = detectSystemChrome();
|
|
212
|
+
if (!chrome) {
|
|
213
|
+
const platform = os_1.default.platform();
|
|
214
|
+
const installLink = 'https://google.com/chrome';
|
|
215
|
+
const linuxExtra = platform === 'linux'
|
|
216
|
+
? '\n sudo apt install google-chrome-stable # Debian/Ubuntu'
|
|
217
|
+
: '';
|
|
218
|
+
console.error(`
|
|
219
|
+
error: Chrome not found.
|
|
220
|
+
|
|
221
|
+
GranClaw requires Google Chrome (or Chromium / Brave / Edge) installed on
|
|
222
|
+
your system. Install it from:
|
|
223
|
+
|
|
224
|
+
${installLink}${linuxExtra}
|
|
225
|
+
|
|
226
|
+
To use a custom binary, set AGENT_BROWSER_EXECUTABLE_PATH before running.
|
|
227
|
+
`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
97
231
|
function cliPackageDir() {
|
|
98
232
|
// dist/index.js runs at <cli-pkg>/dist/, so the package root is one up.
|
|
99
233
|
return path_1.default.resolve(__dirname, '..');
|
|
@@ -103,6 +237,7 @@ function startServer(parsed) {
|
|
|
103
237
|
const templatesDir = path_1.default.join(cliPackageDir(), 'templates');
|
|
104
238
|
const staticDir = path_1.default.join(cliPackageDir(), 'dist', 'frontend');
|
|
105
239
|
requireClaudeCli();
|
|
240
|
+
requireAgentBrowser();
|
|
106
241
|
(0, home_js_1.seedHomeIfNeeded)(homeDir, templatesDir);
|
|
107
242
|
const port = parsed.port ?? (Number(process.env.PORT) || DEFAULT_PORT);
|
|
108
243
|
const env = {
|
|
@@ -112,11 +247,10 @@ function startServer(parsed) {
|
|
|
112
247
|
GRANCLAW_STATIC_DIR: staticDir,
|
|
113
248
|
PORT: String(port),
|
|
114
249
|
};
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
console.log('');
|
|
250
|
+
printBanner(pkg.version, port, homeDir);
|
|
251
|
+
// Open the browser shortly after the server process starts.
|
|
252
|
+
// 1.5 s is enough for the backend to bind its port on most machines.
|
|
253
|
+
setTimeout(() => openBrowser(`http://localhost:${port}`), 1500);
|
|
120
254
|
// Spawn the compiled backend entrypoint. Bundled at dist/backend/index.js
|
|
121
255
|
// by the CLI build script (see scripts/build.js).
|
|
122
256
|
const backendEntry = path_1.default.join(cliPackageDir(), 'dist', 'backend', 'index.js');
|
package/package.json
CHANGED
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* agent/runner.ts
|
|
4
|
-
*
|
|
5
|
-
* Spawns the Claude Code CLI as a child process for a given agent.
|
|
6
|
-
* Streams output back via a callback. Persists session IDs so conversations
|
|
7
|
-
* continue across messages.
|
|
8
|
-
*
|
|
9
|
-
* Onboarding: if CLAUDE.md is missing from the workspace, the runner copies
|
|
10
|
-
* templates/CLAUDE.onboarding.md there. Claude reads it, decides to onboard
|
|
11
|
-
* (or not), and replaces the file itself when done. The host never checks
|
|
12
|
-
* onboarding state — Claude controls it entirely.
|
|
13
|
-
*
|
|
14
|
-
* Claude CLI invocation:
|
|
15
|
-
* claude -p "<message>" --output-format stream-json --verbose [--resume <sessionId>]
|
|
16
|
-
*/
|
|
17
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
18
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
|
-
};
|
|
20
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.spawnEnv = exports.claudeBin = void 0;
|
|
22
|
-
exports.resolveTemplatesDir = resolveTemplatesDir;
|
|
23
|
-
exports.stopAgent = stopAgent;
|
|
24
|
-
exports.bootstrapWorkspace = bootstrapWorkspace;
|
|
25
|
-
exports.runAgent = runAgent;
|
|
26
|
-
const child_process_1 = require("child_process");
|
|
27
|
-
const path_1 = __importDefault(require("path"));
|
|
28
|
-
const fs_1 = __importDefault(require("fs"));
|
|
29
|
-
const config_js_1 = require("../config.js");
|
|
30
|
-
const agent_db_js_1 = require("../agent-db.js");
|
|
31
|
-
const logs_db_js_1 = require("../logs-db.js");
|
|
32
|
-
const messages_db_js_1 = require("../messages-db.js");
|
|
33
|
-
const secrets_vault_js_1 = require("../secrets-vault.js");
|
|
34
|
-
const schedules_db_js_1 = require("../schedules-db.js");
|
|
35
|
-
const cron_parser_1 = require("cron-parser");
|
|
36
|
-
// ── Claude binary resolution ──────────────────────────────────────────────────
|
|
37
|
-
// Ensure ~/.local/bin is in PATH for child processes (where the claude CLI lives).
|
|
38
|
-
exports.claudeBin = process.env.CLAUDE_BIN ?? 'claude';
|
|
39
|
-
exports.spawnEnv = {
|
|
40
|
-
...process.env,
|
|
41
|
-
PATH: [
|
|
42
|
-
path_1.default.join(process.env.HOME ?? '', '.local', 'bin'),
|
|
43
|
-
path_1.default.join(process.env.HOME ?? '', '.nvm', 'versions', 'node', process.version, 'bin'),
|
|
44
|
-
process.env.PATH ?? '',
|
|
45
|
-
].filter(Boolean).join(':'),
|
|
46
|
-
};
|
|
47
|
-
/**
|
|
48
|
-
* Resolve the templates directory.
|
|
49
|
-
*
|
|
50
|
-
* Priority:
|
|
51
|
-
* 1. GRANCLAW_TEMPLATES_DIR env var — set by the CLI entrypoint to the
|
|
52
|
-
* templates dir bundled inside the published package.
|
|
53
|
-
* 2. <GRANCLAW_HOME>/packages/cli/templates — dev-mode fallback when the
|
|
54
|
-
* root dev script does not set the env var.
|
|
55
|
-
*
|
|
56
|
-
* Note: the env var is read on every call (not captured at module load)
|
|
57
|
-
* so the CLI entrypoint can set it just before requiring the backend.
|
|
58
|
-
* The fallback path closes over REPO_ROOT, which is a load-time snapshot
|
|
59
|
-
* of GRANCLAW_HOME — once the process has started, the fallback is stable.
|
|
60
|
-
*/
|
|
61
|
-
function resolveTemplatesDir() {
|
|
62
|
-
const envDir = process.env.GRANCLAW_TEMPLATES_DIR?.trim();
|
|
63
|
-
if (envDir) {
|
|
64
|
-
return path_1.default.resolve(envDir);
|
|
65
|
-
}
|
|
66
|
-
return path_1.default.resolve(config_js_1.REPO_ROOT, 'packages/cli/templates');
|
|
67
|
-
}
|
|
68
|
-
// Track active Claude processes so they can be killed on stop
|
|
69
|
-
const activeProcesses = new Map();
|
|
70
|
-
function stopAgent(agentId) {
|
|
71
|
-
const proc = activeProcesses.get(agentId);
|
|
72
|
-
if (proc) {
|
|
73
|
-
try {
|
|
74
|
-
proc.kill('SIGTERM');
|
|
75
|
-
}
|
|
76
|
-
catch { /* already dead */ }
|
|
77
|
-
activeProcesses.delete(agentId);
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
// ── Workspace bootstrap ───────────────────────────────────────────────────────
|
|
83
|
-
function bootstrapWorkspace(workspaceDir) {
|
|
84
|
-
fs_1.default.mkdirSync(workspaceDir, { recursive: true });
|
|
85
|
-
const claudeMd = path_1.default.join(workspaceDir, 'CLAUDE.md');
|
|
86
|
-
if (!fs_1.default.existsSync(claudeMd)) {
|
|
87
|
-
const template = path_1.default.join(resolveTemplatesDir(), 'CLAUDE.onboarding.md');
|
|
88
|
-
fs_1.default.copyFileSync(template, claudeMd);
|
|
89
|
-
console.log(`[runner] copied onboarding CLAUDE.md to ${workspaceDir}`);
|
|
90
|
-
}
|
|
91
|
-
// Ensure agent has its own .mcp.json to prevent inheriting project-root MCP servers.
|
|
92
|
-
// Claude CLI walks up the directory tree to discover .mcp.json — without this,
|
|
93
|
-
// agents inherit whatever MCP servers the host developer has configured.
|
|
94
|
-
const mcpJson = path_1.default.join(workspaceDir, '.mcp.json');
|
|
95
|
-
if (!fs_1.default.existsSync(mcpJson)) {
|
|
96
|
-
fs_1.default.writeFileSync(mcpJson, JSON.stringify({ mcpServers: {} }, null, 2));
|
|
97
|
-
console.log(`[runner] created empty .mcp.json in ${workspaceDir}`);
|
|
98
|
-
}
|
|
99
|
-
// Bootstrap vault directory structure (second brain)
|
|
100
|
-
const vaultDir = path_1.default.join(workspaceDir, 'vault');
|
|
101
|
-
if (!fs_1.default.existsSync(vaultDir)) {
|
|
102
|
-
for (const sub of ['journal', 'sessions', 'actions', 'topics', 'knowledge']) {
|
|
103
|
-
fs_1.default.mkdirSync(path_1.default.join(vaultDir, sub), { recursive: true });
|
|
104
|
-
}
|
|
105
|
-
console.log(`[runner] created vault structure in ${vaultDir}`);
|
|
106
|
-
}
|
|
107
|
-
// Bootstrap skills from templates
|
|
108
|
-
const skillsTemplateDir = path_1.default.join(resolveTemplatesDir(), 'skills');
|
|
109
|
-
if (fs_1.default.existsSync(skillsTemplateDir)) {
|
|
110
|
-
const targetSkillsDir = path_1.default.join(workspaceDir, '.claude', 'skills');
|
|
111
|
-
for (const skillName of fs_1.default.readdirSync(skillsTemplateDir)) {
|
|
112
|
-
const srcDir = path_1.default.join(skillsTemplateDir, skillName);
|
|
113
|
-
const destDir = path_1.default.join(targetSkillsDir, skillName);
|
|
114
|
-
if (!fs_1.default.statSync(srcDir).isDirectory())
|
|
115
|
-
continue;
|
|
116
|
-
if (fs_1.default.existsSync(destDir))
|
|
117
|
-
continue; // don't overwrite existing
|
|
118
|
-
fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
119
|
-
for (const file of fs_1.default.readdirSync(srcDir)) {
|
|
120
|
-
fs_1.default.copyFileSync(path_1.default.join(srcDir, file), path_1.default.join(destDir, file));
|
|
121
|
-
}
|
|
122
|
-
// Make shell scripts executable
|
|
123
|
-
for (const file of fs_1.default.readdirSync(destDir)) {
|
|
124
|
-
if (file.endsWith('.sh')) {
|
|
125
|
-
fs_1.default.chmodSync(path_1.default.join(destDir, file), 0o755);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
console.log(`[runner] bootstrapped skill "${skillName}" to ${destDir}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
// Bootstrap default vault housekeeping schedule
|
|
132
|
-
const agentId = path_1.default.basename(workspaceDir);
|
|
133
|
-
try {
|
|
134
|
-
const existing = (0, schedules_db_js_1.listSchedules)(agentId);
|
|
135
|
-
const hasHousekeeping = existing.some(s => s.name === 'Vault housekeeping');
|
|
136
|
-
if (!hasHousekeeping) {
|
|
137
|
-
const cron = '30 23 * * *';
|
|
138
|
-
const nextRun = (0, cron_parser_1.parseExpression)(cron, { tz: 'Asia/Singapore' }).next().getTime();
|
|
139
|
-
(0, schedules_db_js_1.createSchedule)(agentId, {
|
|
140
|
-
name: 'Vault housekeeping',
|
|
141
|
-
message: 'Run vault housekeeping: scan all vault folders, rebuild every index.md with one-line summaries for each file, update vault/index.md with folder counts and recent activity. Check for orphaned wikilinks and entities that need topic notes. Never delete files.',
|
|
142
|
-
cron,
|
|
143
|
-
timezone: 'Asia/Singapore',
|
|
144
|
-
nextRun,
|
|
145
|
-
});
|
|
146
|
-
console.log(`[runner] created default vault housekeeping schedule for ${agentId}`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
catch { /* schedules DB may not be ready yet for this agent */ }
|
|
150
|
-
}
|
|
151
|
-
function extractAgentName(workspaceDir) {
|
|
152
|
-
// After onboarding, Claude writes a real CLAUDE.md with the agent name as H1
|
|
153
|
-
const claudeMd = path_1.default.join(workspaceDir, 'CLAUDE.md');
|
|
154
|
-
if (!fs_1.default.existsSync(claudeMd))
|
|
155
|
-
return null;
|
|
156
|
-
const content = fs_1.default.readFileSync(claudeMd, 'utf8');
|
|
157
|
-
const match = content.match(/^#\s+(.+)/m);
|
|
158
|
-
return match ? match[1].trim() : null;
|
|
159
|
-
}
|
|
160
|
-
// ── Runner ────────────────────────────────────────────────────────────────────
|
|
161
|
-
async function runAgent(agent, message, onChunk, options) {
|
|
162
|
-
const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
|
|
163
|
-
const channelId = options?.channelId ?? 'ui';
|
|
164
|
-
bootstrapWorkspace(workspaceDir);
|
|
165
|
-
// Track soul state before this turn so we know if onboarding just completed
|
|
166
|
-
const soulExistedBefore = fs_1.default.existsSync(path_1.default.join(workspaceDir, 'SOUL.md'));
|
|
167
|
-
// If SOUL.md doesn't exist, this is a fresh agent — prepend onboarding nudge
|
|
168
|
-
// so Claude doesn't give a generic reply and actually follows CLAUDE.md
|
|
169
|
-
let finalMessage = message;
|
|
170
|
-
if (!soulExistedBefore) {
|
|
171
|
-
finalMessage = `[SYSTEM: You are a brand new agent with no identity yet. SOUL.md does not exist. You MUST follow the onboarding instructions in your CLAUDE.md before doing anything else. Do NOT give a generic greeting. Start the onboarding process immediately.]\n\nUser message: ${message}`;
|
|
172
|
-
}
|
|
173
|
-
const sessionId = (0, agent_db_js_1.getSession)(workspaceDir, agent.id, channelId);
|
|
174
|
-
// Always inject recent history from ALL channels so the agent has full context
|
|
175
|
-
// (UI chat, workflow results — everything the agent said or received)
|
|
176
|
-
if (soulExistedBefore && !finalMessage.includes('--- Recent History ---')) {
|
|
177
|
-
const recentMessages = (0, messages_db_js_1.getAllRecentMessages)(agent.id, 50)
|
|
178
|
-
.filter(m => m.role !== 'tool_call')
|
|
179
|
-
.slice(-20);
|
|
180
|
-
if (recentMessages.length > 0) {
|
|
181
|
-
const history = recentMessages
|
|
182
|
-
.map(m => {
|
|
183
|
-
const ch = m.channelId !== 'ui' ? ` [${m.channelId}]` : '';
|
|
184
|
-
return `[${m.role}${ch}]: ${m.content.slice(0, 500)}`;
|
|
185
|
-
})
|
|
186
|
-
.join('\n\n');
|
|
187
|
-
finalMessage = `[SYSTEM: Here is recent activity across all channels for context.]\n\n--- Recent History ---\n${history}\n--- End History ---\n\nUser: ${message}`;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
const startedAt = Date.now();
|
|
191
|
-
(0, logs_db_js_1.logAction)(agent.id, 'message', { text: message });
|
|
192
|
-
const attempt = (resume) => new Promise((resolve, reject) => {
|
|
193
|
-
const args = ['-p', finalMessage, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions'];
|
|
194
|
-
if (resume)
|
|
195
|
-
args.push('--resume', resume);
|
|
196
|
-
// Inject core system instructions that every agent must follow (vault, security, skills)
|
|
197
|
-
const systemMd = path_1.default.join(resolveTemplatesDir(), 'DO_NOT_DELETE.md');
|
|
198
|
-
if (fs_1.default.existsSync(systemMd)) {
|
|
199
|
-
args.push('--append-system-prompt-file', systemMd);
|
|
200
|
-
}
|
|
201
|
-
// Use --strict-mcp-config to prevent Claude CLI from inheriting MCP servers
|
|
202
|
-
// from parent directories (e.g., project-root .mcp.json with Playwright).
|
|
203
|
-
// Only MCP servers explicitly in --mcp-config are loaded.
|
|
204
|
-
const agentMcpConfig = path_1.default.join(workspaceDir, 'tools.mcp.json');
|
|
205
|
-
const workspaceMcpConfig = path_1.default.join(workspaceDir, '.mcp.json');
|
|
206
|
-
if (fs_1.default.existsSync(agentMcpConfig)) {
|
|
207
|
-
args.push('--mcp-config', agentMcpConfig, '--strict-mcp-config');
|
|
208
|
-
console.log(`[agent:${agent.id}] loading MCP tools from ${agentMcpConfig} (strict)`);
|
|
209
|
-
}
|
|
210
|
-
else if (fs_1.default.existsSync(workspaceMcpConfig)) {
|
|
211
|
-
args.push('--mcp-config', workspaceMcpConfig, '--strict-mcp-config');
|
|
212
|
-
}
|
|
213
|
-
// Inject secrets so they're available regardless of calling process
|
|
214
|
-
// (agent process has them in env, but orchestrator/workflow runner does not)
|
|
215
|
-
const agentSecrets = (0, secrets_vault_js_1.getSecrets)(agent.id);
|
|
216
|
-
const agentEnv = { ...exports.spawnEnv, ...agentSecrets };
|
|
217
|
-
// CRITICAL: never pass Anthropic auth env vars to the Claude CLI.
|
|
218
|
-
// Claude Code must use the user's subscription (OAuth), not API mode.
|
|
219
|
-
// If these are set, the CLI switches to API mode and fails auth.
|
|
220
|
-
delete agentEnv.ANTHROPIC_API_KEY;
|
|
221
|
-
delete agentEnv.ANTHROPIC_AUTH_TOKEN;
|
|
222
|
-
delete agentEnv.ANTHROPIC_BASE_URL;
|
|
223
|
-
delete agentEnv.CLAUDE_API_KEY;
|
|
224
|
-
// Browser: session directory + persistent profile (so agent-browser always uses saved logins)
|
|
225
|
-
agentEnv.AGENT_BROWSER_SESSIONS_DIR = path_1.default.join(workspaceDir, '.browser-sessions');
|
|
226
|
-
const profileDir = path_1.default.join(workspaceDir, '.browser-profile');
|
|
227
|
-
if (fs_1.default.existsSync(profileDir)) {
|
|
228
|
-
agentEnv.AGENT_BROWSER_PROFILE = profileDir;
|
|
229
|
-
}
|
|
230
|
-
const proc = (0, child_process_1.spawn)(exports.claudeBin, args, { cwd: workspaceDir, env: agentEnv, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
231
|
-
proc.stdin?.end();
|
|
232
|
-
activeProcesses.set(agent.id, proc);
|
|
233
|
-
proc.on('exit', () => { activeProcesses.delete(agent.id); });
|
|
234
|
-
let buffer = '';
|
|
235
|
-
let newSessionId = resume ?? '';
|
|
236
|
-
proc.stdout.on('data', (raw) => {
|
|
237
|
-
buffer += raw.toString();
|
|
238
|
-
const lines = buffer.split('\n');
|
|
239
|
-
buffer = lines.pop() ?? '';
|
|
240
|
-
for (const line of lines) {
|
|
241
|
-
const trimmed = line.trim();
|
|
242
|
-
if (!trimmed)
|
|
243
|
-
continue;
|
|
244
|
-
let parsed;
|
|
245
|
-
try {
|
|
246
|
-
parsed = JSON.parse(trimmed);
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
onChunk({ type: 'text', text: trimmed });
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
handleClaudeEvent(parsed, onChunk, (id) => { newSessionId = id; }, agent.id);
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
proc.stderr.on('data', (raw) => {
|
|
256
|
-
const msg = raw.toString().trim();
|
|
257
|
-
if (msg)
|
|
258
|
-
console.error(`[agent:${agent.id}] stderr:`, msg);
|
|
259
|
-
});
|
|
260
|
-
proc.on('close', async (code) => {
|
|
261
|
-
if (code === 0 || code === null) {
|
|
262
|
-
if (newSessionId) {
|
|
263
|
-
(0, agent_db_js_1.saveSession)(workspaceDir, agent.id, newSessionId, channelId);
|
|
264
|
-
}
|
|
265
|
-
// If SOUL.md was created this turn, Claude finished onboarding — announce name
|
|
266
|
-
if (!soulExistedBefore && fs_1.default.existsSync(path_1.default.join(workspaceDir, 'SOUL.md'))) {
|
|
267
|
-
const name = extractAgentName(workspaceDir);
|
|
268
|
-
if (name) {
|
|
269
|
-
console.log(`[agent:${agent.id}] onboarding complete — name: "${name}"`);
|
|
270
|
-
onChunk({ type: 'agent_ready', name });
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
onChunk({ type: 'done', sessionId: newSessionId });
|
|
274
|
-
(0, logs_db_js_1.logAction)(agent.id, 'system', null, { exitCode: code }, Date.now() - startedAt);
|
|
275
|
-
resolve();
|
|
276
|
-
}
|
|
277
|
-
else {
|
|
278
|
-
(0, logs_db_js_1.logAction)(agent.id, 'system', null, { exitCode: code }, Date.now() - startedAt);
|
|
279
|
-
reject(new Error(`claude exited with code ${code}`));
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
try {
|
|
284
|
-
await attempt(sessionId);
|
|
285
|
-
}
|
|
286
|
-
catch (err) {
|
|
287
|
-
if (sessionId) {
|
|
288
|
-
console.warn(`[agent:${agent.id}] session ${sessionId} rejected, retrying fresh`);
|
|
289
|
-
(0, agent_db_js_1.saveSession)(workspaceDir, agent.id, '', channelId);
|
|
290
|
-
try {
|
|
291
|
-
await attempt(null);
|
|
292
|
-
}
|
|
293
|
-
catch (retryErr) {
|
|
294
|
-
onChunk({ type: 'error', message: retryErr instanceof Error ? retryErr.message : String(retryErr) });
|
|
295
|
-
(0, logs_db_js_1.logAction)(agent.id, 'error', null, { message: retryErr instanceof Error ? retryErr.message : String(retryErr) });
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
onChunk({ type: 'error', message: err instanceof Error ? err.message : String(err) });
|
|
300
|
-
(0, logs_db_js_1.logAction)(agent.id, 'error', null, { message: err instanceof Error ? err.message : String(err) });
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
function handleClaudeEvent(event, onChunk, onSessionId, agentId) {
|
|
305
|
-
const type = event.type;
|
|
306
|
-
if (type === 'assistant') {
|
|
307
|
-
const msg = event.message;
|
|
308
|
-
if (msg?.content) {
|
|
309
|
-
for (const block of msg.content) {
|
|
310
|
-
if (block.type === 'text' && block.text) {
|
|
311
|
-
onChunk({ type: 'text', text: block.text });
|
|
312
|
-
}
|
|
313
|
-
else if (block.type === 'tool_use') {
|
|
314
|
-
const b = block;
|
|
315
|
-
onChunk({ type: 'tool_call', tool: b.name ?? 'unknown', input: b.input });
|
|
316
|
-
if (agentId)
|
|
317
|
-
(0, logs_db_js_1.logAction)(agentId, 'tool_call', { tool: b.name, input: b.input });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
else if (type === 'tool_result') {
|
|
323
|
-
const content = event.content;
|
|
324
|
-
const text = content?.find((c) => c.type === 'text')?.text ?? '';
|
|
325
|
-
onChunk({ type: 'tool_result', tool: '', output: text });
|
|
326
|
-
if (agentId)
|
|
327
|
-
(0, logs_db_js_1.logAction)(agentId, 'tool_result', null, { text: text.slice(0, 500) });
|
|
328
|
-
}
|
|
329
|
-
else if (type === 'result') {
|
|
330
|
-
const sid = event.session_id;
|
|
331
|
-
if (sid)
|
|
332
|
-
onSessionId(sid);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<title>Chat History — Architecture Options</title>
|
|
6
|
-
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
7
|
-
<style>
|
|
8
|
-
:root {
|
|
9
|
-
--bg: #0d0d0d;
|
|
10
|
-
--surface: #161616;
|
|
11
|
-
--card: #1e1e1e;
|
|
12
|
-
--border: #2a2a2a;
|
|
13
|
-
--text: #e8e8e8;
|
|
14
|
-
--muted: #888;
|
|
15
|
-
--accent-a: #7c3aed;
|
|
16
|
-
--accent-b: #0ea5e9;
|
|
17
|
-
--accent-c: #10b981;
|
|
18
|
-
--warn: #f59e0b;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
22
|
-
|
|
23
|
-
body {
|
|
24
|
-
background: var(--bg);
|
|
25
|
-
color: var(--text);
|
|
26
|
-
font-family: 'Inter', system-ui, sans-serif;
|
|
27
|
-
padding: 48px 32px;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
h1 {
|
|
31
|
-
font-size: 1.5rem;
|
|
32
|
-
font-weight: 700;
|
|
33
|
-
margin-bottom: 8px;
|
|
34
|
-
letter-spacing: -0.02em;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.subtitle {
|
|
38
|
-
color: var(--muted);
|
|
39
|
-
font-size: 0.875rem;
|
|
40
|
-
margin-bottom: 48px;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
.options {
|
|
44
|
-
display: grid;
|
|
45
|
-
grid-template-columns: repeat(3, 1fr);
|
|
46
|
-
gap: 24px;
|
|
47
|
-
margin-bottom: 48px;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.option {
|
|
51
|
-
background: var(--card);
|
|
52
|
-
border: 1px solid var(--border);
|
|
53
|
-
border-radius: 12px;
|
|
54
|
-
overflow: hidden;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.option-header {
|
|
58
|
-
padding: 16px 20px 12px;
|
|
59
|
-
border-bottom: 1px solid var(--border);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.option-label {
|
|
63
|
-
font-size: 0.7rem;
|
|
64
|
-
font-weight: 600;
|
|
65
|
-
letter-spacing: 0.1em;
|
|
66
|
-
text-transform: uppercase;
|
|
67
|
-
margin-bottom: 4px;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.option-a .option-label { color: var(--accent-a); }
|
|
71
|
-
.option-b .option-label { color: var(--accent-b); }
|
|
72
|
-
.option-c .option-label { color: var(--accent-c); }
|
|
73
|
-
|
|
74
|
-
.option-title {
|
|
75
|
-
font-size: 1rem;
|
|
76
|
-
font-weight: 600;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.diagram {
|
|
80
|
-
padding: 20px;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/* Hand-drawn style diagrams with SVG */
|
|
84
|
-
.diagram svg {
|
|
85
|
-
width: 100%;
|
|
86
|
-
height: auto;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
.tradeoffs {
|
|
90
|
-
padding: 16px 20px;
|
|
91
|
-
border-top: 1px solid var(--border);
|
|
92
|
-
display: flex;
|
|
93
|
-
flex-direction: column;
|
|
94
|
-
gap: 8px;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.tradeoff {
|
|
98
|
-
display: flex;
|
|
99
|
-
align-items: flex-start;
|
|
100
|
-
gap: 8px;
|
|
101
|
-
font-size: 0.78rem;
|
|
102
|
-
line-height: 1.4;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.tradeoff .icon {
|
|
106
|
-
flex-shrink: 0;
|
|
107
|
-
margin-top: 1px;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.pro { color: var(--accent-c); }
|
|
111
|
-
.con { color: #f87171; }
|
|
112
|
-
|
|
113
|
-
.recommendation {
|
|
114
|
-
background: var(--card);
|
|
115
|
-
border: 1px solid var(--accent-c);
|
|
116
|
-
border-radius: 12px;
|
|
117
|
-
padding: 24px;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.recommendation h2 {
|
|
121
|
-
font-size: 0.7rem;
|
|
122
|
-
font-weight: 600;
|
|
123
|
-
letter-spacing: 0.1em;
|
|
124
|
-
text-transform: uppercase;
|
|
125
|
-
color: var(--accent-c);
|
|
126
|
-
margin-bottom: 8px;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.recommendation h3 {
|
|
130
|
-
font-size: 1.1rem;
|
|
131
|
-
font-weight: 600;
|
|
132
|
-
margin-bottom: 16px;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.rec-diagram {
|
|
136
|
-
background: var(--surface);
|
|
137
|
-
border-radius: 8px;
|
|
138
|
-
padding: 20px;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/* Mermaid theme overrides */
|
|
142
|
-
.mermaid {
|
|
143
|
-
background: transparent !important;
|
|
144
|
-
}
|
|
145
|
-
</style>
|
|
146
|
-
</head>
|
|
147
|
-
<body>
|
|
148
|
-
|
|
149
|
-
<h1>Chat History — Architecture Options</h1>
|
|
150
|
-
<p class="subtitle">How should agent-brother store messages so agents remember across channels and the UI can display history?</p>
|
|
151
|
-
|
|
152
|
-
<div class="options">
|
|
153
|
-
|
|
154
|
-
<!-- OPTION A -->
|
|
155
|
-
<div class="option option-a">
|
|
156
|
-
<div class="option-header">
|
|
157
|
-
<div class="option-label">Option A</div>
|
|
158
|
-
<div class="option-title">Session per conversation</div>
|
|
159
|
-
</div>
|
|
160
|
-
<div class="diagram">
|
|
161
|
-
<div class="mermaid">
|
|
162
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'background': '#1e1e1e', 'primaryColor': '#7c3aed', 'primaryTextColor': '#e8e8e8', 'primaryBorderColor': '#7c3aed', 'lineColor': '#555', 'secondaryColor': '#2a2a2a', 'tertiaryColor': '#1e1e1e', 'edgeLabelBackground': '#1e1e1e', 'fontSize': '12px'}}}%%
|
|
163
|
-
flowchart TD
|
|
164
|
-
WA["📱 WhatsApp"] -->|msg + channel:wa| B
|
|
165
|
-
LI["💼 LinkedIn"] -->|msg + channel:li| B
|
|
166
|
-
UI["🖥 Dashboard UI"] -->|msg + channel:ui| B
|
|
167
|
-
|
|
168
|
-
B["Backend\nOrchestrator"]
|
|
169
|
-
|
|
170
|
-
B -->|resume session_wa| AG
|
|
171
|
-
B -->|resume session_li| AG
|
|
172
|
-
B -->|resume session_ui| AG
|
|
173
|
-
|
|
174
|
-
AG["🤖 Agent\nRunner"]
|
|
175
|
-
AG -->|spawns| CL["claude CLI\n--resume <session_id>"]
|
|
176
|
-
|
|
177
|
-
AG -->|stores| DB[("SQLite\nsession_wa\nsession_li\nsession_ui")]
|
|
178
|
-
|
|
179
|
-
B -->|log msg| MG[("MongoDB\nmessages")]
|
|
180
|
-
|
|
181
|
-
MG -->|history| UI
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
<div class="tradeoffs">
|
|
185
|
-
<div class="tradeoff pro"><span class="icon">✓</span><span>Each channel has its own Claude context window — no cross-talk</span></div>
|
|
186
|
-
<div class="tradeoff pro"><span class="icon">✓</span><span>Fast — no extra tokens injected per request</span></div>
|
|
187
|
-
<div class="tradeoff con"><span class="icon">✗</span><span>Claude sessions expire — agent loses memory cold on restart</span></div>
|
|
188
|
-
<div class="tradeoff con"><span class="icon">✗</span><span>Can't move a conversation between channels</span></div>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
|
|
192
|
-
<!-- OPTION B -->
|
|
193
|
-
<div class="option option-b">
|
|
194
|
-
<div class="option-header">
|
|
195
|
-
<div class="option-label">Option B</div>
|
|
196
|
-
<div class="option-title">Inject history, no sessions</div>
|
|
197
|
-
</div>
|
|
198
|
-
<div class="diagram">
|
|
199
|
-
<div class="mermaid">
|
|
200
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'background': '#1e1e1e', 'primaryColor': '#0ea5e9', 'primaryTextColor': '#e8e8e8', 'primaryBorderColor': '#0ea5e9', 'lineColor': '#555', 'secondaryColor': '#2a2a2a', 'tertiaryColor': '#1e1e1e', 'edgeLabelBackground': '#1e1e1e', 'fontSize': '12px'}}}%%
|
|
201
|
-
flowchart TD
|
|
202
|
-
WA["📱 WhatsApp"] -->|msg| B
|
|
203
|
-
LI["💼 LinkedIn"] -->|msg| B
|
|
204
|
-
UI["🖥 Dashboard UI"] -->|msg| B
|
|
205
|
-
|
|
206
|
-
B["Backend\nOrchestrator"]
|
|
207
|
-
|
|
208
|
-
B -->|fetch last N msgs| MG[("MongoDB\nmessages")]
|
|
209
|
-
MG -->|history| B
|
|
210
|
-
|
|
211
|
-
B -->|prompt = history + msg| AG["🤖 Agent\nRunner"]
|
|
212
|
-
AG -->|spawns fresh| CL["claude CLI\n-p 'prev: ...\nuser: ...'"]
|
|
213
|
-
|
|
214
|
-
AG -->|save reply| MG
|
|
215
|
-
|
|
216
|
-
MG -->|display| UI
|
|
217
|
-
</div>
|
|
218
|
-
</div>
|
|
219
|
-
<div class="tradeoffs">
|
|
220
|
-
<div class="tradeoff pro"><span class="icon">✓</span><span>Full control over what agent sees — no session expiry surprises</span></div>
|
|
221
|
-
<div class="tradeoff pro"><span class="icon">✓</span><span>Conversation can move freely across channels</span></div>
|
|
222
|
-
<div class="tradeoff con"><span class="icon">✗</span><span>Extra tokens on every request — slower and more expensive</span></div>
|
|
223
|
-
<div class="tradeoff con"><span class="icon">✗</span><span>Context window fills up on long conversations</span></div>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
|
|
227
|
-
<!-- OPTION C -->
|
|
228
|
-
<div class="option option-c">
|
|
229
|
-
<div class="option-header">
|
|
230
|
-
<div class="option-label">Option C — Recommended</div>
|
|
231
|
-
<div class="option-title">Hybrid: resume + fallback inject</div>
|
|
232
|
-
</div>
|
|
233
|
-
<div class="diagram">
|
|
234
|
-
<div class="mermaid">
|
|
235
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'background': '#1e1e1e', 'primaryColor': '#10b981', 'primaryTextColor': '#e8e8e8', 'primaryBorderColor': '#10b981', 'lineColor': '#555', 'secondaryColor': '#2a2a2a', 'tertiaryColor': '#1e1e1e', 'edgeLabelBackground': '#1e1e1e', 'fontSize': '12px'}}}%%
|
|
236
|
-
flowchart TD
|
|
237
|
-
WA["📱 WhatsApp"] --> B
|
|
238
|
-
LI["💼 LinkedIn"] --> B
|
|
239
|
-
UI["🖥 Dashboard UI"] --> B
|
|
240
|
-
|
|
241
|
-
B["Backend\nOrchestrator"]
|
|
242
|
-
B -->|save msg| MG[("MongoDB\nmessages")]
|
|
243
|
-
B -->|run| AG["🤖 Agent\nRunner"]
|
|
244
|
-
|
|
245
|
-
AG -->|happy path| CL1["claude CLI\n--resume session_id"]
|
|
246
|
-
CL1 -->|exit 1 / expired| FB["⚠ Session expired\nfallback"]
|
|
247
|
-
|
|
248
|
-
FB -->|fetch last 20 msgs| MG
|
|
249
|
-
MG --> FB
|
|
250
|
-
FB -->|inject as context| CL2["claude CLI\nfresh + history"]
|
|
251
|
-
|
|
252
|
-
CL1 -->|reply| B
|
|
253
|
-
CL2 -->|reply + new session_id| B
|
|
254
|
-
|
|
255
|
-
B -->|save reply| MG
|
|
256
|
-
MG -->|display| UI
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
<div class="tradeoffs">
|
|
260
|
-
<div class="tradeoff pro"><span class="icon">✓</span><span>Fast on happy path (--resume, no extra tokens)</span></div>
|
|
261
|
-
<div class="tradeoff pro"><span class="icon">✓</span><span>Survives session expiry — injects history automatically</span></div>
|
|
262
|
-
<div class="tradeoff pro"><span class="icon">✓</span><span>Full message log in MongoDB for UI + cross-channel queries</span></div>
|
|
263
|
-
<div class="tradeoff con"><span class="icon">✗</span><span>Slightly more complex runner logic (already partially there)</span></div>
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
266
|
-
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
<!-- DATA MODEL -->
|
|
270
|
-
<div class="recommendation">
|
|
271
|
-
<h2>Recommended data model — Option C</h2>
|
|
272
|
-
<h3>MongoDB <code>messages</code> collection + SQLite <code>agent_sessions</code> per (agent, channel, conversation)</h3>
|
|
273
|
-
<div class="rec-diagram">
|
|
274
|
-
<div class="mermaid">
|
|
275
|
-
%%{init: {'theme': 'dark', 'themeVariables': {'background': '#161616', 'primaryColor': '#10b981', 'primaryTextColor': '#e8e8e8', 'primaryBorderColor': '#10b981', 'lineColor': '#555', 'secondaryColor': '#2a2a2a', 'tertiaryColor': '#161616', 'edgeLabelBackground': '#161616', 'fontSize': '13px'}}}%%
|
|
276
|
-
erDiagram
|
|
277
|
-
MESSAGE {
|
|
278
|
-
string id PK
|
|
279
|
-
string agentId
|
|
280
|
-
string channelId "ui | whatsapp | linkedin"
|
|
281
|
-
string conversationId "groups a thread"
|
|
282
|
-
string role "user | assistant"
|
|
283
|
-
string content
|
|
284
|
-
number createdAt
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
AGENT_SESSION {
|
|
288
|
-
string agentId PK
|
|
289
|
-
string channelId PK
|
|
290
|
-
string conversationId PK
|
|
291
|
-
string sessionId "Claude resume token"
|
|
292
|
-
number updatedAt
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
MESSAGE }o--|| AGENT_SESSION : "linked by agentId+channelId+conversationId"
|
|
296
|
-
</div>
|
|
297
|
-
</div>
|
|
298
|
-
</div>
|
|
299
|
-
|
|
300
|
-
<script>
|
|
301
|
-
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
|
|
302
|
-
</script>
|
|
303
|
-
</body>
|
|
304
|
-
</html>
|