@coda-console/grok-bot 0.1.7 → 0.2.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/README.md +40 -50
- package/agent/auth.mjs +88 -0
- package/agent/health.mjs +23 -0
- package/agent/native.mjs +73 -0
- package/agent/routines.mjs +104 -0
- package/agent/session.mjs +292 -78
- package/agent/setup.mjs +4 -3
- package/agent/watch.mjs +143 -0
- package/package.json +28 -27
- package/remote/SKILL.md +12 -22
- package/remote/coda-bot.mjs +5 -13
package/agent/session.mjs
CHANGED
|
@@ -3,6 +3,9 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { CodaMcp } from './mcp.mjs';
|
|
5
5
|
import { bindingGuide } from './setup.mjs';
|
|
6
|
+
import { NativeAuth } from './auth.mjs';
|
|
7
|
+
import { NativeWatch } from './watch.mjs';
|
|
8
|
+
import { RoutineWatch } from './routines.mjs';
|
|
6
9
|
|
|
7
10
|
const MODEL = { id: 'model', name: '模型', category: 'model', type: 'select', currentValue: 'grok-bot', options: [{ value: 'grok-bot', name: 'Grok Bot' }] };
|
|
8
11
|
const READ = new Set(['read_thread', 'read_tool_call', 'agents', 'status', 'workflow_status', 'workflow_list', 'machines', 'machine_diagnostics']);
|
|
@@ -10,11 +13,7 @@ const WRITE = new Set(['new_session', 'send']);
|
|
|
10
13
|
const DIAGNOSTICS = new Set(['machines', 'machine_diagnostics']);
|
|
11
14
|
const fail = message => { throw Object.assign(new Error(message), { code: -32602 }); };
|
|
12
15
|
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
-
|
|
14
|
-
if (/^\s*(?:Child session completed:|\[Coda\] (?:以下子会话(?:已结束|交回结果)|以下会话回复|你请求(?:派到其他(?:项目|目录)的子会话|发给其他会话的消息)))/u.test(text)) return 'result';
|
|
15
|
-
if (/^\s*(?:\[Coda\]\s*)?(?:已开出|用户拒绝了|未能开出|这张卡片已失效,没有开出)[::]/u.test(text)) return 'notification';
|
|
16
|
-
return /^\s*\[Coda\]/u.test(text) ? 'notification' : 'message';
|
|
17
|
-
}
|
|
16
|
+
export const SETTLEMENT_GRACE_MS = 5000;
|
|
18
17
|
export const SUBMISSION_WAIT_MS = 20_000;
|
|
19
18
|
function canonical(value) {
|
|
20
19
|
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
@@ -43,10 +42,12 @@ function url(value) {
|
|
|
43
42
|
}
|
|
44
43
|
|
|
45
44
|
export class Adapter {
|
|
46
|
-
constructor({ rpc, env = process.env, fetcher = fetch, clock = { setTimeout, clearTimeout }, submissionWaitMs = SUBMISSION_WAIT_MS, log = text => console.error(text), mcpFactory = (server, cwd) => new CodaMcp(server, cwd) }) {
|
|
45
|
+
constructor({ rpc, env = process.env, fetcher = fetch, clock = { setTimeout, clearTimeout, now: Date.now }, submissionWaitMs = SUBMISSION_WAIT_MS, log = text => console.error(text), mcpFactory = (server, cwd) => new CodaMcp(server, cwd) }) {
|
|
47
46
|
Object.assign(this, { rpc, env, fetcher, clock, submissionWaitMs, log, mcpFactory });
|
|
48
47
|
this.settings = JSON.parse(env.CODA_PLUGIN_SETTINGS || '{}');
|
|
49
|
-
this.
|
|
48
|
+
this.now = () => this.clock.now ? this.clock.now() : Date.now();
|
|
49
|
+
this.auth = new NativeAuth({ root: env.CODA_PLUGIN_STATE, fetcher, clock });
|
|
50
|
+
this.bots = [];
|
|
50
51
|
this.sessions = new Map();
|
|
51
52
|
this.timeout = duration(this.settings['grok-bot.turnTimeout']);
|
|
52
53
|
}
|
|
@@ -62,11 +63,16 @@ export class Adapter {
|
|
|
62
63
|
}
|
|
63
64
|
session(id) { return this.sessions.get(id) ?? fail('会话尚未加载'); }
|
|
64
65
|
configs(s) {
|
|
65
|
-
const bots = Object.keys(this.settings['grok-bot.bots'] ?? {});
|
|
66
66
|
return [MODEL, { id: 'bot', name: '这是哪个 Bot', type: 'select', currentValue: s.saved.bot,
|
|
67
|
-
options: [{ value: '', name: '请选择 Bot' }, ...
|
|
67
|
+
options: [{ value: '', name: '请选择 Bot(先发送 /login 登录)' }, ...this.bots.map(bot => ({ value: bot.agentId, name: `${bot.name || bot.agentId} (${bot.agentId})` })),
|
|
68
|
+
...(!this.bots.some(b => b.agentId === s.saved.bot) && s.saved.bot ? [{ value: s.saved.bot, name: s.saved.bot }] : [])] }];
|
|
69
|
+
}
|
|
70
|
+
wait(s, state) { if (this.waits) this.rpc.notify('_coda/wait', { sessionId: s.id, waitId: s.subscriptionId ?? 'grok-native', kind: 'task', title: 'Grok 主会话订阅', state }); }
|
|
71
|
+
async refreshBots() {
|
|
72
|
+
if (!this.auth.client) return;
|
|
73
|
+
const result = await this.auth.client.request('ListGrokBotAgents', {});
|
|
74
|
+
this.bots = result.agents ?? [];
|
|
68
75
|
}
|
|
69
|
-
wait(s, state) { if (this.waits) this.rpc.notify('_coda/wait', { sessionId: s.id, waitId: 'grok-bot-routines', kind: 'timer', recurring: true, title: 'Grok Bot 的定时任务', state }); }
|
|
70
76
|
update(s, update) { this.rpc.notify('session/update', { sessionId: s.id, update }); }
|
|
71
77
|
say(s, text) { this.update(s, { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text } }); }
|
|
72
78
|
start(s, autonomous) {
|
|
@@ -74,12 +80,13 @@ export class Adapter {
|
|
|
74
80
|
if (autonomous && !this.waits) fail('Coda 未声明 waits,不能开启自主轮次');
|
|
75
81
|
const turn = { id: randomUUID(), autonomous, controller: new AbortController(), tools: new Map() };
|
|
76
82
|
turn.result = new Promise(resolve => { turn.resolve = resolve; });
|
|
77
|
-
|
|
83
|
+
|
|
78
84
|
s.turn = turn;
|
|
79
85
|
if (autonomous) this.wait(s, 'handling');
|
|
80
86
|
turn.timer = this.clock.setTimeout(() => {
|
|
81
87
|
if (s.turn !== turn) return;
|
|
82
|
-
|
|
88
|
+
turn.failed = true;
|
|
89
|
+
this.say(s, `等待 Grok Bot 超过 ${this.settings['grok-bot.turnTimeout'] ?? 'PT30M'},本轮已结束;云端任务未必停止。之后收到的原生消息将标为补充消息。`);
|
|
83
90
|
this.finish(s, 'end_turn');
|
|
84
91
|
}, this.timeout);
|
|
85
92
|
return turn;
|
|
@@ -90,15 +97,16 @@ export class Adapter {
|
|
|
90
97
|
for (const [toolCallId, call] of turn.tools) if (!call.background)
|
|
91
98
|
this.update(s, { sessionUpdate: 'tool_call_update', toolCallId, status: 'failed', rawOutput: '本轮已结束,远端操作结果未确认' });
|
|
92
99
|
this.clock.clearTimeout(turn.timer);
|
|
100
|
+
this.clock.clearTimeout(turn.settlementTimer);
|
|
101
|
+
if (turn.activityId) this.update(s, { sessionUpdate: 'tool_call_update', toolCallId: turn.activityId, status: stopReason === 'cancelled' || turn.failed ? 'failed' : 'completed' });
|
|
102
|
+
if (turn.nativeId) s.closedRuns.add(turn.nativeId);
|
|
93
103
|
s.turn = null;
|
|
94
104
|
turn.controller.abort();
|
|
95
|
-
if (turn.autonomous) this.wait(s, 'waiting');
|
|
105
|
+
if (turn.autonomous) this.wait(s, s.watch ? 'waiting' : 'done');
|
|
96
106
|
turn.resolve({ stopReason });
|
|
97
107
|
}
|
|
98
|
-
active(s,
|
|
99
|
-
if (
|
|
100
|
-
// Webhook runs must carry their ID, including after a cancellation and a new prompt.
|
|
101
|
-
if (turn === undefined && s.turn && !s.turn.autonomous) fail('处理 Coda 消息必须带 Webhook 中的 turn');
|
|
108
|
+
active(s, id, create = false) {
|
|
109
|
+
if (id !== undefined && id !== s.turn?.id) fail('轮次已结束或不匹配');
|
|
102
110
|
if (!s.turn && create) return this.start(s, true);
|
|
103
111
|
return s.turn;
|
|
104
112
|
}
|
|
@@ -112,13 +120,13 @@ export class Adapter {
|
|
|
112
120
|
let saved = { bot: '', bound: false };
|
|
113
121
|
if (load) {
|
|
114
122
|
try {
|
|
115
|
-
|
|
116
|
-
saved
|
|
123
|
+
saved = JSON.parse(readFileSync(this.file(id), 'utf8'));
|
|
124
|
+
if (typeof saved.bot !== 'string' || typeof saved.bound !== 'boolean') fail('无效的 Bot 会话状态');
|
|
125
|
+
delete saved.instructions;
|
|
117
126
|
}
|
|
118
127
|
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
119
128
|
}
|
|
120
|
-
const
|
|
121
|
-
const s = { id, saved, instructions, diagnosticsEnabled: mcpServers.some(server => server.name === 'diagnostics'), clients: [], routes: new Map(), unavailable: new Map(), turn: null };
|
|
129
|
+
const s = { id, saved, sent: new Map(), closedRuns: new Set(), texts: new Map(), widgets: new Set(), diagnosticsEnabled: mcpServers.some(server => server.name === 'diagnostics'), clients: [], routes: new Map(), unavailable: new Map(), turn: null };
|
|
122
130
|
// Coda and the optional built-in diagnostics plugin are separate stdio MCP servers. Discover
|
|
123
131
|
// both at load time; never treat a tool name returned by a server as authority
|
|
124
132
|
// to expand the whitelist, or fall back to coda for absent diagnostics tools.
|
|
@@ -145,42 +153,254 @@ export class Adapter {
|
|
|
145
153
|
s.routes.set(tool.name, catalog.client);
|
|
146
154
|
}
|
|
147
155
|
this.sessions.set(id, s);
|
|
148
|
-
this.
|
|
156
|
+
if (this.auth.client) {
|
|
157
|
+
try { await this.refreshBots(); if (saved.bot) await this.attach(s); }
|
|
158
|
+
catch (error) { s.setupError = error.message; this.log(error.message); }
|
|
159
|
+
}
|
|
149
160
|
return { ...(load ? {} : { sessionId: id }), configOptions: this.configs(s) };
|
|
150
161
|
}
|
|
151
162
|
async prompt(s, prompt) {
|
|
152
|
-
if (!Array.isArray(prompt) || prompt.some(
|
|
153
|
-
const turn = this.start(s, false);
|
|
163
|
+
if (!Array.isArray(prompt) || prompt.some(b => b.type !== 'text' || typeof b.text !== 'string')) fail('Grok Bot 目前只接受文本');
|
|
154
164
|
const text = prompt.map(b => b.text).join('\n');
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
if (!text.trim()) fail('消息不能为空');
|
|
166
|
+
const turn = this.start(s, false);
|
|
157
167
|
void (async () => {
|
|
158
|
-
if (!
|
|
159
|
-
|
|
168
|
+
if (!this.auth.client || text.trim() === '/login') {
|
|
169
|
+
this.beginLogin(s); this.finish(s, 'end_turn'); return;
|
|
170
|
+
}
|
|
171
|
+
if (text.trim() === '/bind') {
|
|
172
|
+
if (!this.remote) throw new Error('请升级到支持远端 Agent 入口的 Coda');
|
|
160
173
|
const code = await this.rpc.request('_coda/remote/code', { sessionId: s.id }, { signal: turn.controller.signal });
|
|
161
|
-
if (s.turn
|
|
162
|
-
this.say(s, bindingGuide(s.id, code));
|
|
163
|
-
this.finish(s, 'end_turn');
|
|
174
|
+
if (s.turn === turn) { this.say(s, bindingGuide(s.id, code)); this.finish(s, 'end_turn'); }
|
|
164
175
|
return;
|
|
165
176
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
|
|
170
|
-
if (!key) throw new Error('这个 Bot 的 Webhook key 为空');
|
|
171
|
-
const characters = [...text];
|
|
172
|
-
const preview = characters.slice(0, 500).join('') + (characters.length > 500 ? '……(完整内容用 coda_input 取)' : '');
|
|
173
|
-
const response = await this.fetcher(url(bot.webhook), { method: 'POST', redirect: 'error', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
|
|
174
|
-
body: JSON.stringify({ source: 'coda', sessionId: s.id, turn: turn.id, kind: s.input.kind, preview }), signal: turn.controller.signal });
|
|
175
|
-
if (!response.ok) throw new Error(`Grok Webhook 返回 HTTP ${response.status};未确认云端是否开始处理,不自动重发。`);
|
|
176
|
-
await response.body?.cancel();
|
|
177
|
-
})().catch(error => {
|
|
178
|
-
if (s.turn !== turn) return;
|
|
179
|
-
this.say(s, `消息未能完成:${error.message}`);
|
|
180
|
-
this.finish(s, 'end_turn');
|
|
181
|
-
});
|
|
177
|
+
if (!s.saved.bot) { this.say(s, '请在「这是哪个 Bot」中按名字或 ID 选择 Bot,再发送消息。'); this.finish(s, 'end_turn'); return; }
|
|
178
|
+
if (!s.watch) await this.attach(s);
|
|
179
|
+
if (s.turn === turn) await this.sendNative(s, turn, text);
|
|
180
|
+
})().catch(error => { if (s.turn === turn) { this.say(s, `消息未能完成:${error.message};未自动重发。认证失效请发送 /login。`); this.finish(s, 'end_turn'); } });
|
|
182
181
|
return turn.result;
|
|
183
182
|
}
|
|
183
|
+
beginLogin(s) {
|
|
184
|
+
const login = this.auth.login();
|
|
185
|
+
this.say(s, `请打开官方登录链接:${login.url}\n手机可能停在「Completing sign-in」,请再点一次 Continue。登录完成后会自动显示 Bot 列表。`);
|
|
186
|
+
if (this.loginTask) return;
|
|
187
|
+
const waitId = `grok-login-${randomUUID()}`;
|
|
188
|
+
this.rpc.notify('_coda/wait', { sessionId: s.id, waitId, kind: 'task', state: 'waiting', title: '等待 Grok 官方登录' });
|
|
189
|
+
this.loginTask = login.promise.then(async () => {
|
|
190
|
+
await this.refreshBots();
|
|
191
|
+
for (const session of this.sessions.values()) {
|
|
192
|
+
this.update(session, { sessionUpdate: 'config_option_update', configOptions: this.configs(session) });
|
|
193
|
+
if (session.saved.bot) { session.routines?.close(); session.watch?.close(); this.wait(session, 'done'); session.watch = null; await this.attach(session); }
|
|
194
|
+
}
|
|
195
|
+
this.rpc.notify('_coda/wait', { sessionId: s.id, waitId, state: 'handling' });
|
|
196
|
+
this.say(s, 'Grok 登录成功,请在「这是哪个 Bot」选择 Bot。');
|
|
197
|
+
this.rpc.notify('_coda/wait', { sessionId: s.id, waitId, state: 'done' });
|
|
198
|
+
}).catch(error => {
|
|
199
|
+
if (this.closed) return;
|
|
200
|
+
this.rpc.notify('_coda/wait', { sessionId: s.id, waitId, state: 'handling' });
|
|
201
|
+
this.say(s, `登录未完成:${error.message};请发送 /login 重试。`);
|
|
202
|
+
this.rpc.notify('_coda/wait', { sessionId: s.id, waitId, state: 'failed' });
|
|
203
|
+
}).finally(() => { this.loginTask = null; });
|
|
204
|
+
}
|
|
205
|
+
nativeNotice(s, text) {
|
|
206
|
+
const own = !s.turn; if (own) this.start(s, true);
|
|
207
|
+
this.say(s, text);
|
|
208
|
+
if (own) this.finish(s, 'end_turn');
|
|
209
|
+
}
|
|
210
|
+
healthCallbacks(s, source) {
|
|
211
|
+
return {
|
|
212
|
+
log: message => this.log(JSON.stringify({ sessionId: s.id, source, message })),
|
|
213
|
+
error: error => this.nativeNotice(s, `Grok ${source}持续失败:${error.message}。认证失效请发送 /login;其他断线按游标重连,不重发消息。`),
|
|
214
|
+
recovered: () => this.nativeNotice(s, `Grok ${source}已恢复。`),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
async attach(s) {
|
|
218
|
+
if (!this.waits) throw new Error('当前 Coda 不支持 waits,无法保持原生订阅与自主轮次');
|
|
219
|
+
s.subscriptionId = `grok-native-${randomUUID()}`;
|
|
220
|
+
this.wait(s, 'waiting');
|
|
221
|
+
const watch = new NativeWatch({ client: this.auth.client, bot: s.saved.bot, saved: s.saved, clock: this.clock,
|
|
222
|
+
commit: () => { if (s.watch === watch) this.save(s); }, event: event => { if (s.watch === watch) return this.nativeEvent(s, event); },
|
|
223
|
+
...this.healthCallbacks(s, '主会话订阅') });
|
|
224
|
+
s.watch = watch;
|
|
225
|
+
s.routines?.close();
|
|
226
|
+
s.routines = new RoutineWatch({ client: this.auth.client, bot: s.saved.bot, saved: s.saved, clock: this.clock,
|
|
227
|
+
commit: () => this.save(s), notify: event => this.rpc.notify('_coda/wait', { sessionId: s.id, ...event }),
|
|
228
|
+
...this.healthCallbacks(s, 'Routine 记录') });
|
|
229
|
+
try { await watch.start(); void s.routines.hint(); }
|
|
230
|
+
catch (error) { watch.close(); s.watch = null; this.wait(s, 'failed'); throw error; }
|
|
231
|
+
}
|
|
232
|
+
async sendNative(s, turn, text) {
|
|
233
|
+
turn.messageId = randomUUID();
|
|
234
|
+
s.sent.set(turn.messageId, turn.id);
|
|
235
|
+
const result = await this.auth.client.request('SendGrokBotUserMessage', { agentId: s.saved.bot, sessionId: '', messageId: turn.messageId, text,
|
|
236
|
+
sentAtMs: String(this.now()), source: 1, initiator: 1, sendSource: 1 }, { signal: turn.controller.signal });
|
|
237
|
+
if (!result.dispatched) throw new Error('Grok 未确认主会话开始执行');
|
|
238
|
+
}
|
|
239
|
+
async nativeEvent(s, event) {
|
|
240
|
+
if (this.closed) return;
|
|
241
|
+
if (event.connected) { if (s.connected) void s.routines.hint(); s.connected = true; return; }
|
|
242
|
+
if (event.routinesChanged) { void s.routines.hint(); return; }
|
|
243
|
+
if (event.entry) {
|
|
244
|
+
const { row, body } = event.entry;
|
|
245
|
+
if (body.kind === 'message' && body.role === 'user') {
|
|
246
|
+
if (s.sent.has(body.clientNonce)) {
|
|
247
|
+
if (s.sent.get(body.clientNonce) === s.turn?.id) s.turn.nativeId = body.requestId;
|
|
248
|
+
else s.closedRuns.add(body.requestId);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
if (s.turn?.settlement && s.turn.nativeId !== body.requestId) this.finish(s, 'end_turn');
|
|
252
|
+
if (!s.turn) this.start(s, true);
|
|
253
|
+
if (!s.turn.nativeId) s.turn.nativeId = body.requestId;
|
|
254
|
+
this.update(s, { sessionUpdate: 'user_message_chunk', content: { type: 'text', text: body.content ?? '' } });
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (body.kind !== 'send-message') return;
|
|
259
|
+
if (body.message?.type === 'widget' && (body.respondedValue !== undefined || body.widgetSkipped)) return;
|
|
260
|
+
const late = s.closedRuns.has(body.requestId);
|
|
261
|
+
if (!late && s.turn?.autonomous && s.turn.hasReply && s.turn.nativeId && s.turn.nativeId !== body.requestId) this.finish(s, 'end_turn');
|
|
262
|
+
if (!s.turn) {
|
|
263
|
+
this.start(s, true);
|
|
264
|
+
if (late) { this.say(s, 'Grok 上一轮补充消息:'); s.turn.settlement = { turnId: body.requestId, outcome: 'success', settledAtMs: String(this.now()) }; }
|
|
265
|
+
}
|
|
266
|
+
const turn = s.turn;
|
|
267
|
+
if (!turn.nativeId && !(late && turn.messageId)) turn.nativeId = body.requestId;
|
|
268
|
+
// Preserve late native output as an explicit supplement, never silently drop it.
|
|
269
|
+
if (body.requestId && turn.nativeId !== body.requestId) this.say(s, 'Grok 上一轮补充消息:');
|
|
270
|
+
if (turn.nativeId === body.requestId) {
|
|
271
|
+
turn.hasReply = true;
|
|
272
|
+
if (s.lastSettlement?.turnId === body.requestId)
|
|
273
|
+
(turn.pendingSettlements ??= new Map()).set(body.requestId, s.lastSettlement);
|
|
274
|
+
}
|
|
275
|
+
if (body.message?.type === 'text') {
|
|
276
|
+
const text = body.message.content ?? '', previous = s.texts.get(row.entryId) ?? '';
|
|
277
|
+
if (text !== previous) this.say(s, text.startsWith(previous) ? text.slice(previous.length) : `\nGrok 修订消息:${text}`);
|
|
278
|
+
s.texts.set(row.entryId, text);
|
|
279
|
+
} else if (body.message?.type === 'widget') {
|
|
280
|
+
if (!s.widgets.has(row.entryId)) { s.widgets.add(row.entryId); void this.widget(s, turn, row.entryId, body.message.widget); }
|
|
281
|
+
} else this.say(s, `Grok 返回未支持的消息类型 ${body.message?.type ?? 'unknown'},请在 Grok 客户端查看。`);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (event.live) {
|
|
285
|
+
const live = event.live;
|
|
286
|
+
if (live.hasRunningSubagents || s.hadSubagents) void s.routines.hint(!!live.hasRunningSubagents);
|
|
287
|
+
s.hadSubagents = !!live.hasRunningSubagents;
|
|
288
|
+
if (live.isRunning && !s.turn) this.start(s, true);
|
|
289
|
+
if (live.isRunning && s.turn) {
|
|
290
|
+
s.turn.nativeRunning = true;
|
|
291
|
+
if (live.activity?.kind === 'thinking' && !s.turn.thinking) {
|
|
292
|
+
s.turn.thinking = true;
|
|
293
|
+
this.update(s, { sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: 'Grok 正在思考(原生状态,未提供思考正文)' } });
|
|
294
|
+
}
|
|
295
|
+
if (live.activity?.kind === 'tool') {
|
|
296
|
+
const title = live.activity.detail || live.activity.tool || 'Grok 活动';
|
|
297
|
+
if (title !== s.turn.activityTitle) {
|
|
298
|
+
if (s.turn.activityId) this.update(s, { sessionUpdate: 'tool_call_update', toolCallId: s.turn.activityId, status: 'completed' });
|
|
299
|
+
s.turn.activityTitle = title; s.turn.activityId = randomUUID();
|
|
300
|
+
this.update(s, { sessionUpdate: 'tool_call', toolCallId: s.turn.activityId, title, kind: 'other', status: 'in_progress' });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (live.awaiting) {
|
|
305
|
+
this.log(JSON.stringify({ sessionId: s.id, nativeAwaiting: live.awaiting }));
|
|
306
|
+
if (!s.awaiting) { if (!s.turn) this.start(s, true); this.say(s, 'Grok 正在等待原生审批,请到 Grok 客户端处理。'); }
|
|
307
|
+
}
|
|
308
|
+
s.awaiting = !!live.awaiting;
|
|
309
|
+
const settlement = live.lastTurnSettlement;
|
|
310
|
+
if (settlement && !live.isRunning) s.lastSettlement = settlement;
|
|
311
|
+
if (!live.isRunning && settlement && !s.closedRuns.has(settlement.turnId) && !(event.snapshot && !s.turn)) {
|
|
312
|
+
if (!s.turn) this.start(s, true);
|
|
313
|
+
// An outgoing prompt needs its echo/reply to identify the native run.
|
|
314
|
+
// Autonomous runs have no outgoing message and may settle silently.
|
|
315
|
+
if (!s.turn.nativeId && s.turn.autonomous) s.turn.nativeId = settlement.turnId;
|
|
316
|
+
if (s.turn.nativeId === settlement.turnId) this.applySettlement(s, settlement);
|
|
317
|
+
else if (!s.turn.nativeId) (s.turn.pendingSettlements ??= new Map()).set(settlement.turnId, settlement);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (event.failed) {
|
|
321
|
+
const id = event.failed.turnId;
|
|
322
|
+
if (id && s.closedRuns.has(id)) {
|
|
323
|
+
this.log(JSON.stringify({ sessionId: s.id, closedRunFailure: event.failed }));
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (s.turn && (!id || s.turn.nativeId !== id)) {
|
|
327
|
+
if (id && !s.turn.nativeId) {
|
|
328
|
+
// A Coda prompt is not associated with a native turn until its echo
|
|
329
|
+
// (or reply) arrives. Retain failures until that association is known.
|
|
330
|
+
(s.turn.pendingFailures ??= new Map()).set(id, event.failed);
|
|
331
|
+
this.log(JSON.stringify({ sessionId: s.id, unassociatedFailure: event.failed }));
|
|
332
|
+
} else this.say(s, `Grok 其他轮次 ${id ?? '(未提供 ID)'} 失败(${event.failed.code ?? 'unknown'})`);
|
|
333
|
+
} else {
|
|
334
|
+
if (!s.turn) { this.start(s, true); s.turn.nativeId = id; }
|
|
335
|
+
s.turn.failed = true;
|
|
336
|
+
this.say(s, `Grok 运行失败(${event.failed.code ?? 'unknown'})`);
|
|
337
|
+
this.finish(s, 'end_turn');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (s.turn?.nativeId && s.turn.pendingFailures) {
|
|
341
|
+
const failed = s.turn.pendingFailures.get(s.turn.nativeId);
|
|
342
|
+
delete s.turn.pendingFailures;
|
|
343
|
+
if (failed) return this.nativeEvent(s, { failed });
|
|
344
|
+
}
|
|
345
|
+
if (s.turn?.nativeId && s.turn.pendingSettlements) {
|
|
346
|
+
const settlement = s.turn.pendingSettlements.get(s.turn.nativeId);
|
|
347
|
+
delete s.turn.pendingSettlements;
|
|
348
|
+
if (settlement) this.applySettlement(s, settlement);
|
|
349
|
+
}
|
|
350
|
+
const turn = s.turn;
|
|
351
|
+
if (turn?.settlement?.outcome === 'success') {
|
|
352
|
+
const afterSettlement = event.heartbeat && BigInt(event.heartbeat.serverTimeMs ?? 0) > BigInt(turn.settlement.settledAtMs ?? 0);
|
|
353
|
+
if (turn.hasReply || afterSettlement) await this.completeSettled(s, turn, !!afterSettlement);
|
|
354
|
+
else this.settlementGrace(s, turn);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
applySettlement(s, settlement) {
|
|
358
|
+
s.turn.settlement = settlement;
|
|
359
|
+
s.turn.nativeRunning = false;
|
|
360
|
+
if (settlement.outcome === 'cancelled') this.finish(s, 'cancelled');
|
|
361
|
+
else if (settlement.outcome !== 'success') {
|
|
362
|
+
s.turn.failed = true;
|
|
363
|
+
this.say(s, `Grok 运行结束:${settlement.outcome}`);
|
|
364
|
+
this.finish(s, 'end_turn');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
settlementGrace(s, turn) {
|
|
368
|
+
if (turn.settlementTimer || s.turn !== turn) return;
|
|
369
|
+
turn.settlementTimer = this.clock.setTimeout(() => {
|
|
370
|
+
turn.settlementTimer = null;
|
|
371
|
+
void this.completeSettled(s, turn, true).catch(error => {
|
|
372
|
+
if (s.turn !== turn) return;
|
|
373
|
+
s.watch.health.failure(error);
|
|
374
|
+
this.settlementGrace(s, turn);
|
|
375
|
+
});
|
|
376
|
+
}, SETTLEMENT_GRACE_MS);
|
|
377
|
+
}
|
|
378
|
+
async completeSettled(s, turn, allowEmpty) {
|
|
379
|
+
if (s.turn !== turn || turn.reconciling || turn.widgetPending || (!turn.hasReply && !allowEmpty)) return;
|
|
380
|
+
turn.reconciling = true;
|
|
381
|
+
try {
|
|
382
|
+
await s.watch.reconcile();
|
|
383
|
+
if (s.turn === turn && !turn.widgetPending) this.finish(s, 'end_turn');
|
|
384
|
+
} finally { turn.reconciling = false; }
|
|
385
|
+
}
|
|
386
|
+
async widget(s, turn, entryId, widget) {
|
|
387
|
+
turn.widgetPending = (turn.widgetPending ?? 0) + 1;
|
|
388
|
+
try {
|
|
389
|
+
if (!Array.isArray(widget?.options) || !widget.options.length) throw new Error('widget 没有可选项,请到 Grok 客户端处理');
|
|
390
|
+
const options = widget.options.map((option, index) => ({ optionId: String(index), name: option.label ?? String(option.value), kind: 'allow_once' }));
|
|
391
|
+
const answer = await this.rpc.request('session/request_permission', { sessionId: s.id,
|
|
392
|
+
toolCall: { toolCallId: `widget:${entryId}`, title: widget.prompt || 'Grok 提问', status: 'pending', kind: 'other' }, options }, { signal: turn.controller.signal });
|
|
393
|
+
if (answer.outcome?.outcome !== 'selected') return;
|
|
394
|
+
const choice = options.findIndex(o => o.optionId === answer.outcome.optionId);
|
|
395
|
+
if (choice < 0) throw new Error('未知 widget 选项');
|
|
396
|
+
const result = await this.auth.client.request('RespondGrokBotWidget', { agentId: s.saved.bot, sessionId: '', entryId, value: widget.options[choice].value });
|
|
397
|
+
if (!result.accepted) throw new Error('Grok 未接受 widget 回答');
|
|
398
|
+
} catch (error) { if (s.turn === turn) this.say(s, `Grok 卡片未完成:${error.message}`); }
|
|
399
|
+
finally {
|
|
400
|
+
turn.widgetPending--;
|
|
401
|
+
if (turn.settlement?.outcome === 'success') this.settlementGrace(s, turn);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
184
404
|
async trace(s, name, args, operation, replyAfter) {
|
|
185
405
|
const turn = s.turn, toolCallId = randomUUID();
|
|
186
406
|
const background = replyAfter !== undefined && !!this.waits;
|
|
@@ -233,6 +453,7 @@ export class Adapter {
|
|
|
233
453
|
if (name === 'new_session' && (typeof args.project !== 'string' || !args.project.trim()))
|
|
234
454
|
fail('new_session 必须指定目标 project;Bot 所在的空目录没有代码,请选择要修复的项目');
|
|
235
455
|
this.provider(s, name);
|
|
456
|
+
const local = !s.turn;
|
|
236
457
|
const turn = this.active(s, id, WRITE.has(name));
|
|
237
458
|
const signal = turn?.controller.signal;
|
|
238
459
|
let forwarded = args;
|
|
@@ -249,7 +470,9 @@ export class Adapter {
|
|
|
249
470
|
// Approval calls outlive a Coda turn and must not inherit its abort signal
|
|
250
471
|
// or the MCP client's default 60-second timeout (null disables both).
|
|
251
472
|
return this.trace(s, name, args, () => this.provider(s, name).call(name, forwarded, mayWaitForApproval ? null : signal),
|
|
252
|
-
mayWaitForApproval ? this.submissionWaitMs : undefined)
|
|
473
|
+
mayWaitForApproval ? this.submissionWaitMs : undefined).finally(() => {
|
|
474
|
+
if (local && turn && s.turn === turn && !turn.nativeRunning && !turn.nativeId) this.finish(s, 'end_turn');
|
|
475
|
+
});
|
|
253
476
|
}
|
|
254
477
|
provider(s, name) {
|
|
255
478
|
const client = s.routes.get(name);
|
|
@@ -286,18 +509,28 @@ export class Adapter {
|
|
|
286
509
|
const s = this.session(p.sessionId);
|
|
287
510
|
if (method === 'session/set_config_option') {
|
|
288
511
|
if (p.configId === 'model' && p.value === 'grok-bot') return { configOptions: this.configs(s) };
|
|
289
|
-
if (p.configId !== 'bot' || typeof p.value !== 'string'
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
512
|
+
if (p.configId !== 'bot' || typeof p.value !== 'string') fail('未知的 Bot 配置');
|
|
513
|
+
await this.refreshBots();
|
|
514
|
+
const bot = this.bots.find(b => b.agentId === p.value || b.name === p.value);
|
|
515
|
+
if (p.value && !bot) fail('未找到 Bot,请先 /login 并刷新列表');
|
|
516
|
+
if (s.turn) fail('请先结束本轮再切换 Bot');
|
|
517
|
+
if ((bot?.agentId ?? '') === s.saved.bot) return { configOptions: this.configs(s) };
|
|
518
|
+
if (s.watch) { s.watch.close(); this.wait(s, 'done'); s.watch = null; }
|
|
519
|
+
s.routines?.close();
|
|
520
|
+
s.saved = { bot: bot?.agentId ?? '', bound: false };
|
|
521
|
+
s.closedRuns.clear(); s.texts.clear(); s.widgets.clear(); s.sent.clear();
|
|
522
|
+
s.connected = false; s.hadSubagents = false; s.lastSettlement = null; s.awaiting = false;
|
|
523
|
+
this.save(s);
|
|
524
|
+
if (bot) await this.attach(s);
|
|
296
525
|
return { configOptions: this.configs(s) };
|
|
297
526
|
}
|
|
298
527
|
if (method === 'session/prompt') return this.prompt(s, p.prompt);
|
|
299
528
|
if (method === 'session/cancel') {
|
|
300
|
-
|
|
529
|
+
const turn = s.turn;
|
|
530
|
+
if (turn && s.saved.bot && this.auth.client) {
|
|
531
|
+
await this.auth.client.request('InterruptGrokBotAgentRun', { agentId: s.saved.bot, sessionId: '' });
|
|
532
|
+
}
|
|
533
|
+
if (turn && s.turn === turn) this.finish(s, 'cancelled');
|
|
301
534
|
return {};
|
|
302
535
|
}
|
|
303
536
|
const args = p.params ?? {};
|
|
@@ -305,32 +538,13 @@ export class Adapter {
|
|
|
305
538
|
if (p.method === 'hello') {
|
|
306
539
|
s.saved.bound = true;
|
|
307
540
|
this.save(s);
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
return { sessionId: s.id, bot: s.saved.bot, instructions: s.instructions };
|
|
541
|
+
// Idempotent confirmation of the authenticated Coda tool binding.
|
|
542
|
+
return { sessionId: s.id, bot: s.saved.bot };
|
|
311
543
|
}
|
|
312
544
|
if (!s.saved.bound) fail('Bot 尚未完成绑定,请先运行 bind');
|
|
313
|
-
if (p.method === 'input') {
|
|
314
|
-
if (typeof args.turn !== 'string' || !args.turn) fail('input 必须带 Webhook 的 turn');
|
|
315
|
-
if (!s.input || s.input.turn !== args.turn) fail('本轮输入不存在或已被下一轮替换;请使用最新 Webhook 的 turn');
|
|
316
|
-
return { ...s.input, instructions: s.instructions };
|
|
317
|
-
}
|
|
318
|
-
if (p.method === 'say') {
|
|
319
|
-
if (typeof args.text !== 'string' || !args.text.trim()) fail('say 需要非空 text');
|
|
320
|
-
const turn = this.active(s, args.turn, true);
|
|
321
|
-
this.say(s, args.text);
|
|
322
|
-
return { turn: turn.id };
|
|
323
|
-
}
|
|
324
|
-
if (p.method === 'done') {
|
|
325
|
-
const turn = this.active(s, args.turn);
|
|
326
|
-
if (!turn) fail('没有正在进行的轮次');
|
|
327
|
-
if (turn.tools.size) fail('仍有工具调用或审批在进行,请等待结果再 done');
|
|
328
|
-
this.finish(s, 'end_turn');
|
|
329
|
-
return {};
|
|
330
|
-
}
|
|
331
545
|
if (p.method === 'tool') return this.tool(s, args);
|
|
332
546
|
if (p.method === 'telemetry') return this.telemetry(s, args);
|
|
333
547
|
fail(`未知的远端方法:${p.method}`);
|
|
334
548
|
}
|
|
335
|
-
close() { for (const s of this.sessions.values()) { this.finish(s, 'cancelled'); for (const client of s.clients) client.close(); } }
|
|
549
|
+
close() { this.closed = true; this.auth.close(); for (const s of this.sessions.values()) { s.routines?.close(); s.watch?.close(); this.finish(s, 'cancelled'); for (const client of s.clients) client.close(); } }
|
|
336
550
|
}
|
package/agent/setup.mjs
CHANGED
|
@@ -18,7 +18,8 @@ curl -fL ${quote(`${RAW}/SKILL.md`)} -o /workspace/.coda-bot/SKILL.md
|
|
|
18
18
|
net 若打印 Tailscale 登录链接,请让我完成登录,把云电脑加入能访问 Coda 后端的 tailnet;然后重新执行 bind。密钥需要时使用 Grok 的密钥卡片,不贴到聊天。
|
|
19
19
|
|
|
20
20
|
在 Bot 设置里添加命令启动的自定义 MCP 服务:\`node /workspace/.coda-bot/coda-bot.mjs mcp\`。同账号多个 Bot 共用云电脑时,每个 Bot 必须在命令末尾追加自己的会话 ID:\`${sessionId}\`,避免串用绑定。
|
|
21
|
-
读取并安装 /workspace/.coda-bot/SKILL.md 为此 Bot 的 Skill
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
读取并安装 /workspace/.coda-bot/SKILL.md 为此 Bot 的 Skill。
|
|
22
|
+
正常消息与回复走原生主会话,定时任务仍由 Grok Routine 执行。
|
|
23
|
+
升级旧 Bot:不再需要 Coda 消息(coda)Webhook Routine。保留 Coda 巡检(coda-2)定时 Routine;请先让我确认,再将旧指令中的 coda_input、coda_say、coda_done 改成「有发现就在对话里告诉用户」。更新脚本和 Skill 后重启 MCP。
|
|
24
|
+
这些工具只提供 Coda 数据和经卡片授权的操作,Bot 的正常回复会自动镜像到 Coda。`;
|
|
24
25
|
}
|
package/agent/watch.mjs
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { pause } from './auth.mjs';
|
|
3
|
+
import { ConnectionHealth, authenticationError } from './health.mjs';
|
|
4
|
+
|
|
5
|
+
export const TRANSCRIPT_CACHE_LIMIT = 2000;
|
|
6
|
+
|
|
7
|
+
export function decode(entry) {
|
|
8
|
+
if (entry.bodyOmitted) throw new Error('原生 transcript 正文被省略,无法交付;请在 Grok 客户端查看');
|
|
9
|
+
try { return JSON.parse(Buffer.from(entry.body, 'base64').toString('utf8')); }
|
|
10
|
+
catch { throw new Error('无法解析 Grok transcript 正文'); }
|
|
11
|
+
}
|
|
12
|
+
export const sequence = entries => entries.reduce((n, e) => BigInt(e.updatedSeq ?? 0) > BigInt(n) ? String(e.updatedSeq) : n, '0');
|
|
13
|
+
// A single connection owns a Bot's main transcript. The adapter serializes
|
|
14
|
+
// events; reconnects use the committed cursor, never resend user messages.
|
|
15
|
+
export class NativeWatch {
|
|
16
|
+
constructor({ client, bot, saved, clock, commit, event, error, recovered, log }) {
|
|
17
|
+
Object.assign(this, { client, bot, saved, clock, commit, event, error, recovered, log });
|
|
18
|
+
this.health = new ConnectionHealth({ error, recovered, log });
|
|
19
|
+
this.controller = new AbortController();
|
|
20
|
+
}
|
|
21
|
+
async history({ all = false } = {}) {
|
|
22
|
+
const entries = [];
|
|
23
|
+
let beforeSeq, generation;
|
|
24
|
+
for (;;) {
|
|
25
|
+
const page = await this.client.request('ListGrokBotTranscriptEntries', { agentId: this.bot, sessionId: '', limit: 1000,
|
|
26
|
+
...(beforeSeq ? { beforeSeq, generation } : {}) }, { signal: AbortSignal.any([this.controller.signal, AbortSignal.timeout(30_000)]) });
|
|
27
|
+
if (generation !== undefined && generation !== (page.generation ?? 0)) throw new Error('Grok 历史在分页时已重置,重新连接后核对');
|
|
28
|
+
generation = page.generation ?? 0;
|
|
29
|
+
const rows = page.entries ?? [];
|
|
30
|
+
entries.push(...rows);
|
|
31
|
+
if (!all || rows.length < 1000) break;
|
|
32
|
+
const next = rows.reduce((n, r) => BigInt(r.seq) < BigInt(n) ? r.seq : n, rows[0].seq);
|
|
33
|
+
if (beforeSeq && BigInt(next) >= BigInt(beforeSeq)) throw new Error('Grok 历史分页游标没有前进');
|
|
34
|
+
beforeSeq = next;
|
|
35
|
+
}
|
|
36
|
+
return { entries, generation };
|
|
37
|
+
}
|
|
38
|
+
async start() {
|
|
39
|
+
if (!this.saved.stream) {
|
|
40
|
+
const h = await this.history();
|
|
41
|
+
// Existing Grok history is a baseline, not a new autonomous run.
|
|
42
|
+
this.saved.stream = { generation: h.generation ?? 0, afterUpdatedSeq: sequence(h.entries ?? []), seen: {}, hashes: {}, baselineAt: Date.now() };
|
|
43
|
+
for (const row of h.entries ?? []) {
|
|
44
|
+
this.saved.stream.seen[row.entryId] = String(row.updatedSeq ?? row.seq);
|
|
45
|
+
this.saved.stream.hashes[row.entryId] = createHash('sha256').update(row.body ?? '').digest('hex');
|
|
46
|
+
}
|
|
47
|
+
this.commit();
|
|
48
|
+
}
|
|
49
|
+
if (this.trim()) this.commit();
|
|
50
|
+
this.running = this.loop();
|
|
51
|
+
}
|
|
52
|
+
trim() {
|
|
53
|
+
const state = this.saved.stream;
|
|
54
|
+
const oldSize = Object.keys(state.seen).length;
|
|
55
|
+
const oldHashSize = Object.keys(state.hashes ?? {}).length;
|
|
56
|
+
const ids = Object.keys(state.seen).sort((a, b) => BigInt(state.seen[a]) > BigInt(state.seen[b]) ? -1 : 1).slice(0, TRANSCRIPT_CACHE_LIMIT);
|
|
57
|
+
state.seen = Object.fromEntries(ids.map(id => [id, state.seen[id]]));
|
|
58
|
+
state.hashes = Object.fromEntries(ids.map(id => [id, state.hashes?.[id]]));
|
|
59
|
+
return oldSize !== ids.length || oldHashSize !== ids.length;
|
|
60
|
+
}
|
|
61
|
+
async rows(rows) {
|
|
62
|
+
if (rows.agentId !== this.bot || (rows.sessionId ?? '') !== '') return;
|
|
63
|
+
const state = this.saved.stream;
|
|
64
|
+
const through = BigInt(rows.replayThrough ?? state.afterUpdatedSeq);
|
|
65
|
+
let changed = false;
|
|
66
|
+
try {
|
|
67
|
+
for (const row of [...(rows.entries ?? [])].sort((a, b) => BigInt(a.updatedSeq) < BigInt(b.updatedSeq) ? -1 : 1)) {
|
|
68
|
+
const seq = String(row.updatedSeq ?? row.seq);
|
|
69
|
+
// Eviction must not turn normal history reconciliation into replay.
|
|
70
|
+
if (state.seen[row.entryId] === undefined && !rows.newGeneration && BigInt(seq) <= through) continue;
|
|
71
|
+
if (state.seen[row.entryId] !== undefined && BigInt(state.seen[row.entryId]) >= BigInt(seq)) continue;
|
|
72
|
+
const body = decode(row);
|
|
73
|
+
const hash = createHash('sha256').update(row.body).digest('hex');
|
|
74
|
+
if (state.hashes[row.entryId] !== hash && !(rows.replay && body.timestampMs < state.baselineAt && !state.seen[row.entryId]))
|
|
75
|
+
await this.event({ entry: { row, body } });
|
|
76
|
+
state.hashes[row.entryId] = hash;
|
|
77
|
+
state.seen[row.entryId] = seq;
|
|
78
|
+
if (BigInt(seq) > BigInt(state.afterUpdatedSeq)) state.afterUpdatedSeq = seq;
|
|
79
|
+
changed = true;
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
// Commit delivered rows once per batch, including a partial batch whose
|
|
83
|
+
// later entry failed. Keep only the newest updatedSeq IDs and hashes.
|
|
84
|
+
if (changed) { this.trim(); this.commit(); }
|
|
85
|
+
}
|
|
86
|
+
await this.event({ checkpoint: true });
|
|
87
|
+
}
|
|
88
|
+
async reconcile() {
|
|
89
|
+
const history = await this.history();
|
|
90
|
+
await this.rows({ agentId: this.bot, entries: history.entries });
|
|
91
|
+
}
|
|
92
|
+
async loop() {
|
|
93
|
+
const signal = this.controller.signal;
|
|
94
|
+
while (!signal.aborted) {
|
|
95
|
+
try {
|
|
96
|
+
const stream = await this.client.request('WatchGrokBotTranscripts', {
|
|
97
|
+
cursors: [{ agentId: this.bot, sessionId: '', generation: this.saved.stream.generation, afterUpdatedSeq: this.saved.stream.afterUpdatedSeq }],
|
|
98
|
+
includeUnlistedAgents: true, inlineBodyMaxBytes: 262144,
|
|
99
|
+
}, { stream: true, signal });
|
|
100
|
+
for await (const value of stream) {
|
|
101
|
+
if (signal.aborted) break;
|
|
102
|
+
if (value.connected) await this.event({ connected: true });
|
|
103
|
+
const reset = value.cleared ?? value.cursorTooOld;
|
|
104
|
+
if (reset && (!reset.agentId || reset.agentId === this.bot) && (reset.sessionId ?? '') === '') {
|
|
105
|
+
const history = await this.history({ all: true });
|
|
106
|
+
const previous = this.saved.stream;
|
|
107
|
+
const newGeneration = previous.generation !== (history.generation ?? 0);
|
|
108
|
+
const replayThrough = previous.afterUpdatedSeq;
|
|
109
|
+
this.saved.stream.generation = history.generation ?? 0;
|
|
110
|
+
if (newGeneration) this.saved.stream.seen = {};
|
|
111
|
+
this.saved.stream.afterUpdatedSeq = '0';
|
|
112
|
+
// Keep IDs across cursor resets; the new stream replays history.
|
|
113
|
+
await this.rows({ agentId: this.bot, entries: history.entries, replay: true, replayThrough, newGeneration });
|
|
114
|
+
this.saved.stream.afterUpdatedSeq = sequence(history.entries);
|
|
115
|
+
this.trim(); this.commit();
|
|
116
|
+
this.log('原生游标已重置,静默重连');
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (value.rows) await this.rows(value.rows);
|
|
120
|
+
if (value.agentState) for (const live of value.agentState.live ?? [])
|
|
121
|
+
if (live.agentId === this.bot && (live.sessionId ?? '') === '')
|
|
122
|
+
await this.event({ live, snapshot: !!value.agentState.snapshot });
|
|
123
|
+
if (value.agentStateChanged?.agentId === this.bot && value.agentStateChanged.families?.some(f => ['subagents', 'automations'].includes(f)))
|
|
124
|
+
await this.event({ routinesChanged: true });
|
|
125
|
+
if (value.turnFailed?.agentId === this.bot && (value.turnFailed.sessionId ?? '') === '') await this.event({ failed: value.turnFailed });
|
|
126
|
+
if (value.heartbeat) await this.event({ heartbeat: value.heartbeat });
|
|
127
|
+
if (value.heartbeat || value.rows || value.agentState) this.health.success();
|
|
128
|
+
}
|
|
129
|
+
if (signal.aborted) break;
|
|
130
|
+
this.log('原生订阅正常结束,静默重连');
|
|
131
|
+
this.health.success();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (signal.aborted) break;
|
|
134
|
+
this.health.failure(error);
|
|
135
|
+
// Authentication errors need a new official login, not a retry loop.
|
|
136
|
+
if (authenticationError(error)) break;
|
|
137
|
+
}
|
|
138
|
+
try { await pause(this.clock, this.health.delay, signal); }
|
|
139
|
+
catch { break; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
close() { this.controller.abort(); }
|
|
143
|
+
}
|