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.
Files changed (70) hide show
  1. package/dist/backend/agent/browser-bin.js +62 -0
  2. package/dist/backend/agent/channel-broadcast.js +25 -0
  3. package/dist/backend/agent/compaction-retry.js +68 -0
  4. package/dist/backend/agent/context-budget.js +36 -0
  5. package/dist/backend/agent/message-assembly.js +25 -0
  6. package/dist/backend/agent/process.js +42 -68
  7. package/dist/backend/agent/runner-pi.js +462 -218
  8. package/dist/backend/agent/telegram-adapter.js +17 -71
  9. package/dist/backend/agent/telegram-chats.js +56 -0
  10. package/dist/backend/agent/telegram-http-client.js +2 -27
  11. package/dist/backend/agent/telegram-markdown.js +46 -0
  12. package/dist/backend/agent-db.js +0 -14
  13. package/dist/backend/app-config.js +26 -0
  14. package/dist/backend/app-secrets.js +99 -0
  15. package/dist/backend/assets/stealth-extension/stealth.js +206 -11
  16. package/dist/backend/browser/session-manager.js +41 -89
  17. package/dist/backend/browser/stealth.js +52 -64
  18. package/dist/backend/browser-sessions.js +52 -45
  19. package/dist/backend/config.js +0 -17
  20. package/dist/backend/data-db.js +0 -14
  21. package/dist/backend/esm-import.js +0 -18
  22. package/dist/backend/extensions/loader.js +90 -0
  23. package/dist/backend/extensions/types.js +2 -0
  24. package/dist/backend/index.js +30 -5
  25. package/dist/backend/integrations/agent-integrations-db.js +53 -0
  26. package/dist/backend/integrations/registry.js +64 -0
  27. package/dist/backend/integrations/routes.js +42 -0
  28. package/dist/backend/integrations/types.js +2 -0
  29. package/dist/backend/lib/flatten-markdown-tables.js +81 -0
  30. package/dist/backend/lib/i18n-telegram.js +35 -35
  31. package/dist/backend/logs-db.js +0 -8
  32. package/dist/backend/messages-db.js +16 -8
  33. package/dist/backend/orchestrator/agent-manager.js +4 -32
  34. package/dist/backend/orchestrator/browser-live.js +364 -217
  35. package/dist/backend/orchestrator/server.js +160 -210
  36. package/dist/backend/providers-config.js +67 -27
  37. package/dist/backend/routes/logs.js +0 -1
  38. package/dist/backend/scheduler.js +1 -8
  39. package/dist/backend/schedules-db.js +0 -12
  40. package/dist/backend/secrets-vault.js +0 -6
  41. package/dist/backend/takeover-listeners.js +27 -0
  42. package/dist/backend/takeover-messages.js +0 -31
  43. package/dist/backend/takeover-state.js +12 -7
  44. package/dist/backend/takeover-timeout.js +1 -25
  45. package/dist/backend/takeover-url-resolver.js +29 -0
  46. package/dist/backend/tasks-db.js +0 -11
  47. package/dist/backend/telemetry.js +30 -0
  48. package/dist/backend/usage-scanner.js +1 -18
  49. package/dist/backend/workflows/runner.js +2 -28
  50. package/dist/backend/workflows-db.js +37 -14
  51. package/dist/backend/workspace-pool.js +24 -19
  52. package/dist/frontend/assets/index-Hymtb-e4.css +1 -0
  53. package/dist/frontend/assets/index-gSUjRoId.js +158 -0
  54. package/dist/frontend/index.html +2 -2
  55. package/dist/home.js +20 -0
  56. package/dist/index.js +14 -0
  57. package/dist/telemetry.js +37 -0
  58. package/package.json +9 -4
  59. package/templates/AGENT.onboarding.md +19 -2
  60. package/templates/SYSTEM.md +88 -8
  61. package/templates/skills/email/SKILL.md +300 -0
  62. package/templates/skills/email/gmcli.sh +55 -0
  63. package/templates/skills/email/read-imap.py +231 -0
  64. package/templates/skills/email/send-smtp.py +104 -0
  65. package/templates/skills/skill-creator/SKILL.md +188 -0
  66. package/templates/skills/whatsapp/SKILL.md +147 -0
  67. package/templates/skills/whatsapp/whatsapp.sh +20 -0
  68. package/templates/skills/workflows/SKILL.md +57 -0
  69. package/dist/frontend/assets/index-BmQN0cOF.css +0 -1
  70. 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
- // Clean up stale 'processing' jobs from previous crashes/restarts
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
- if (!channelClients.has(channelId))
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'; // default until client sends a message with channelId
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
- for (const ws of targets) {
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; // 'ui', 'telegram:xxx' — each is its own lane
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 { /* non-fatal */ }
144
- // Stream chunks directly to channel clients.
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); // stop 10min timeout — entry stays for runner-pi to restore handle
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
- fullResponse += chunk.text;
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 { /* non-fatal — WAL/locking can fail under parallel writes */ }
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
- // Persist the final assistant message. tool_call rows were already
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, // ordered after the last tool_call
182
+ createdAt: Date.now() + toolCallCount,
203
183
  });
204
184
  }
205
185
  }
206
- catch { /* non-fatal */ }
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
  }