@coda-console/grok-bot 0.2.1 → 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 CHANGED
@@ -28,7 +28,9 @@ Coda 的 `systemPrompt` 接受格式校验,但不发送给 Grok、不保存,
28
28
 
29
29
  订阅正常到期、游标重置后静默重连;暂时失败只写日志并退避。连续 3 次失败才在会话提示一次,认证失效立即提示,恢复后提示一次。Routine 刷新采用相同策略。
30
30
 
31
- 主会话订阅用 `_coda/wait` 保持活动;自主运行用 handling/waiting 切换轮次。Coda 的未结束等待项会阻止 Agent 空闲回收;退出、断网或归档时不镜像,重新加载后按游标补齐。Grok 的 Routine 执行不依赖 Coda 在线。
31
+ 插件声明 `lifecycle: "external"`,主会话订阅和 Routine 用 `_coda/wait kind: "observation"` 保持观测,Coda 显示呼吸状态;自主运行用 handling/waiting 切换观测到的轮次。Coda 的未结束等待项会阻止 Agent 空闲回收;退出、断网或归档时不镜像,重新加载后按游标补齐。Grok 的 Routine 执行不依赖 Coda 在线。Coda 不根据这些轮次建立自动回复权,也不把巡检文字自动交给委派方。
32
+
33
+ 其他会话通过 `send` 委派时,消息带原请求编号和回传地址。Bot 按任务约定调用 `coda_send`,用 `replyTo` 指定原请求编号并发送结果;`final` 默认 `true`,`false` 用于约定的进展或需要对方处理的问题。回传会唤醒原会话,不再建立反向等待。升级插件后需同步远端的 `coda-bot.mjs` 与本技能,让 Bot 的工具声明包含回传参数。
32
34
 
33
35
  ## Coda 数据与授权工具(可选)
34
36
 
@@ -44,6 +46,8 @@ node /workspace/.coda-bot/coda-bot.mjs mcp <Coda-sessionId>
44
46
 
45
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 结果由适配器处理。
46
48
 
