@coda-console/grok-bot 0.3.0 → 0.3.1
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 +7 -3
- package/agent/session.mjs +44 -20
- package/package.json +1 -1
- package/remote/SKILL.md +7 -3
package/README.md
CHANGED
|
@@ -46,6 +46,8 @@ node /workspace/.coda-bot/coda-bot.mjs mcp <Coda-sessionId>
|
|
|
46
46
|
|
|
47
47
|
工具包括 `read_thread`、`read_tool_call`、`agents`、`status`、`workflow_status`、`workflow_list`、`telemetry`、`machines`、`machine_diagnostics`、`new_session`、`send`(MCP 名加 `coda_` 前缀)。Coda 工具调用保留完整输入输出;轮外只读调用仅写日志,不开启空轮次。`new_session` 在目标目录开独立会话,`send` 给已有会话发消息,均由 Coda 授权卡片批准;稳定 requestId、turn 票据和后台 send 结果由适配器处理。
|
|
48
48
|
|
|
49
|
+
巡检按远端 Skill 先开只归因的独立会话(不改代码、不开 PR),prompt 写清现象、证据、时间和版本,要求结论、证据链、置信度与是否建议修复。下次巡检读取结论,确认需要修复的代码问题才开修复会话,引用归因会话与结论并走完仓库 PR 流程;偶发、外部因素或用户暂不处理的问题只记忆。用户指定的 Agent、暂不处理事项等也写进记忆,开会话前先读取遵守,不在 Skill 固定模型或推理强度。卡片审批、不重复开会话、不轮询等规则照旧;GitHub 只读限制只约束 Bot 自己。
|
|
50
|
+
|
|
49
51
|
诊断工具需在 Coda 安装并为目录启用内置「机器诊断」插件(`diagnostics` MCP),参见 [诊断工具](https://github.com/coda-console/coda/pull/2003);遥测端点参见 [遥测 JSON 接口](https://github.com/coda-console/coda/pull/2004)。没有 new_session 的旧 Coda 会如实报不可用。
|
|
50
52
|
|
|
51
53
|
## 从 0.1.7 升级
|
|
@@ -62,9 +64,11 @@ node /workspace/.coda-bot/coda-bot.mjs mcp <Coda-sessionId>
|
|
|
62
64
|
|
|
63
65
|
这是未获稳定性保证的私有接口,上游升级可能使登录、字段或接口失效;错误会显示在会话中。[Cursor 服务条款 §1.5](https://cursor.com/terms-of-service) 限制逆向工程等行为(法律不允许此限制的情况除外),使用前需评估该条款风险。
|
|
64
66
|
|
|
65
|
-
- SetStatus
|
|
66
|
-
-
|
|
67
|
+
- 原生活动标题保留 detail,按 callId/tool/target/detail 识别活动。0.78.1 的 tool/target 是自由字符串;已有只读记录的 43 条 tool 活动均为 SetStatus,它只报告说明,不能据此推断底层命令或文件操作,因此 SetStatus 与未经验证的名称均报 ACP `other`。target 没有已验证的文件路径语义,不编造 locations 或输入输出。thinking 只显示“正在思考”,不是思考正文。
|
|
68
|
+
- Coda 代理工具的 kind:read_thread、read_tool_call、agents、status、workflow_status、workflow_list、machines、machine_diagnostics → `other`(数据查询,显示「调用 N 个工具」;Coda 会把没有路径的 `read` 调用也计为文件,不能用于这些查询);telemetry → `fetch`(「联网 N 次」);new_session、send → `other`。保留完整输入输出,没有文件位置就不报 locations。
|
|
69
|
+
- 按 requestId/turnId 归并。成功结算且主 Agent 无运行、撰写、重试、activity 或 awaiting 状态时才考虑收尾;再次出现主 Agent 活动会撤销待收尾状态。最后正文之后没有活动,立即核对历史并收尾;先回复又出现活动则等待后文,最多宽限 10 秒,再核对历史;完全无正文仍用 5 秒宽限。后文到达即可完成,不必等满宽限。hasRunningSubagents 属于独立 Routine 观测,不阻塞主轮次;heartbeat.serverTimeMs 只有时间,没有正文游标,不作为正文交付完成的保证。上游没有“最后正文已落盘”标志,超出宽限或取消/超时后的消息仍原样显示为 Bot 的话,不加前缀,不按正文去重。
|
|
70
|
+
- 10 秒宽限依据:2026-10-31 的 requestId `11223fc4-fbff-4364-a6d7-47911999652b`,只读历史中 t7s0/t7s1 正文时间为 15:34:39.339 / 15:34:51.225(SGT),相差 11.886 秒;Coda 的中间活动在 15:34:43.694 被收尾,t7s1 在 15:34:51.495 到达,晚 7.801 秒。历史不保留该次 settlement,不能把本地收尾时间当精确的原生 settledAtMs;10 秒是覆盖已知约 8 秒延迟的兜底,不是上游保证。正常“工具→最终正文→结算”不增加等待。
|
|
67
71
|
- entryId、updatedSeq 和正文摘要只保留按 updatedSeq 排序的最近 2000 条,每批条目合并写盘;更早条目若被服务端修订,会当作新条目呈现。断线按游标重连,游标失效分页读历史补齐并去重,不重发用户消息。首次选择只建立历史基线,不灌入旧聊天。正文被服务端省略或消息类型不支持时明确提示去 Grok 客户端查看。
|
|
68
72
|
- Stop 调 InterruptGrokBotAgentRun。widget 选项变成 Coda 卡片,选择后交回 RespondGrokBotWidget;未选择则不代答。auto-review 尚未实测,收到 awaiting 时提示去 Grok 客户端处理,并记事件日志。
|
|
69
73
|
|
|
70
|
-
实机已验证官方登录、Bot 列表、原生发送/状态/活动/回复、Interrupt、widget 选择交回、Routine run 与 executor ID 的关联,以及无需任何 Coda 汇报工具也会出现的主会话结论(结束状态比正文早 121 ms)。完整新适配器尚未真机部署;自动续期、auto-review、executor 内部过程未实测。适配器测试使用假原生服务端与假时钟。
|
|
74
|
+
实机已验证官方登录、Bot 列表、原生发送/状态/活动/回复、Interrupt、widget 选择交回、Routine run 与 executor ID 的关联,以及无需任何 Coda 汇报工具也会出现的主会话结论(结束状态比正文早 121 ms)。完整新适配器尚未真机部署;自动续期、auto-review、executor 内部过程未实测。适配器测试使用假原生服务端与假时钟。0.3.1 仅只读核对原生历史、订阅及已有协议/观察记录,没有向真实 Bot 发消息或运行新版适配器,也没有更新云电脑与 Routine。
|
package/agent/session.mjs
CHANGED
|
@@ -14,6 +14,9 @@ const DIAGNOSTICS = new Set(['machines', 'machine_diagnostics']);
|
|
|
14
14
|
const fail = message => { throw Object.assign(new Error(message), { code: -32602 }); };
|
|
15
15
|
const object = value => value && typeof value === 'object' && !Array.isArray(value);
|
|
16
16
|
export const SETTLEMENT_GRACE_MS = 5000;
|
|
17
|
+
// Observed t7s1 arrived 7.8 s after Coda closed a successful run whose first
|
|
18
|
+
// reply preceded a tool activity. There is no transcript-drained marker.
|
|
19
|
+
export const ACTIVITY_REPLY_GRACE_MS = 10_000;
|
|
17
20
|
export const SUBMISSION_WAIT_MS = 20_000;
|
|
18
21
|
function canonical(value) {
|
|
19
22
|
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
@@ -86,7 +89,7 @@ export class Adapter {
|
|
|
86
89
|
turn.timer = this.clock.setTimeout(() => {
|
|
87
90
|
if (s.turn !== turn) return;
|
|
88
91
|
turn.failed = true;
|
|
89
|
-
this.say(s, `等待 Grok Bot 超过 ${this.settings['grok-bot.turnTimeout'] ?? 'PT30M'}
|
|
92
|
+
this.say(s, `等待 Grok Bot 超过 ${this.settings['grok-bot.turnTimeout'] ?? 'PT30M'},本轮已结束;云端任务未必停止。之后收到的原生消息仍会显示。`);
|
|
90
93
|
this.finish(s, 'end_turn');
|
|
91
94
|
}, this.timeout);
|
|
92
95
|
return turn;
|
|
@@ -261,14 +264,14 @@ export class Adapter {
|
|
|
261
264
|
if (!late && s.turn?.autonomous && s.turn.hasReply && s.turn.nativeId && s.turn.nativeId !== body.requestId) this.finish(s, 'end_turn');
|
|
262
265
|
if (!s.turn) {
|
|
263
266
|
this.start(s, true);
|
|
264
|
-
if (late)
|
|
267
|
+
if (late) s.turn.settlement = { turnId: body.requestId, outcome: 'success', settledAtMs: String(this.now()) };
|
|
265
268
|
}
|
|
266
269
|
const turn = s.turn;
|
|
267
270
|
if (!turn.nativeId && !(late && turn.messageId)) turn.nativeId = body.requestId;
|
|
268
|
-
//
|
|
269
|
-
if (body.requestId && turn.nativeId !== body.requestId) this.say(s, 'Grok 上一轮补充消息:');
|
|
271
|
+
// Late output remains the Bot's own speech, even during another prompt.
|
|
270
272
|
if (turn.nativeId === body.requestId) {
|
|
271
273
|
turn.hasReply = true;
|
|
274
|
+
turn.awaitingReply = false;
|
|
272
275
|
if (s.lastSettlement?.turnId === body.requestId)
|
|
273
276
|
(turn.pendingSettlements ??= new Map()).set(body.requestId, s.lastSettlement);
|
|
274
277
|
}
|
|
@@ -285,30 +288,42 @@ export class Adapter {
|
|
|
285
288
|
const live = event.live;
|
|
286
289
|
if (live.hasRunningSubagents || s.hadSubagents) void s.routines.hint(!!live.hasRunningSubagents);
|
|
287
290
|
s.hadSubagents = !!live.hasRunningSubagents;
|
|
288
|
-
|
|
289
|
-
|
|
291
|
+
// Subagents can run independently; heartbeat serverTimeMs is not a
|
|
292
|
+
// transcript watermark. Only main-agent activity affects this turn.
|
|
293
|
+
const busy = !!(live.isRunning || live.isComposingMessage || live.isRetrying || live.activity || live.awaiting);
|
|
294
|
+
if (busy && !s.turn) this.start(s, true);
|
|
295
|
+
if (busy && s.turn) {
|
|
290
296
|
s.turn.nativeRunning = true;
|
|
297
|
+
if (s.turn.hasReply) s.turn.awaitingReply = true;
|
|
298
|
+
delete s.turn.settlement;
|
|
299
|
+
this.clock.clearTimeout(s.turn.settlementTimer);
|
|
300
|
+
s.turn.settlementTimer = null;
|
|
291
301
|
if (live.activity?.kind === 'thinking' && !s.turn.thinking) {
|
|
292
302
|
s.turn.thinking = true;
|
|
293
303
|
this.update(s, { sessionUpdate: 'agent_thought_chunk', content: { type: 'text', text: 'Grok 正在思考(原生状态,未提供思考正文)' } });
|
|
294
304
|
}
|
|
295
305
|
if (live.activity?.kind === 'tool') {
|
|
296
306
|
const title = live.activity.detail || live.activity.tool || 'Grok 活动';
|
|
297
|
-
|
|
307
|
+
const activityKey = JSON.stringify([live.activity.callId, live.activity.tool, live.activity.target, title]);
|
|
308
|
+
if (activityKey !== s.turn.activityKey) {
|
|
298
309
|
if (s.turn.activityId) this.update(s, { sessionUpdate: 'tool_call_update', toolCallId: s.turn.activityId, status: 'completed' });
|
|
299
|
-
s.turn.
|
|
310
|
+
s.turn.activityKey = activityKey; s.turn.activityId = randomUUID();
|
|
311
|
+
// 0.78.1 defines tool/target as free strings. Observed SetStatus
|
|
312
|
+
// carries a prose summary, not the operation it describes. Keep
|
|
313
|
+
// it (and unverified names) as other; target is not a file path.
|
|
300
314
|
this.update(s, { sessionUpdate: 'tool_call', toolCallId: s.turn.activityId, title, kind: 'other', status: 'in_progress' });
|
|
301
315
|
}
|
|
302
316
|
}
|
|
303
317
|
}
|
|
318
|
+
if (!busy && s.turn) s.turn.nativeRunning = false;
|
|
304
319
|
if (live.awaiting) {
|
|
305
320
|
this.log(JSON.stringify({ sessionId: s.id, nativeAwaiting: live.awaiting }));
|
|
306
321
|
if (!s.awaiting) { if (!s.turn) this.start(s, true); this.say(s, 'Grok 正在等待原生审批,请到 Grok 客户端处理。'); }
|
|
307
322
|
}
|
|
308
323
|
s.awaiting = !!live.awaiting;
|
|
309
324
|
const settlement = live.lastTurnSettlement;
|
|
310
|
-
if (settlement && !
|
|
311
|
-
if (!
|
|
325
|
+
if (settlement && !busy) s.lastSettlement = settlement;
|
|
326
|
+
if (!busy && settlement && !s.closedRuns.has(settlement.turnId) && !(event.snapshot && !s.turn)) {
|
|
312
327
|
if (!s.turn) this.start(s, true);
|
|
313
328
|
// An outgoing prompt needs its echo/reply to identify the native run.
|
|
314
329
|
// Autonomous runs have no outgoing message and may settle silently.
|
|
@@ -349,14 +364,13 @@ export class Adapter {
|
|
|
349
364
|
}
|
|
350
365
|
const turn = s.turn;
|
|
351
366
|
if (turn?.settlement?.outcome === 'success') {
|
|
352
|
-
|
|
353
|
-
if (turn.hasReply || afterSettlement) await this.completeSettled(s, turn, !!afterSettlement);
|
|
367
|
+
if (turn.hasReply && !turn.awaitingReply) await this.completeSettled(s, turn);
|
|
354
368
|
else this.settlementGrace(s, turn);
|
|
355
369
|
}
|
|
356
370
|
}
|
|
357
371
|
applySettlement(s, settlement) {
|
|
372
|
+
if (settlement.outcome === 'success' && s.turn.nativeRunning) return;
|
|
358
373
|
s.turn.settlement = settlement;
|
|
359
|
-
s.turn.nativeRunning = false;
|
|
360
374
|
if (settlement.outcome === 'cancelled') this.finish(s, 'cancelled');
|
|
361
375
|
else if (settlement.outcome !== 'success') {
|
|
362
376
|
s.turn.failed = true;
|
|
@@ -365,7 +379,7 @@ export class Adapter {
|
|
|
365
379
|
}
|
|
366
380
|
}
|
|
367
381
|
settlementGrace(s, turn) {
|
|
368
|
-
if (turn.settlementTimer || s.turn !== turn) return;
|
|
382
|
+
if (turn.settlementTimer || turn.reconciling || turn.nativeRunning || s.turn !== turn) return;
|
|
369
383
|
turn.settlementTimer = this.clock.setTimeout(() => {
|
|
370
384
|
turn.settlementTimer = null;
|
|
371
385
|
void this.completeSettled(s, turn, true).catch(error => {
|
|
@@ -373,15 +387,22 @@ export class Adapter {
|
|
|
373
387
|
s.watch.health.failure(error);
|
|
374
388
|
this.settlementGrace(s, turn);
|
|
375
389
|
});
|
|
376
|
-
}, SETTLEMENT_GRACE_MS);
|
|
390
|
+
}, turn.awaitingReply ? ACTIVITY_REPLY_GRACE_MS : SETTLEMENT_GRACE_MS);
|
|
377
391
|
}
|
|
378
|
-
async completeSettled(s, turn,
|
|
379
|
-
if (s.turn !== turn || turn.reconciling || turn.widgetPending ||
|
|
392
|
+
async completeSettled(s, turn, allowPending = false) {
|
|
393
|
+
if (s.turn !== turn || turn.reconciling || turn.widgetPending || turn.nativeRunning || !turn.settlement ||
|
|
394
|
+
((!turn.hasReply || turn.awaitingReply) && !allowPending)) return;
|
|
395
|
+
const settlement = turn.settlement;
|
|
380
396
|
turn.reconciling = true;
|
|
381
397
|
try {
|
|
382
398
|
await s.watch.reconcile();
|
|
383
|
-
if (s.turn === turn && !turn.widgetPending) this.finish(s, 'end_turn');
|
|
384
|
-
} finally {
|
|
399
|
+
if (s.turn === turn && !turn.widgetPending && !turn.nativeRunning && turn.settlement === settlement) this.finish(s, 'end_turn');
|
|
400
|
+
} finally {
|
|
401
|
+
turn.reconciling = false;
|
|
402
|
+
// A newer idle snapshot or a widget can arrive while history is read.
|
|
403
|
+
// If it prevented finishing, keep the bounded fallback alive.
|
|
404
|
+
if (s.turn === turn && turn.settlement?.outcome === 'success') this.settlementGrace(s, turn);
|
|
405
|
+
}
|
|
385
406
|
}
|
|
386
407
|
async widget(s, turn, entryId, widget) {
|
|
387
408
|
turn.widgetPending = (turn.widgetPending ?? 0) + 1;
|
|
@@ -404,7 +425,10 @@ export class Adapter {
|
|
|
404
425
|
async trace(s, name, args, operation, replyAfter) {
|
|
405
426
|
const turn = s.turn, toolCallId = randomUUID();
|
|
406
427
|
const background = replyAfter !== undefined && !!this.waits;
|
|
407
|
-
|
|
428
|
+
// Coda counts read calls without locations as files. Data queries are
|
|
429
|
+
// other tools, not file reads; telemetry performs an HTTP fetch.
|
|
430
|
+
const kind = name === 'telemetry' ? 'fetch' : 'other';
|
|
431
|
+
const toolCall = { toolCallId, title: `Coda · ${name}`, kind, status: 'pending', rawInput: args };
|
|
408
432
|
if (turn) { turn.tools.set(toolCallId, { background }); this.update(s, { sessionUpdate: 'tool_call', ...toolCall }); }
|
|
409
433
|
else this.log(JSON.stringify({ sessionId: s.id, toolCallId, name, arguments: args, state: 'started' }));
|
|
410
434
|
// Register while the initiating turn is still active, before dispatch. The
|
package/package.json
CHANGED
package/remote/SKILL.md
CHANGED
|
@@ -31,9 +31,13 @@ coda_machines 与 coda_machine_diagnostics 由 Coda 内置的「机器诊断」
|
|
|
31
31
|
|
|
32
32
|
## 像用户一样创建工作会话与跟进
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
开会话前先读 `/workspace/.coda-bot/memory/<sessionId>.json`,遵守用户已记录的要求(使用哪个 Agent、哪些问题暂不处理等),并查已有问题与会话,避免重复开。用 coda_agents 确认目标目录、机器与可用 Agent,再用 coda_new_session 创建独立会话;project 必填,使用目录名字或 ID,别的机器上的目录按 ID。Agent 与其他运行参数按用户要求和任务需要选择,由用户在卡片上审阅并批准;Bot 不设父会话审批档位。
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
巡检发现问题,先开**归因会话**:只归因,不改代码、不开 PR。prompt 写清现象、证据(会话引用、日志)、时间、受影响版本与重点代码路径,要求给出归因结论、证据链、置信度及是否建议修复。
|
|
37
|
+
|
|
38
|
+
下次巡检用 coda_read_thread 读归因结论,必要时用 coda_read_tool_call 查证据。确认是需要修复的代码问题,再开**修复会话**;prompt 引用归因会话与结论,写清验收方式,要求「按仓库惯例提交、开 PR,并走完 PR 流程直到检查与交叉评审通过」。结论是偶发、外部因素,或用户说暂不处理的,记进记忆,不开修复会话。
|
|
39
|
+
|
|
40
|
+
PR 流程包含检查、交叉评审和评审自动发的评论,都是修复会话的正常工作;需要修复或重启评审时按流程处理。不要把 Bot 自己的「不要评论/不要开 issue/不要推送」等限制复制进 prompt,也不要限制修复会话走 PR 流程。
|
|
37
41
|
|
|
38
42
|
coda_new_session 每次都在 Bot 会话里出允许/拒绝卡片,工具立即返回「已请用户批准;结果会作为新消息告诉你」。不要重复调用或轮询,之后只会收到「已开出:会话引用与标题」「用户拒绝了:标题」,或没开出的「未能开出:标题(原因)」(如目标目录已删除)、「这张卡片已失效,没有开出:标题」,不会收到独立会话之后的工作结果。没开出的按原因查明后再决定是否重新提交,不原样重试。收到开出通知后记下引用,下次巡检用 coda_read_thread 读该会话、必要时 coda_read_tool_call 查看证据;不能用 coda_status 的 children 列表判断独立会话是否存在。
|
|
39
43
|
|
|
@@ -41,7 +45,7 @@ coda_new_session 每次都在 Bot 会话里出允许/拒绝卡片,工具立
|
|
|
41
45
|
|
|
42
46
|
如果 Coda 没有提供 new_session,明确告诉用户当前 Coda 不支持创建独立会话,等待升级;不要改用 spawn 或其他手段代替。
|
|
43
47
|
|
|
44
|
-
|
|
48
|
+
把用户对开会话的要求、已报告问题的指纹、证据范围、归因结论、相关会话/修复任务、最后状态按 sessionId 保存在 `/workspace/.coda-bot/memory/<sessionId>.json`。
|
|
45
49
|
同一问题无新证据不重复开任务;先读已记录会话,核实修复、拒绝或失败状态后再决定下一步。
|
|
46
50
|
|
|
47
51
|
## 接收委派与明确回传
|