granclaw 0.0.1-beta.9 → 0.0.1-beta.91
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/backend/agent/browser-bin.js +62 -0
- package/dist/backend/agent/channel-broadcast.js +25 -0
- package/dist/backend/agent/compaction-retry.js +68 -0
- package/dist/backend/agent/context-budget.js +36 -0
- package/dist/backend/agent/message-assembly.js +25 -0
- package/dist/backend/agent/process.js +42 -68
- package/dist/backend/agent/runner-pi.js +462 -218
- package/dist/backend/agent/telegram-adapter.js +17 -71
- package/dist/backend/agent/telegram-chats.js +56 -0
- package/dist/backend/agent/telegram-http-client.js +2 -27
- package/dist/backend/agent/telegram-markdown.js +46 -0
- package/dist/backend/agent-db.js +0 -14
- package/dist/backend/app-config.js +26 -0
- package/dist/backend/app-secrets.js +99 -0
- package/dist/backend/assets/stealth-extension/stealth.js +206 -11
- package/dist/backend/browser/session-manager.js +41 -89
- package/dist/backend/browser/stealth.js +52 -64
- package/dist/backend/browser-sessions.js +52 -45
- package/dist/backend/config.js +0 -17
- package/dist/backend/data-db.js +0 -14
- package/dist/backend/esm-import.js +0 -18
- package/dist/backend/extensions/loader.js +90 -0
- package/dist/backend/extensions/types.js +2 -0
- package/dist/backend/index.js +30 -5
- package/dist/backend/integrations/agent-integrations-db.js +53 -0
- package/dist/backend/integrations/registry.js +64 -0
- package/dist/backend/integrations/routes.js +42 -0
- package/dist/backend/integrations/types.js +2 -0
- package/dist/backend/lib/flatten-markdown-tables.js +81 -0
- package/dist/backend/lib/i18n-telegram.js +35 -35
- package/dist/backend/logs-db.js +0 -8
- package/dist/backend/messages-db.js +16 -8
- package/dist/backend/orchestrator/agent-manager.js +4 -32
- package/dist/backend/orchestrator/browser-live.js +364 -217
- package/dist/backend/orchestrator/server.js +160 -210
- package/dist/backend/providers-config.js +67 -27
- package/dist/backend/routes/logs.js +0 -1
- package/dist/backend/scheduler.js +1 -8
- package/dist/backend/schedules-db.js +0 -12
- package/dist/backend/secrets-vault.js +0 -6
- package/dist/backend/takeover-listeners.js +27 -0
- package/dist/backend/takeover-messages.js +0 -31
- package/dist/backend/takeover-state.js +12 -7
- package/dist/backend/takeover-timeout.js +1 -25
- package/dist/backend/takeover-url-resolver.js +29 -0
- package/dist/backend/tasks-db.js +0 -11
- package/dist/backend/telemetry.js +30 -0
- package/dist/backend/usage-scanner.js +1 -18
- package/dist/backend/workflows/runner.js +2 -28
- package/dist/backend/workflows-db.js +37 -14
- package/dist/backend/workspace-pool.js +24 -19
- package/dist/frontend/assets/index-Hymtb-e4.css +1 -0
- package/dist/frontend/assets/index-gSUjRoId.js +158 -0
- package/dist/frontend/index.html +2 -2
- package/dist/home.js +20 -0
- package/dist/index.js +14 -0
- package/dist/telemetry.js +37 -0
- package/package.json +9 -4
- package/templates/AGENT.onboarding.md +19 -2
- package/templates/SYSTEM.md +88 -8
- package/templates/skills/email/SKILL.md +300 -0
- package/templates/skills/email/gmcli.sh +55 -0
- package/templates/skills/email/read-imap.py +231 -0
- package/templates/skills/email/send-smtp.py +104 -0
- package/templates/skills/skill-creator/SKILL.md +188 -0
- package/templates/skills/whatsapp/SKILL.md +147 -0
- package/templates/skills/whatsapp/whatsapp.sh +20 -0
- package/templates/skills/workflows/SKILL.md +57 -0
- package/dist/frontend/assets/index-BmQN0cOF.css +0 -1
- package/dist/frontend/assets/index-CDbh9xnJ.js +0 -143
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerBrowserProvider = registerBrowserProvider;
|
|
7
|
+
exports._resetBrowserProvidersForTests = _resetBrowserProvidersForTests;
|
|
8
|
+
exports.buildArgv = buildArgv;
|
|
9
|
+
exports.resolveBrowserBinary = resolveBrowserBinary;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const stealth_js_1 = require("../browser/stealth.js");
|
|
13
|
+
const providers = [];
|
|
14
|
+
function registerBrowserProvider(provider) {
|
|
15
|
+
providers.push(provider);
|
|
16
|
+
}
|
|
17
|
+
function _resetBrowserProvidersForTests() {
|
|
18
|
+
providers.length = 0;
|
|
19
|
+
}
|
|
20
|
+
function buildArgv(res, command, args) {
|
|
21
|
+
return [...res.preCommandArgs, command, ...args, ...res.postCommandArgs];
|
|
22
|
+
}
|
|
23
|
+
async function resolveBrowserBinary(agentId, workspaceDir) {
|
|
24
|
+
for (const provider of providers) {
|
|
25
|
+
const resolution = await provider(agentId, workspaceDir);
|
|
26
|
+
if (resolution)
|
|
27
|
+
return resolution;
|
|
28
|
+
}
|
|
29
|
+
const cdpFile = `/tmp/granclaw-cdp-${agentId}.url`;
|
|
30
|
+
if (fs_1.default.existsSync(cdpFile)) {
|
|
31
|
+
try {
|
|
32
|
+
const wsUrl = fs_1.default.readFileSync(cdpFile, 'utf8').trim();
|
|
33
|
+
const port = new URL(wsUrl.replace('ws://', 'http://')).port;
|
|
34
|
+
if (port) {
|
|
35
|
+
return {
|
|
36
|
+
bin: process.env.AGENT_BROWSER_BIN ?? 'agent-browser',
|
|
37
|
+
preCommandArgs: ['--cdp', port, '--session', agentId],
|
|
38
|
+
postCommandArgs: [],
|
|
39
|
+
env: {},
|
|
40
|
+
isRemote: false,
|
|
41
|
+
recordingSupported: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
const bin = process.env.AGENT_BROWSER_BIN ?? 'agent-browser';
|
|
48
|
+
const profileDir = path_1.default.join(workspaceDir, '.browser-profile');
|
|
49
|
+
const preCommandArgs = ['--session', agentId];
|
|
50
|
+
if (fs_1.default.existsSync(profileDir)) {
|
|
51
|
+
preCommandArgs.push('--profile', profileDir);
|
|
52
|
+
}
|
|
53
|
+
preCommandArgs.push(...(0, stealth_js_1.stealthArgv)());
|
|
54
|
+
return {
|
|
55
|
+
bin,
|
|
56
|
+
preCommandArgs,
|
|
57
|
+
postCommandArgs: [],
|
|
58
|
+
env: {},
|
|
59
|
+
isRemote: false,
|
|
60
|
+
recordingSupported: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WS_OPEN = void 0;
|
|
4
|
+
exports.broadcastToClients = broadcastToClients;
|
|
5
|
+
exports.getOrCreateChannelSet = getOrCreateChannelSet;
|
|
6
|
+
exports.WS_OPEN = 1;
|
|
7
|
+
function broadcastToClients(clients, data) {
|
|
8
|
+
const json = JSON.stringify(data);
|
|
9
|
+
let sent = 0;
|
|
10
|
+
for (const ws of clients) {
|
|
11
|
+
if (ws.readyState === exports.WS_OPEN) {
|
|
12
|
+
ws.send(json);
|
|
13
|
+
sent++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return sent;
|
|
17
|
+
}
|
|
18
|
+
function getOrCreateChannelSet(map, channelId) {
|
|
19
|
+
let set = map.get(channelId);
|
|
20
|
+
if (!set) {
|
|
21
|
+
set = new Set();
|
|
22
|
+
map.set(channelId, set);
|
|
23
|
+
}
|
|
24
|
+
return set;
|
|
25
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCompactionWithRecovery = runCompactionWithRecovery;
|
|
4
|
+
const TURN_PREFIX_FAILURE_RE = /turn prefix summarization failed/i;
|
|
5
|
+
const AVOID_SPLIT_LADDER = [
|
|
6
|
+
{ strategy: 'avoid-split-50', reservePct: 0.2, keepPct: 0.5 },
|
|
7
|
+
{ strategy: 'avoid-split-70', reservePct: 0.15, keepPct: 0.7 },
|
|
8
|
+
{ strategy: 'avoid-split-90', reservePct: 0.1, keepPct: 0.9 },
|
|
9
|
+
];
|
|
10
|
+
const MINIMAL_KEEP = {
|
|
11
|
+
strategy: 'minimal-keep',
|
|
12
|
+
reservePct: 0.5,
|
|
13
|
+
keepPct: 0.05,
|
|
14
|
+
};
|
|
15
|
+
function toTokens(pct, window, floor = 0) {
|
|
16
|
+
return Math.max(floor, Math.floor(window * pct));
|
|
17
|
+
}
|
|
18
|
+
function toError(err) {
|
|
19
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
20
|
+
}
|
|
21
|
+
async function runCompactionWithRecovery(io, modelContextWindow) {
|
|
22
|
+
const attempts = [];
|
|
23
|
+
try {
|
|
24
|
+
await io.compact();
|
|
25
|
+
attempts.push({ attempt: 1, strategy: 'default' });
|
|
26
|
+
return { succeeded: true, attempts };
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
attempts.push({ attempt: 1, strategy: 'default', error: toError(err) });
|
|
30
|
+
}
|
|
31
|
+
const lastError = () => attempts[attempts.length - 1]?.error;
|
|
32
|
+
if (lastError() && TURN_PREFIX_FAILURE_RE.test(lastError().message)) {
|
|
33
|
+
for (const step of AVOID_SPLIT_LADDER) {
|
|
34
|
+
try {
|
|
35
|
+
io.applySettings({
|
|
36
|
+
reserveTokens: toTokens(step.reservePct, modelContextWindow),
|
|
37
|
+
keepRecentTokens: toTokens(step.keepPct, modelContextWindow),
|
|
38
|
+
});
|
|
39
|
+
await io.compact();
|
|
40
|
+
attempts.push({ attempt: attempts.length + 1, strategy: step.strategy });
|
|
41
|
+
return { succeeded: true, attempts };
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
attempts.push({
|
|
45
|
+
attempt: attempts.length + 1,
|
|
46
|
+
strategy: step.strategy,
|
|
47
|
+
error: toError(err),
|
|
48
|
+
});
|
|
49
|
+
if (!TURN_PREFIX_FAILURE_RE.test(toError(err).message))
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
io.applySettings({
|
|
56
|
+
reserveTokens: toTokens(MINIMAL_KEEP.reservePct, modelContextWindow),
|
|
57
|
+
keepRecentTokens: toTokens(MINIMAL_KEEP.keepPct, modelContextWindow, 2_000),
|
|
58
|
+
});
|
|
59
|
+
await io.compact();
|
|
60
|
+
attempts.push({ attempt: attempts.length + 1, strategy: 'minimal-keep' });
|
|
61
|
+
return { succeeded: true, attempts };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const e = toError(err);
|
|
65
|
+
attempts.push({ attempt: attempts.length + 1, strategy: 'minimal-keep', error: e });
|
|
66
|
+
return { succeeded: false, attempts, finalError: e };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.planContextBudget = planContextBudget;
|
|
4
|
+
const FIXED_OVERHEAD_TOKENS = 2_000;
|
|
5
|
+
function estimateTokensFromChars(chars) {
|
|
6
|
+
return Math.ceil(chars / 4);
|
|
7
|
+
}
|
|
8
|
+
function planContextBudget(input) {
|
|
9
|
+
const { currentTokens, incomingChars, contextWindow, maxOutputTokens } = input;
|
|
10
|
+
const outputReserve = Math.min(maxOutputTokens, Math.floor(contextWindow * 0.2));
|
|
11
|
+
const budget = contextWindow - outputReserve - FIXED_OVERHEAD_TOKENS;
|
|
12
|
+
const incomingTokens = estimateTokensFromChars(incomingChars);
|
|
13
|
+
const projectedTokens = currentTokens + incomingTokens;
|
|
14
|
+
if (incomingTokens + FIXED_OVERHEAD_TOKENS + outputReserve >= contextWindow) {
|
|
15
|
+
return {
|
|
16
|
+
projectedTokens,
|
|
17
|
+
budget,
|
|
18
|
+
action: 'abort',
|
|
19
|
+
reason: `Incoming message (~${incomingTokens} tok) plus output reservation (${outputReserve}) exceeds the model's ${contextWindow}-token window on its own. Send a shorter message or switch to a larger-context model.`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (projectedTokens <= budget) {
|
|
23
|
+
return {
|
|
24
|
+
projectedTokens,
|
|
25
|
+
budget,
|
|
26
|
+
action: 'send',
|
|
27
|
+
reason: `Projected ${projectedTokens} tok ≤ budget ${budget} (window ${contextWindow}, output reserve ${outputReserve}).`,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
projectedTokens,
|
|
32
|
+
budget,
|
|
33
|
+
action: 'compact',
|
|
34
|
+
reason: `Projected ${projectedTokens} tok > budget ${budget} (window ${contextWindow}, output reserve ${outputReserve}). Compaction required before send.`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.assembleAssistantMessage = assembleAssistantMessage;
|
|
4
|
+
function assembleAssistantMessage(chunks) {
|
|
5
|
+
let out = '';
|
|
6
|
+
let textRunHasContent = false;
|
|
7
|
+
let pendingToolSeparator = false;
|
|
8
|
+
for (const chunk of chunks) {
|
|
9
|
+
if (chunk.type === 'text') {
|
|
10
|
+
if (!chunk.text)
|
|
11
|
+
continue;
|
|
12
|
+
if (pendingToolSeparator && textRunHasContent) {
|
|
13
|
+
out += '\n\n';
|
|
14
|
+
}
|
|
15
|
+
out += chunk.text;
|
|
16
|
+
textRunHasContent = true;
|
|
17
|
+
pendingToolSeparator = false;
|
|
18
|
+
}
|
|
19
|
+
else if (chunk.type === 'tool_call') {
|
|
20
|
+
if (textRunHasContent)
|
|
21
|
+
pendingToolSeparator = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|
|
@@ -1,20 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* agent/process.ts
|
|
4
|
-
*
|
|
5
|
-
* Standalone agent process — spawned by the orchestrator, one per agent.
|
|
6
|
-
*
|
|
7
|
-
* Architecture:
|
|
8
|
-
* WS server → receives messages → [BB evaluate] → enqueue()
|
|
9
|
-
* Queue worker (poll loop) → dequeueNext() → runAgent() → broadcastToChannel chunks
|
|
10
|
-
*
|
|
11
|
-
* WebSocket protocol:
|
|
12
|
-
* Client → Agent: { type: 'message', text: string, channelId?: string }
|
|
13
|
-
* Agent → Client: { type: 'queued' }
|
|
14
|
-
* { type: 'chunk', chunk: StreamChunk }
|
|
15
|
-
* { type: 'error', message: string }
|
|
16
|
-
* { type: 'blocked', reason: string }
|
|
17
|
-
*/
|
|
18
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
4
|
};
|
|
@@ -26,9 +10,11 @@ const path_1 = __importDefault(require("path"));
|
|
|
26
10
|
const config_js_1 = require("../config.js");
|
|
27
11
|
const agent_db_js_1 = require("../agent-db.js");
|
|
28
12
|
const runner_pi_js_1 = require("./runner-pi.js");
|
|
13
|
+
const channel_broadcast_js_1 = require("./channel-broadcast.js");
|
|
29
14
|
const messages_db_js_1 = require("../messages-db.js");
|
|
30
15
|
const telegram_adapter_js_1 = require("./telegram-adapter.js");
|
|
31
16
|
const browser_sessions_js_1 = require("../browser-sessions.js");
|
|
17
|
+
const message_assembly_js_1 = require("./message-assembly.js");
|
|
32
18
|
const takeover_state_js_1 = require("../takeover-state.js");
|
|
33
19
|
const takeover_timeout_js_1 = require("../takeover-timeout.js");
|
|
34
20
|
const agentId = process.env.AGENT_ID;
|
|
@@ -37,7 +23,6 @@ if (!agentId || !port) {
|
|
|
37
23
|
console.error('[agent/process] AGENT_ID and AGENT_PORT env vars are required');
|
|
38
24
|
process.exit(1);
|
|
39
25
|
}
|
|
40
|
-
// ── Main ───────────────────────────────────────────────────────────────────────
|
|
41
26
|
function main() {
|
|
42
27
|
const agent = (0, config_js_1.getAgent)(agentId);
|
|
43
28
|
if (!agent) {
|
|
@@ -45,21 +30,17 @@ function main() {
|
|
|
45
30
|
process.exit(1);
|
|
46
31
|
}
|
|
47
32
|
const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
|
|
48
|
-
|
|
33
|
+
(0, runner_pi_js_1.bootstrapWorkspace)(workspaceDir, agentId);
|
|
49
34
|
const cleaned = (0, agent_db_js_1.cleanupStaleJobs)(workspaceDir);
|
|
50
35
|
if (cleaned > 0)
|
|
51
36
|
console.log(`[agent:${agentId}] cleaned up ${cleaned} stale processing jobs`);
|
|
52
|
-
// ── WebSocket server ───────────────────────────────────────────────────────
|
|
53
37
|
const wss = new ws_1.WebSocketServer({ port });
|
|
54
|
-
// Map from channelId → set of WS clients subscribed to that channel
|
|
55
38
|
const channelClients = new Map();
|
|
56
39
|
function getChannelClients(channelId) {
|
|
57
|
-
|
|
58
|
-
channelClients.set(channelId, new Set());
|
|
59
|
-
return channelClients.get(channelId);
|
|
40
|
+
return (0, channel_broadcast_js_1.getOrCreateChannelSet)(channelClients, channelId);
|
|
60
41
|
}
|
|
61
42
|
wss.on('connection', (ws) => {
|
|
62
|
-
let clientChannelId = 'ui';
|
|
43
|
+
let clientChannelId = 'ui';
|
|
63
44
|
console.log(`[agent:${agentId}] client connected`);
|
|
64
45
|
ws.on('message', (raw) => {
|
|
65
46
|
let msg;
|
|
@@ -76,8 +57,6 @@ function main() {
|
|
|
76
57
|
ws.send(JSON.stringify({ type: 'stopped', killed: stopped }));
|
|
77
58
|
}
|
|
78
59
|
else if (msg.type === 'subscribe' && msg.channelId) {
|
|
79
|
-
// Subscribe this WS client to a channel without sending a message.
|
|
80
|
-
// Used by the frontend to receive live chunks from scheduled runs.
|
|
81
60
|
clientChannelId = msg.channelId;
|
|
82
61
|
getChannelClients(clientChannelId).add(ws);
|
|
83
62
|
}
|
|
@@ -89,7 +68,6 @@ function main() {
|
|
|
89
68
|
}
|
|
90
69
|
});
|
|
91
70
|
ws.on('close', () => {
|
|
92
|
-
// Remove from whichever channel set it was in
|
|
93
71
|
for (const [id, set] of channelClients.entries()) {
|
|
94
72
|
set.delete(ws);
|
|
95
73
|
if (set.size === 0)
|
|
@@ -99,34 +77,24 @@ function main() {
|
|
|
99
77
|
});
|
|
100
78
|
});
|
|
101
79
|
console.log(`[agent:${agentId}] WS listening on ws://localhost:${port}`);
|
|
102
|
-
// ── Telegram adapter ───────────────────────────────────────────────────────
|
|
103
|
-
// Started automatically if TELEGRAM_BOT_TOKEN is set (via Secrets in the UI).
|
|
104
|
-
// The user adds TELEGRAM_BOT_TOKEN as a secret → orchestrator injects it as
|
|
105
|
-
// an env var when spawning this process → adapter picks it up here.
|
|
106
80
|
let telegramAdapter = null;
|
|
107
81
|
const telegramBotToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
108
82
|
if (telegramBotToken) {
|
|
109
83
|
telegramAdapter = new telegram_adapter_js_1.TelegramAdapter(agentId, telegramBotToken, workspaceDir);
|
|
110
84
|
}
|
|
111
|
-
// ── Queue worker ───────────────────────────────────────────────────────────
|
|
112
85
|
function broadcastToChannel(channelId, data) {
|
|
113
|
-
const json = JSON.stringify(data);
|
|
114
86
|
const targets = channelClients.get(channelId);
|
|
115
87
|
if (!targets)
|
|
116
88
|
return;
|
|
117
|
-
|
|
118
|
-
if (ws.readyState === ws_1.WebSocket.OPEN)
|
|
119
|
-
ws.send(json);
|
|
120
|
-
}
|
|
89
|
+
(0, channel_broadcast_js_1.broadcastToClients)(targets, data);
|
|
121
90
|
}
|
|
122
|
-
// Track busy state per channel type so UI chat can run while workflows/schedules execute
|
|
123
91
|
const busyChannels = new Set();
|
|
124
92
|
function channelType(channelId) {
|
|
125
93
|
if (channelId.startsWith('wf-'))
|
|
126
94
|
return 'workflow';
|
|
127
95
|
if (channelId === 'schedule')
|
|
128
96
|
return 'schedule';
|
|
129
|
-
return channelId;
|
|
97
|
+
return channelId;
|
|
130
98
|
}
|
|
131
99
|
async function processNext() {
|
|
132
100
|
const job = (0, agent_db_js_1.dequeueNext)(workspaceDir, agentId, busyChannels);
|
|
@@ -136,41 +104,56 @@ function main() {
|
|
|
136
104
|
busyChannels.add(lane);
|
|
137
105
|
try {
|
|
138
106
|
const isTelegramJob = telegramAdapter !== null && job.channelId.startsWith('telegram:');
|
|
139
|
-
// Save the prompt so it's visible in run history immediately
|
|
140
107
|
try {
|
|
141
108
|
(0, messages_db_js_1.saveMessage)({ id: (0, crypto_1.randomUUID)(), agentId: agentId, channelId: job.channelId, role: 'user', content: job.message });
|
|
142
109
|
}
|
|
143
|
-
catch {
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
// tool_call rows are persisted to the DB the moment they arrive
|
|
147
|
-
// (not batched at turn end). Reason: if a user leaves the chat view
|
|
148
|
-
// mid-turn and navigates to /dashboard, ChatPage unmounts and loses
|
|
149
|
-
// its in-memory streaming state. On return, it refetches history
|
|
150
|
-
// from the DB — if tool_calls were still buffered in memory, the
|
|
151
|
-
// user would see an empty chat while the agent was clearly still
|
|
152
|
-
// working. Persisting as-they-happen makes the live state
|
|
153
|
-
// refetchable. See regression A (view-switch-state.spec.ts).
|
|
154
|
-
let fullResponse = '';
|
|
110
|
+
catch { }
|
|
111
|
+
const chunkBuffer = [];
|
|
155
112
|
let toolCallCount = 0;
|
|
156
|
-
// Inject context message if a human takeover was pending
|
|
157
113
|
let messageText = job.message;
|
|
158
114
|
if ((0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
159
|
-
(0, takeover_state_js_1.cancelTakeoverTimer)(agentId);
|
|
115
|
+
(0, takeover_state_js_1.cancelTakeoverTimer)(agentId);
|
|
160
116
|
messageText =
|
|
161
117
|
`[User completed browser interaction]\n` +
|
|
162
118
|
`User said: "${job.message}"`;
|
|
163
119
|
}
|
|
120
|
+
if (job.channelId.startsWith('telegram:')) {
|
|
121
|
+
const proactive = (0, messages_db_js_1.getProactiveMessagesSinceLastUser)(agentId, job.channelId);
|
|
122
|
+
if (proactive.length > 0) {
|
|
123
|
+
const lines = proactive.map((m, i) => {
|
|
124
|
+
const ts = new Date(m.createdAt).toISOString().slice(11, 16) + ' UTC';
|
|
125
|
+
return ` ${i + 1}. [${ts}] ${m.content}`;
|
|
126
|
+
}).join('\n');
|
|
127
|
+
messageText =
|
|
128
|
+
`[System: since your last Telegram exchange you proactively sent the user these messages ` +
|
|
129
|
+
`(they are not in your conversation history but the user received them):\n${lines}\n` +
|
|
130
|
+
`]\n\n` +
|
|
131
|
+
`User's new reply: ${messageText}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
164
134
|
await (0, runner_pi_js_1.runAgent)(agent, messageText, (chunk) => {
|
|
165
135
|
broadcastToChannel(job.channelId, { type: 'chunk', chunk });
|
|
166
136
|
if (chunk.type === 'text') {
|
|
167
|
-
|
|
137
|
+
chunkBuffer.push({ type: 'text', text: chunk.text });
|
|
168
138
|
if (isTelegramJob) {
|
|
169
139
|
telegramAdapter.appendChunk(job.channelId, chunk.text);
|
|
170
140
|
}
|
|
171
141
|
}
|
|
142
|
+
if (chunk.type === 'takeover_requested') {
|
|
143
|
+
const takeoverUrl = chunk.takeoverUrl;
|
|
144
|
+
if (isTelegramJob) {
|
|
145
|
+
telegramAdapter.notifyTakeover(job.channelId, takeoverUrl);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
broadcastToChannel(job.channelId, {
|
|
149
|
+
type: 'chunk',
|
|
150
|
+
chunk: { type: 'text', text: `\n\n🔗 **Takeover link:** ${takeoverUrl}\n\n` },
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
172
154
|
if (chunk.type === 'tool_call') {
|
|
173
155
|
const tcString = `${chunk.tool}(${JSON.stringify(chunk.input)})`;
|
|
156
|
+
chunkBuffer.push({ type: 'tool_call', tool: chunk.tool, input: chunk.input });
|
|
174
157
|
toolCallCount++;
|
|
175
158
|
try {
|
|
176
159
|
(0, messages_db_js_1.saveMessage)({
|
|
@@ -181,16 +164,13 @@ function main() {
|
|
|
181
164
|
content: tcString,
|
|
182
165
|
});
|
|
183
166
|
}
|
|
184
|
-
catch {
|
|
167
|
+
catch { }
|
|
185
168
|
if (isTelegramJob) {
|
|
186
|
-
// Live status update — appears in the user's chat as the
|
|
187
|
-
// acknowledgment message gets edited to show progress.
|
|
188
169
|
void telegramAdapter.appendToolStep(job.channelId, chunk.tool);
|
|
189
170
|
}
|
|
190
171
|
}
|
|
191
172
|
}, { channelId: job.channelId });
|
|
192
|
-
|
|
193
|
-
// saved one-by-one above, so no batch here.
|
|
173
|
+
const fullResponse = (0, message_assembly_js_1.assembleAssistantMessage)(chunkBuffer);
|
|
194
174
|
try {
|
|
195
175
|
if (fullResponse) {
|
|
196
176
|
(0, messages_db_js_1.saveMessage)({
|
|
@@ -199,13 +179,12 @@ function main() {
|
|
|
199
179
|
channelId: job.channelId,
|
|
200
180
|
role: 'assistant',
|
|
201
181
|
content: fullResponse,
|
|
202
|
-
createdAt: Date.now() + toolCallCount,
|
|
182
|
+
createdAt: Date.now() + toolCallCount,
|
|
203
183
|
});
|
|
204
184
|
}
|
|
205
185
|
}
|
|
206
|
-
catch {
|
|
186
|
+
catch { }
|
|
207
187
|
(0, agent_db_js_1.markDone)(workspaceDir, job.id);
|
|
208
|
-
// Arm 10-minute timeout if the agent registered a takeover during this run
|
|
209
188
|
if ((0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
210
189
|
const timer = setTimeout(() => {
|
|
211
190
|
(0, takeover_timeout_js_1.handleTakeoverTimeout)(agentId, workspaceDir).catch((err) => {
|
|
@@ -214,14 +193,9 @@ function main() {
|
|
|
214
193
|
}, takeover_state_js_1.TAKEOVER_TIMEOUT_MS);
|
|
215
194
|
(0, takeover_state_js_1.updateTakeoverTimer)(agentId, timer);
|
|
216
195
|
}
|
|
217
|
-
// Belt-and-suspenders: if the agent left a browser session open (e.g.
|
|
218
|
-
// forgot to call close), finalize it so recordings don't stay "active"
|
|
219
|
-
// forever and stream subscribers detach cleanly.
|
|
220
|
-
// Skip if a human takeover is pending — browser session must stay alive
|
|
221
196
|
if (!(0, takeover_state_js_1.hasTakeover)(agentId)) {
|
|
222
197
|
(0, browser_sessions_js_1.forceCloseActiveSession)(agentId);
|
|
223
198
|
}
|
|
224
|
-
// Send the full reply back to Telegram once the turn is complete
|
|
225
199
|
if (isTelegramJob) {
|
|
226
200
|
await telegramAdapter.flushReply(job.channelId);
|
|
227
201
|
}
|