49
+ 巡检按远端 Skill 先开只归因的独立会话(不改代码、不开 PR),prompt 写清现象、证据、时间和版本,要求结论、证据链、置信度与是否建议修复。下次巡检读取结论,确认需要修复的代码问题才开修复会话,引用归因会话与结论并走完仓库 PR 流程;偶发、外部因素或用户暂不处理的问题只记忆。用户指定的 Agent、暂不处理事项等也写进记忆,开会话前先读取遵守,不在 Skill 固定模型或推理强度。卡片审批、不重复开会话、不轮询等规则照旧;GitHub 只读限制只约束 Bot 自己。
50
+
47
51
  诊断工具需在 Coda 安装并为目录启用内置「机器诊断」插件(`diagnostics` MCP),参见 [诊断工具](https://github.com/coda-console/coda/pull/2003);遥测端点参见 [遥测 JSON 接口](https://github.com/coda-console/coda/pull/2004)。没有 new_session 的旧 Coda 会如实报不可用。
48
52
 
49
53
  ## 从 0.1.7 升级
@@ -60,9 +64,11 @@ node /workspace/.coda-bot/coda-bot.mjs mcp <Coda-sessionId>
60
64
 
61
65
  这是未获稳定性保证的私有接口,上游升级可能使登录、字段或接口失效;错误会显示在会话中。[Cursor 服务条款 §1.5](https://cursor.com/terms-of-service) 限制逆向工程等行为(法律不允许此限制的情况除外),使用前需评估该条款风险。
62
66
 
63
- - SetStatus 只提供名称/说明,映射为 tool_call 标题和状态,不伪造云端工具输入输出。thinking 只显示“正在思考”,不是思考正文。
64
- - 按 requestId/turnId 归并,成功结束且已有正文时立即核对历史并收尾;没有正文时,等到 serverTimeMs 晚于 settledAtMs 的心跳或 5 秒宽限期,再核对历史并收尾,不把正常的静默运行挂到 turnTimeout。上游没有“最后正文已落盘”标志,极迟消息仍作为补充显示,不静默丢弃。
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 秒延迟的兜底,不是上游保证。正常“工具→最终正文→结算”不增加等待。
65
71
  - entryId、updatedSeq 和正文摘要只保留按 updatedSeq 排序的最近 2000 条,每批条目合并写盘;更早条目若被服务端修订,会当作新条目呈现。断线按游标重连,游标失效分页读历史补齐并去重,不重发用户消息。首次选择只建立历史基线,不灌入旧聊天。正文被服务端省略或消息类型不支持时明确提示去 Grok 客户端查看。
66
72
  - Stop 调 InterruptGrokBotAgentRun。widget 选项变成 Coda 卡片,选择后交回 RespondGrokBotWidget;未选择则不代答。auto-review 尚未实测,收到 awaiting 时提示去 Grok 客户端处理,并记事件日志。
67
73
 
68
- 实机已验证官方登录、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。
@@ -35,7 +35,7 @@ export class RoutineWatch {
35
35
  return this.flight;
36
36
  }
37
37
  activity(run, state, title) {
38
- this.notify({ waitId: `grok-routine:${this.bot}:${run.id}`, kind: 'task', state, title });
38
+ this.notify({ waitId: `grok-routine:${this.bot}:${run.id}`, kind: 'observation', state, title });
39
39
  }
40
40
  async refresh() {
41
41
  const signal = AbortSignal.any([this.controller.signal, AbortSignal.timeout(30_000)]);
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(',')}]`;
@@ -67,7 +70,7 @@ export class Adapter {
67
70
  options: [{ value: '', name: '请选择 Bot(先发送 /login 登录)' }, ...this.bots.map(bot => ({ value: bot.agentId, name: `${bot.name || bot.agentId} (${bot.agentId})` })),
68
71
  ...(!this.bots.some(b => b.agentId === s.saved.bot) && s.saved.bot ? [{ value: s.saved.bot, name: s.saved.bot }] : [])] }];
69
72
  }
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 }); }
73
+ wait(s, state) { if (this.waits) this.rpc.notify('_coda/wait', { sessionId: s.id, waitId: s.subscriptionId ?? 'grok-native', kind: 'observation', title: 'Grok 主会话观测', state }); }
71
74
  async refreshBots() {
72
75
  if (!this.auth.client) return;
73
76
  const result = await this.auth.client.request('ListGrokBotAgents', {});
@@ -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) { this.say(s, 'Grok 上一轮补充消息:'); s.turn.settlement = { turnId: body.requestId, outcome: 'success', settledAtMs: String(this.now()) }; }
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
- // Preserve late native output as an explicit supplement, never silently drop it.
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
- if (live.isRunning && !s.turn) this.start(s, true);
289
- if (live.isRunning && s.turn) {
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
- if (title !== s.turn.activityTitle) {
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.activityTitle = title; s.turn.activityId = randomUUID();
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 && !live.isRunning) s.lastSettlement = settlement;
311
- if (!live.isRunning && settlement && !s.closedRuns.has(settlement.turnId) && !(event.snapshot && !s.turn)) {
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
- 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);
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, allowEmpty) {
379
- if (s.turn !== turn || turn.reconciling || turn.widgetPending || (!turn.hasReply && !allowEmpty)) return;
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 { turn.reconciling = false; }
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
- const toolCall = { toolCallId, title: `Coda · ${name}`, kind: 'other', status: 'pending', rawInput: args };
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
@@ -466,7 +490,7 @@ export class Adapter {
466
490
  if (typeof ticket !== 'string' || !ticket || s.turn !== turn) fail('没有有效的 Coda turn 票据');
467
491
  forwarded = { ...args, turn: ticket, requestId: requestId(turn, name, args) };
468
492
  }
469
- const mayWaitForApproval = name === 'send';
493
+ const mayWaitForApproval = name === 'send' && args.replyTo === undefined;
470
494
  // Approval calls outlive a Coda turn and must not inherit its abort signal
471
495
  // or the MCP client's default 60-second timeout (null disables both).
472
496
  return this.trace(s, name, args, () => this.provider(s, name).call(name, forwarded, mayWaitForApproval ? null : signal),
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "id": "grok-bot",
4
4
  "displayName": "Grok Bot",
5
5
  "description": "通过 Grok Bot 原生主会话接入 Coda,镜像回复与 Routine 运行记录。",
6
- "version": "0.2.1",
6
+ "version": "0.3.1",
7
7
  "coda": {
8
8
  "channels": ["preview"]
9
9
  },
@@ -32,6 +32,7 @@
32
32
  "agents": [
33
33
  {
34
34
  "id": "grok-bot",
35
+ "lifecycle": "external",
35
36
  "displayName": "Grok Bot",
36
37
  "icon": {
37
38
  "light": "icons/grok.pdf",
package/remote/SKILL.md CHANGED
@@ -31,9 +31,13 @@ coda_machines 与 coda_machine_diagnostics 由 Coda 内置的「机器诊断」
31
31
 
32
32
  ## 像用户一样创建工作会话与跟进
33
33
 
34
- 需要修复时,先用 coda_agents 确认目标目录、机器与可用 Agent,再用 coda_new_session 创建独立会话。project 必填,使用目录名字或 ID;别的机器上的目录按 ID。默认选择 `agent: "codex"`、`mode: "agent"`,其他 model、effort、options 按任务需要写明;用户在卡片上审阅并批准这些参数。Bot 不设父会话审批档位。
34
+ 开会话前先读 `/workspace/.coda-bot/memory/<sessionId>.json`,遵守用户已记录的要求(使用哪个 Agent、哪些问题暂不处理等),并查已有问题与会话,避免重复开。用 coda_agents 确认目标目录、机器与可用 Agent,再用 coda_new_session 创建独立会话;project 必填,使用目录名字或 ID,别的机器上的目录按 ID。Agent 与其他运行参数按用户要求和任务需要选择,由用户在卡片上审阅并批准;Bot 不设父会话审批档位。
35
35
 
36
- prompt 写清证据(会话引用、日志、时间和受影响版本)、重点代码路径、验收方式,以及「按仓库惯例提交、开 PR,并走完 PR 流程直到检查与交叉评审通过」。PR 流程包含检查、交叉评审和评审自动发的评论,都是正常工作;需要修复或重启评审时按流程处理。不要把 Bot 自己的「不要评论/不要开 issue/不要推送」等限制复制进 prompt,也不要限制工作会话走 PR 流程。
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,5 +45,14 @@ coda_new_session 每次都在 Bot 会话里出允许/拒绝卡片,工具立
41
45
 
42
46
  如果 Coda 没有提供 new_session,明确告诉用户当前 Coda 不支持创建独立会话,等待升级;不要改用 spawn 或其他手段代替。
43
47
 
44
- 把已报告问题的指纹、证据范围、相关会话/修复任务、最后状态按 sessionId 保存在 `/workspace/.coda-bot/memory/<sessionId>.json`。
48
+ 把用户对开会话的要求、已报告问题的指纹、证据范围、归因结论、相关会话/修复任务、最后状态按 sessionId 保存在 `/workspace/.coda-bot/memory/<sessionId>.json`。
45
49
  同一问题无新证据不重复开任务;先读已记录会话,核实修复、拒绝或失败状态后再决定下一步。
50
+
51
+ ## 接收委派与明确回传
52
+
53
+ 你的生命周期由 Grok 管理。Coda 观测到的巡检、定时运行和自主发言不会自动回复其他会话。
54
+ 收到其他会话的委派时,保留消息里的原请求编号与回传地址。仅在任务约定需要汇报,或需要对方处理问题时,调用 `coda_send`:
55
+ `childId` 是原发起会话地址,`replyTo` 是原请求编号,`prompt` 是回传正文。
56
+ `final` 默认 `true`,表示这次委派已完成;进展或待处理问题用 `final: false`。最终回传后不能继续向这次委派回传。
57
+ 回传已获原委派授权,不再等待反向审批,也不会让你等待对方的回复;它会主动唤醒对方,不用轮询。
58
+ 没有 `replyTo` 的 `coda_send` 仍是普通消息,按原来的审批规则处理。例行巡检没有需要回报的事情时,不调用回传工具。
@@ -35,7 +35,11 @@ export const TOOLS = [
35
35
  proxy('workflow_list', '列出项目的工作流程。', { project: string('项目名称或 ID') }),
36
36
  proxy('new_session', '像用户一样在目标目录开一个独立会话,每次要用户在 Coda 卡片上批准;不是你的子会话。立即返回请用户批准的回执,只会收到已开出/被拒绝/未能开出通知,工作结果不会推给你;记下会话引用,之后用 coda_read_thread 跟进,不要重复开同一任务。任务写清证据、重点代码路径、验收方式、按仓库惯例开 PR 并走完 PR 流程直到评审通过(含检查、交叉评审和自动评审评论)。GitHub 只读限制仅约束 Bot 自己,不要转述进任务或限制会话走 PR 流程。',
37
37
  { project: string('必填:目标目录名字或 ID;别的机器上的目录按 ID'), agent: string('Agent ID,默认选择 codex'), prompt: string('证据、重点代码路径、验收方式;按仓库惯例提交、开 PR 并走 PR 流程直到评审通过'), title: string('会话标题'), model: string('模型'), effort: string('推理强度'), mode: string('独立会话的审批档位,codex 默认选择 agent;用户在卡片上批准'), options: anyObject }, ['project', 'agent', 'prompt']),
38
- proxy('send', '给已有会话发消息,每次由 Coda 授权卡片批准。可能先返回「已提交」;不要重试或轮询。', { childId: string('目标会话 ID 或引用(沿用 Coda 参数名,不表示子会话)'), prompt: string('消息正文') }, ['childId', 'prompt']),
38
+ proxy('send', '给已有会话发消息,由 Coda 授权。明确回传委派结果时带 replyTo,直接唤醒原请求方,不建立反向等待。可能先返回「已提交」;不要轮询。', {
39
+ childId: string('目标会话 ID 或引用(沿用 Coda 参数名,不表示子会话);回传时用原委派附带的地址'), prompt: string('消息或回传正文'),
40
+ replyTo: string('原委派附带的请求编号,只能回传给该请求的发起方'),
41
+ final: { type: 'boolean', description: '仅回传时使用,默认 true 表示委派完成;约定的进展或需要对方处理的问题用 false' },
42
+ }, ['childId', 'prompt']),
39
43
  ];
40
44
 
41
45
  async function jsonPost(address, path, body, token, fetcher = fetch) {