ai_co_discuss 0.3.11 → 0.3.13

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 (46) hide show
  1. package/.agents/skills/discuss-participant/SKILL.md +1 -1
  2. package/backend/free_room.py +2410 -2274
  3. package/backend/schema.sql +27 -0
  4. package/backend/server.py +21 -11
  5. package/docs/architecture.md +2 -1
  6. package/docs/deployment.md +10 -0
  7. package/docs/en/deployment.md +10 -0
  8. package/docs/en/workflow.md +1 -1
  9. package/docs/runner.md +1 -1
  10. package/docs/workflow.md +2 -2
  11. package/package.json +3 -2
  12. package/runner/adapters/antigravity_app.mjs +91 -0
  13. package/runner/adapters/antigravity_cli.mjs +320 -0
  14. package/runner/adapters/antigravity_sidecar.mjs +50 -0
  15. package/runner/{api_participant.mjs → adapters/api.mjs} +19 -5
  16. package/runner/adapters/claude.mjs +313 -0
  17. package/runner/adapters/cli.mjs +126 -0
  18. package/runner/adapters/codex_app.mjs +70 -0
  19. package/runner/adapters/codex_cli.mjs +286 -0
  20. package/runner/adapters/common.mjs +196 -0
  21. package/runner/adapters/index.mjs +143 -0
  22. package/runner/adapters/profiles.mjs +116 -0
  23. package/runner/admin_account.mjs +1 -1
  24. package/runner/discussion_launch.mjs +69 -79
  25. package/runner/discussion_local.mjs +1 -1
  26. package/runner/discussion_runner.mjs +14 -13
  27. package/runner/discussion_service.mjs +3 -3
  28. package/runner/discussion_setup.mjs +1 -1
  29. package/runner/discussion_sidecar.mjs +5 -50
  30. package/runner/discussion_skills.mjs +1 -1
  31. package/runner/execution_progress.mjs +99 -0
  32. package/runner/expert_prompts.mjs +19 -29
  33. package/runner/files.mjs +10 -0
  34. package/runner/free_runner.mjs +136 -30
  35. package/runner/remote_launch.mjs +10 -5
  36. package/runner/scheduler.mjs +124 -32
  37. package/runner/session_driver.mjs +5 -3
  38. package/web/app.js +116 -23
  39. package/web/backup.js +6 -6
  40. package/web/index.html +2 -0
  41. package/web/modern.css +1 -1
  42. package/web/modern.js +1699 -1353
  43. package/runner/discussion_adapters.mjs +0 -959
  44. package/runner/discussion_models.mjs +0 -271
  45. package/runner/session_cut.mjs +0 -167
  46. /package/runner/{codex_app_client.mjs → adapters/codex_app_client.mjs} +0 -0
@@ -509,6 +509,33 @@ CREATE TABLE IF NOT EXISTS scheduler_routing (
509
509
  created_at INTEGER NOT NULL,
510
510
  PRIMARY KEY (room_id, message_id)
511
511
  );
512
+ -- Older development databases may retain execution_deliveries; it is no longer read or written.
513
+ -- What the runner's adapter reported each execution can do (stop it, see its progress), and whether its software stopped
514
+ -- once the execution was voided: the page describes a stop as it really went (a window or App the runner cannot stop
515
+ -- keeps generating). A side table so older databases need no column change; executions are not backed up, so neither is this.
516
+ CREATE TABLE IF NOT EXISTS execution_capabilities (
517
+ execution_id INTEGER PRIMARY KEY REFERENCES member_executions(id) ON DELETE CASCADE,
518
+ room_id TEXT NOT NULL,
519
+ stoppable INTEGER CHECK(stoppable IN (0,1)),
520
+ progress INTEGER CHECK(progress IN (0,1)),
521
+ stop_result TEXT NOT NULL DEFAULT '' CHECK(stop_result IN ('','stopped','unstoppable','unconfirmed'))
522
+ );
523
+ -- What a rewind does to each runner member's session, as its adapter reported: cut by the runner, rebuilt each time
524
+ -- (fresh), or kept as it is. A member without a report is kept; manual and online members are always kept.
525
+ CREATE TABLE IF NOT EXISTS member_capabilities (
526
+ member_id INTEGER PRIMARY KEY REFERENCES room_members(id) ON DELETE CASCADE,
527
+ room_id TEXT NOT NULL,
528
+ rewind TEXT NOT NULL CHECK(rewind IN ('cut','fresh','kept'))
529
+ );
530
+ -- What a member submitted for an execution the user had already voided. Refused as an answer, so it never joins the
531
+ -- transcript or AI reads; kept so the page can show the late work folded, and so a member the runner could not stop
532
+ -- is known to have finished that turn. One per execution; not backed up, like executions.
533
+ CREATE TABLE IF NOT EXISTS late_answers (
534
+ execution_id INTEGER PRIMARY KEY REFERENCES member_executions(id) ON DELETE CASCADE,
535
+ room_id TEXT NOT NULL,
536
+ text TEXT NOT NULL,
537
+ created_at INTEGER NOT NULL
538
+ );
512
539
  -- Why a request was cancelled; a side table so older databases need no column change. Requests cancelled before it existed have no row.
513
540
  CREATE TABLE IF NOT EXISTS request_cancel_reasons (
514
541
  request_id INTEGER PRIMARY KEY REFERENCES member_requests(id) ON DELETE CASCADE,
package/backend/server.py CHANGED
@@ -27,14 +27,14 @@ from .remote_launch import (COMMAND_WAIT_MAX, claim_machine_commands, finish_mac
27
27
  machine_launch_action, machine_sync, pair_machine, remote_launch_status, submit_machine_command,
28
28
  submit_remote_launch)
29
29
  from .free_room import (add_member, after_free_message, authorize_free_write, check_answer, check_proxy, execution_action, free_view,
30
- inbox, interrupt_member, member_action, member_values, migrate_free_tables, proxy_member, reserve_execution,
30
+ inbox, interrupt_member, member_action, member_capability, member_values, migrate_free_tables, proxy_member, reserve_execution,
31
31
  discussion_members, room_mode, save_room_access, sync_discussion_members, view_execution)
32
32
  from .free_room import add_discussion_member, convert_suggestion, discussion_member_action, message_suggestions, save_suggestions, sync_participants
33
33
  from .free_room import active_members, adopt_recommendation, ask_members, asks_view, checked_asks, recommend_member, register_member, route_held_messages, require_member
34
- from .free_room import describe_members, execution_notes, label_members, member_descriptions, member_labels, reorder_speakers, set_chain_rounds, speaking_order, stop_room_work
35
- from .free_room import continue_chain, execution_timeline, group_route_plan, retract_message, rewind_plan, rewind_room, withdrawals
34
+ from .free_room import describe_members, execution_notes, label_members, member_descriptions, member_labels, reorder_speakers, set_chain_rounds, speaking_order, stop_room, stop_room_work
35
+ from .free_room import ADAPTER_VALUE, continue_chain, execution_timeline, group_route_plan, retract_message, rewind_plan, rewind_room, withdrawals
36
36
  from .free_room import review_capability, review_roster, checked_review_plan, save_review_plan, check_review_retry
37
- from .free_room import review_ready, advance_review
37
+ from .free_room import review_ready, advance_review, late_answer
38
38
  from .identity import MAX_ROOM_AGENTS, PARTICIPANTS, is_ai, is_participant
39
39
  from .room_import import import_room, imported_attachment
40
40
  from .runtime import (authorize_write, begin_write, event_view,
@@ -71,6 +71,8 @@ MAX_STATE_LENGTH = 4000
71
71
  MAX_BRIEF_LENGTH = 1600
72
72
  MAX_SUMMARY_LENGTH = 1000
73
73
  PAGE_SIZE = 20
74
+ # A reader may ask for larger pages (`limit`) so a long transcript loads in one request instead of many.
75
+ MAX_PAGE_SIZE = 200
74
76
  UNCATEGORIZED_FOLDER_ID = "__uncategorized__"
75
77
  ASSETS = {"/": ("index.html", "text/html"),
76
78
  "/app.js": ("app.js", "text/javascript"),
@@ -985,7 +987,7 @@ def delete_room_records(connection, room_id):
985
987
  """Remove a room's dependent records in the caller's transaction."""
986
988
  connection.execute("DELETE FROM task_message_sources WHERE session_id IN (SELECT id FROM discussion_sessions WHERE room_id=?)", (room_id,))
987
989
  connection.execute("DELETE FROM discussion_sessions WHERE room_id=?", (room_id,))
988
- for table in ("member_recommendations", "free_queue", "speaking_orders", "member_requests", "member_descriptions", "member_labels", "execution_notes", "execution_segments", "member_executions", "member_sessions", "member_registrations", "room_members", "room_modes", "free_batches", "free_chains", "free_handoffs", "free_settings", "free_round_limits", "free_chain_holds", "room_links",
990
+ for table in ("member_recommendations", "free_queue", "speaking_orders", "member_requests", "member_descriptions", "member_labels", "execution_notes", "execution_segments", "execution_capabilities", "late_answers", "member_executions", "member_capabilities", "member_sessions", "member_registrations", "room_members", "room_modes", "free_batches", "free_chains", "free_handoffs", "free_settings", "free_round_limits", "free_chain_holds", "room_links",
989
991
  "discussion_options", "round_budgets", "scheduler_decisions", "scheduler_routing", "discussion_conclusions", "discussion_sessions"):
990
992
  if table == "scheduler_decisions":
991
993
  for child in ("scheduler_feedback", "scheduler_decision_context", "scheduler_decision_inputs"):
@@ -1397,7 +1399,8 @@ def append_free_moderation(connection, room, payload, actor):
1397
1399
  def record_reported_session(connection, room_id, token, payload):
1398
1400
  """Keep the software session a manual member posted from, so the local service can take the member over."""
1399
1401
  session_id, client = payload.get("session_id"), payload.get("session_client")
1400
- if client not in ("claude", "codex", "antigravity") or not isinstance(session_id, str) or not SESSION_ID_PATTERN.fullmatch(session_id) or not token:
1402
+ if (not isinstance(client, str) or not ADAPTER_VALUE.fullmatch(client) or not isinstance(session_id, str)
1403
+ or not SESSION_ID_PATTERN.fullmatch(session_id) or not token):
1401
1404
  return
1402
1405
  member = connection.execute("SELECT id FROM room_members WHERE room_id=? AND token_hash=? AND status='active' AND access='manual' AND adapter IN ('manual','')",
1403
1406
  (room_id, token_hash(token))).fetchone()
@@ -1825,18 +1828,19 @@ def query_messages(connection, room_id, params, token=""):
1825
1828
  """Read one bounded page; incremental cursors advance only through returned rows."""
1826
1829
  require_room(connection, room_id)
1827
1830
  page = max(1, int(params.get("page", 1)))
1831
+ size = min(MAX_PAGE_SIZE, max(1, int(params.get("limit", PAGE_SIZE))))
1828
1832
  cutoff = session_message_access(connection, room_id, params, token)
1829
1833
  where, values = message_filters(room_id, params)
1830
1834
  if cutoff is not None:
1831
1835
  where += " AND messages.id<=?"
1832
1836
  values.append(cutoff)
1833
1837
  total = connection.execute(f"SELECT COUNT(*) FROM ({MESSAGE_QUERY} WHERE {where})", values).fetchone()[0]
1834
- rows = connection.execute(f"{MESSAGE_QUERY} WHERE {where} ORDER BY messages.id LIMIT ? OFFSET ?", [*values, PAGE_SIZE + 1, (page - 1) * PAGE_SIZE]).fetchall()
1835
- messages = [message_with_attachments(connection, row) for row in rows[:PAGE_SIZE]]
1838
+ rows = connection.execute(f"{MESSAGE_QUERY} WHERE {where} ORDER BY messages.id LIMIT ? OFFSET ?", [*values, size + 1, (page - 1) * size]).fetchall()
1839
+ messages = [message_with_attachments(connection, row) for row in rows[:size]]
1836
1840
  result = {"messages": messages, "page": page, "total": total,
1837
- "pages": max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE)}
1841
+ "pages": max(1, (total + size - 1) // size)}
1838
1842
  if "after" in params:
1839
- result.update(next_after=messages[-1]["id"] if messages else int(params["after"]), has_more=len(rows) > PAGE_SIZE)
1843
+ result.update(next_after=messages[-1]["id"] if messages else int(params["after"]), has_more=len(rows) > size)
1840
1844
  return result
1841
1845
 
1842
1846
 
@@ -2561,7 +2565,7 @@ class DiscussionHandler(BaseHTTPRequestHandler):
2561
2565
  connection.commit()
2562
2566
  return self.respond(result)
2563
2567
  # A discussion keeps the local service's inbox and per-member executions for the user's direct questions.
2564
- if parts[3:4] in (["inbox"], ["executions"]) or (free and parts[3:4] in (["members"], ["suggestions"], ["recommendations"])):
2568
+ if parts[3:4] in (["inbox"], ["executions"]) or (free and parts[3:4] in (["members"], ["suggestions"], ["recommendations"], ["stop"])):
2565
2569
  return self.handle_free_api(connection, parts, method)
2566
2570
  if parts[3:4] == ["members"] and method == "POST" and len(parts) in (4, 5):
2567
2571
  if len(parts) == 4:
@@ -2785,6 +2789,8 @@ class DiscussionHandler(BaseHTTPRequestHandler):
2785
2789
  result = adopt_recommendation(connection, room_id, token, int(parts[4]), self.read_payload())
2786
2790
  elif len(parts) == 6 and parts[3] == "executions" and parts[5] == "notes" and method == "GET":
2787
2791
  result = execution_notes(connection, room_id, int(parts[4]), token)
2792
+ elif len(parts) == 6 and parts[3] == "executions" and parts[5] == "late-answer" and method == "GET":
2793
+ result = late_answer(connection, room_id, int(parts[4]), token)
2788
2794
  elif len(parts) == 5 and parts[3] == "executions":
2789
2795
  execution_id = int(parts[4])
2790
2796
  result = (view_execution(connection, room_id, execution_id, token) if method == "GET"
@@ -2795,6 +2801,10 @@ class DiscussionHandler(BaseHTTPRequestHandler):
2795
2801
  result = member_action(connection, room_id, int(parts[4]), token, self.read_payload())
2796
2802
  elif len(parts) == 6 and parts[3] == "members" and parts[5] == "interrupt" and method == "POST":
2797
2803
  result = interrupt_member(connection, room_id, int(parts[4]), token)
2804
+ elif len(parts) == 6 and parts[3] == "members" and parts[5] == "capabilities" and method == "POST":
2805
+ result = member_capability(connection, room_id, int(parts[4]), token, self.read_payload())
2806
+ elif parts[3:] == ["stop"] and method == "POST":
2807
+ result = stop_room(connection, room_id, token)
2798
2808
  else:
2799
2809
  raise LookupError("地址不存在")
2800
2810
  connection.commit()
@@ -29,7 +29,8 @@ flowchart LR
29
29
  | 目录 | 职责 |
30
30
  | --- | --- |
31
31
  | `backend/` | 两套等价后端:`server.py`(Python + SQLite)与 `worker.mjs`(Cloudflare Pages + D1),共用 `schema.sql` |
32
- | `runner/` | 统一命令 `codiscuss`、常驻讨论服务、各软件与 API 适配器、智能调度;只使用 Node.js 内置模块 |
32
+ | `runner/` | 统一命令 `codiscuss`、常驻讨论服务、房间编排、智能调度;只使用 Node.js 内置模块 |
33
+ | `runner/adapters/` | 适配层:各软件(Claude Code、Codex CLI / App、Antigravity CLI / App、模型 API)的静态事实与投递、停止、会话、模型读取,对上提供统一接口;新接一家软件只改这里 |
33
34
  | `frontend/` | Vue 界面、Markdown 渲染与备份源码 |
34
35
  | `web/` | 可直接运行的静态网页与构建产物 |
35
36
  | `.agents/skills/` | 主持、参与与报告技能 |
@@ -107,6 +107,16 @@ codiscuss token
107
107
  codiscuss health --site https://YOUR_PROJECT_NAME.pages.dev
108
108
  ```
109
109
 
110
+ ### 升级执行能力协议
111
+
112
+ 网站与 runner 必须配套升级,不能混用旧的投递方式协议。升级步骤:
113
+
114
+ 1. 暂停新请求,结束所有正在进行的本机执行,再停止 runner 和网站写入;仅在网页点击停止不能保证原软件已退出。
115
+ 2. 备份 D1 与附件,在数据库副本验证升级。重新执行 `backend/schema.sql`,创建 `execution_capabilities`、`member_capabilities`、`late_answers`;已有表缺列时须按升级版本另外补列。
116
+ 3. 更新网站与所有连接它的 runner,核对房间读取、能力上报、停止和暂缓投递,再恢复请求。
117
+
118
+ 旧 `execution_deliveries` 表保留但不再读取,也不迁移;该表未部署到线上,仅有临时测试数据。保留旧表不代表支持混版运行。本机 Python 站点同样须先结束执行,再配套更新网站与 runner。
119
+
110
120
  ### 实时推送(可选)
111
121
 
112
122
  未配置时网页每 12 秒检查一次房间是否有变化,有变化才重新读取。需要跨实例实时推送时,先部署通知 Worker,再在私有配置中绑定:
@@ -107,6 +107,16 @@ After publishing, check the page, `/api/health`, and:
107
107
  codiscuss health --site https://YOUR_PROJECT_NAME.pages.dev
108
108
  ```
109
109
 
110
+ ### Upgrading the Execution Capability Protocol
111
+
112
+ Upgrade the site and every connected runner together; the old delivery-name protocol is incompatible.
113
+
114
+ 1. Pause new requests, end all active local executions, then stop runners and site writes. A website stop alone does not prove the original software has exited.
115
+ 2. Back up D1 and attachments and verify the upgrade on a database copy. Reapply `backend/schema.sql` to create `execution_capabilities`, `member_capabilities` and `late_answers`; add any missing columns in existing tables separately for the version being upgraded.
116
+ 3. Update the site and runners, verify room reads, capability reports, stops and held deliveries, then resume requests.
117
+
118
+ The unused `execution_deliveries` table is retained without migration. It was never deployed online and contains only temporary test data. Retaining it does not provide mixed-version compatibility. Local Python sites also require executions to end before updating the site and runner together.
119
+
110
120
  ### Live Updates (Optional)
111
121
 
112
122
  Without this, the page polls every 12 seconds. For live updates across instances, deploy the notification Worker and bind it in your private configuration:
@@ -22,7 +22,7 @@ How collaboration works inside a room. For installation, see [Setup](runner.md).
22
22
  - **Expert presets** — a second row under the platforms lists presets; one click adds a member with its description, prompt, project access (and optionally model and effort) set. `Codex-评审` (read-only reviewer) and `Gemini-文档` (writes docs only, via agy) are built in; the bookmark on a card's back saves it as your own preset on the computer running the local service.
23
23
  - **Host delegation** — when the host mentions one member, that member replies to you directly, unless the host asks for the work back to hand it on (for example to a reviewer); when it mentions several, the host summarizes after they finish. One message of yours lets the host run up to 3 rounds by default (a round: the host dispatches, the members answer, the work comes back for a summary); change it (1–20 rounds) when creating the room, or later under 协作设置 (Collaboration settings) on the room page. At the limit it stops and waits for you; 继续 (Continue) in the side panel lets it run on.
24
24
  - **Everyone** — `@全体成员` asks all members to answer; `@全体依次` asks them one after another in speaking order.
25
- - **Stop and retry** — stop a member's current run from its menu; failed runs can be resent with one click.
25
+ - **Stop and retry** — stop a member's current run from its card or menu, or stop all running and queued work from the side panel. “Stopped” requires a confirmed process exit; otherwise the card says “Stop unconfirmed” or “Voided, may still be generating.” Late replies stay out of the transcript; only the owner can expand their text. New tasks wait for termination confirmation, a late reply or the original 10-minute deadline. Resuming a discussion or resending does not bypass or restart that wait. Retry puts the original message back in the composer for you to send.
26
26
  - **Retract and edit** — a message no member has started can be retracted from its “…” menu, and its text returns to the input. To change a message that was already handled, click “Edit” and resend: it and everything after it fold into a “rewound” line that AI members no longer read, and the sessions of Claude Code, Codex and agy members this machine wakes go back to before it. Manual, web and app members keep their own windows, so their sessions cannot go back. Files members changed are not restored. Neither works while a discussion is open.
27
27
  - **Branch** — “Branch from here” in an AI message's “…” menu creates a new room with the history up to that message; the Claude Code, Codex and agy members this machine wakes fork their own sessions and continue from there, while the original room and sessions stay as they were. Other members start new sessions. Both rooms share the same project directory, so files are not branched. Needs the local service.
28
28
 
package/docs/runner.md CHANGED
@@ -65,7 +65,7 @@ API 成员可以担任专家或主持人,只有房间读写工具,不访问
65
65
 
66
66
  ## 智能调度(可选)
67
67
 
68
- 建房时在“智能调度”下选择供应商并填写密钥,需要连接本机服务。作用见 [使用指南](workflow.md#智能调度)。
68
+ 建房时在“智能调度”下选择供应商并填写密钥,需要连接本机服务。卡片下方的“判断方式”可选“引导(试验)”(默认,正在打磨)或“稳定”(已通过回归评测),影响未点名消息的分派和讨论中的插话、下一位发言、下一轮判断。作用见 [使用指南](workflow.md#智能调度)。
69
69
 
70
70
  翻开成员卡片,“提示”一行可以给这位专家单独写几句要求,例如“回答先给结论”。它只发给这位专家,不改变权限;部分软件带有预设,可随时恢复。卡片背面的书签图标把这张卡片的描述、提示词、项目权限、模型与强度存为专家预设,之后在“添加”行一键加入,见 [使用指南](workflow.md#成员管理)。
71
71
 
package/docs/workflow.md CHANGED
@@ -19,8 +19,8 @@
19
19
  - **未点名的消息**:交给主持人分派;只有一位成员时直接交给它。
20
20
  - **主持人派活**:主持人点名一位成员时,该成员直接回复你;主持人需要接着安排(如做完再交评审)时会要求它做完交回,点名多位时都完成后主持人再汇总。你的一条消息最多让主持人推进 3 轮(主持人派活、成员回答、交回汇总为一轮),建房时在“推进方式”里或建房后在房间页的“协作设置”里调整(1–20 轮);到上限会停下等你,右栏点“继续”即接着推进。
21
21
  - **全体回答**:`@全体成员` 请所有成员各自回答,`@全体依次` 按发言顺序逐个回答。
22
- - **停止与重发**:成员菜单可停止当前执行;失败卡的“重发”把原用户消息填回输入框,确认后再发送。
23
- - **撤回与编辑**:还没有成员开始处理的消息可在“…”菜单撤回,原文回到输入框。已被处理的消息点“编辑”改写后重发:这条消息及之后的记录折叠为“已回退”,AI 不再读到它们,由本机唤醒的 Claude Code、Codex、agy 会话会退回到这条消息之前;手动接入、网页成员和 App 的会话在各自窗口中,无法回退。成员已改动的文件不会撤销。讨论进行中不能撤回或编辑。
22
+ - **停止与重发**:处理中卡片或成员菜单可停止该成员当前这一次;右栏“全部停止”停止所有成员并取消还没送达的请求。本机确认进程退出后才显示“已停止”,无法确认时显示“停止未确认”;自己窗口或 App 里的会话停不了时显示“已作废,可能仍在生成”。后续回答不进入记录,只有房主展开“未采纳的回答”时能读取正文。停止未确认或停不了时,新消息等待结束确认、迟到回答或原有 10 分钟期限;继续讨论、主动重发不跳过等待,也不重新计时。被停止的成员下次工作时会被告知。失败卡的“重发”把原用户消息填回输入框,确认后再发送。
23
+ - **撤回与编辑**:还没有成员开始处理的消息可撤回,原文回到输入框;最新一条消息即使已开始处理也可撤回,相当于停止对它的处理并回退到它之前。输入框为空时连按两下 Esc 也可撤回最近一条。已被处理的消息点“编辑”改写后重发:这条消息及之后的记录折叠为“已回退”,AI 不再读到它们,由本机唤醒的 Claude Code、Codex、agy 会话会退回到这条消息之前;手动接入、网页成员和 App 的会话在各自窗口中,无法回退。成员已改动的文件不会撤销。讨论进行中不能撤回或编辑。
24
24
  - **分支**:AI 发言的“…”菜单“从这里分支”会新建一个房间,保留到这条发言为止的记录,由本机唤醒的 Claude Code、Codex、agy 从各自会话分叉、从这里接着工作,原房间和原会话不变;其他成员以新会话开始。两个房间共用同一项目目录,文件不分叉。需要连接本机服务。
25
25
 
26
26
  普通成员之间的 `@` 只是引用,不会唤醒对方。多位成员共享同一项目目录时不加锁,由你划分文件;两位成员同时改动同一文件时,成员栏会提示可能的冲突。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -8,7 +8,7 @@
8
8
  "dev": "npm run build && wrangler pages dev --ip 127.0.0.1 --port 8787",
9
9
  "db:local": "wrangler d1 execute DB --local --file backend/schema.sql",
10
10
  "db:remote": "wrangler d1 execute DB --remote --file backend/schema.sql",
11
- "check": "node --check backend/worker.mjs && node --check web/app.js && node --check runner/discussion_runner.mjs && node --check runner/discussion_adapters.mjs && node --check runner/discussion_sidecar.mjs && node --check runner/codex_app_client.mjs && node --check runner/discussion_service.mjs && node --check runner/codiscuss.mjs && node --check runner/discussion_launch.mjs && node --check runner/discussion_models.mjs && node --check runner/discussion_setup.mjs && node --check runner/discussion_skills.mjs && node --check runner/discussion_tui.mjs && node --check runner/discussion_local.mjs && node --check runner/free_runner.mjs && node --check runner/session_driver.mjs && node --check runner/discussion_api.mjs && node --check runner/api_participant.mjs && node --check runner/scheduler.mjs && node --check runner/remote_launch.mjs && node --check runner/admin_account.mjs",
11
+ "check": "node --check backend/worker.mjs && node --check web/app.js && node --check runner/admin_account.mjs && node --check runner/codiscuss.mjs && node --check runner/discussion_api.mjs && node --check runner/discussion_launch.mjs && node --check runner/discussion_local.mjs && node --check runner/discussion_runner.mjs && node --check runner/discussion_service.mjs && node --check runner/discussion_setup.mjs && node --check runner/discussion_sidecar.mjs && node --check runner/discussion_skills.mjs && node --check runner/discussion_tui.mjs && node --check runner/execution_progress.mjs && node --check runner/expert_prompts.mjs && node --check runner/files.mjs && node --check runner/free_runner.mjs && node --check runner/remote_launch.mjs && node --check runner/scheduler.mjs && node --check runner/session_driver.mjs && node --check runner/adapters/antigravity_app.mjs && node --check runner/adapters/antigravity_cli.mjs && node --check runner/adapters/antigravity_sidecar.mjs && node --check runner/adapters/api.mjs && node --check runner/adapters/claude.mjs && node --check runner/adapters/cli.mjs && node --check runner/adapters/codex_app.mjs && node --check runner/adapters/codex_app_client.mjs && node --check runner/adapters/codex_cli.mjs && node --check runner/adapters/common.mjs && node --check runner/adapters/index.mjs && node --check runner/adapters/profiles.mjs",
12
12
  "runner": "node runner/discussion_runner.mjs",
13
13
  "service": "node runner/discussion_service.mjs",
14
14
  "build:ui": "vite build --config frontend/vite.config.mjs --mode markdown && vite build --config frontend/vite.config.mjs --mode backup && vite build --config frontend/vite.config.mjs",
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "files": [
70
70
  "runner/*.mjs",
71
+ "runner/adapters/*.mjs",
71
72
  "runner/*.py",
72
73
  "backend/*.py",
73
74
  "backend/schema.sql",
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Antigravity App adapter: each task is written as a request file for the CoDiscuss Sidecar the App runs
3
+ * (`antigravity_sidecar.mjs`), which starts the turn in the App; its receipt is read back on a later check. The turn runs
4
+ * inside the App, so the runner can neither stop it nor see its progress. Conversations are read through the agy adapter.
5
+ */
6
+ import { createHash } from 'node:crypto';
7
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
8
+ import { homedir } from 'node:os';
9
+ import { join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { atomic_json } from '../files.mjs';
12
+ import { antigravity_project, database_settings } from './antigravity_cli.mjs';
13
+ import { bind_access, not_sent } from './common.mjs';
14
+
15
+ const IN_APP = { stoppable: false, progress: false };
16
+
17
+ /** Queue one immutable request for the official App-managed Sidecar. */
18
+ async function deliver(config, binding, task) {
19
+ if (!task.id.startsWith('free-')) throw new Error('Sidecar 只支持自由协作执行请求,旧讨论事件已停用');
20
+ const directory = resolve(config.ipc_directory);
21
+ try {
22
+ mkdirSync(directory, { recursive: true });
23
+ const ready = JSON.parse(readFileSync(join(directory, 'ready.json'), 'utf8'));
24
+ // Only a new conversation is created in the Sidecar's project; an existing one is reached from any project.
25
+ if (!binding.session_id && (config.project_id || config.project_name || config.cwd)) {
26
+ const project = antigravity_project(config);
27
+ if (ready.project_id !== project.id) throw new Error('Sidecar 所属项目与配置不符,请先运行 setup-sidecar');
28
+ }
29
+ try { process.kill(ready.pid, 0); } catch { throw new Error('Antigravity Sidecar 尚未运行'); }
30
+ if (binding.session_id) bind_access(task.access_path, binding.session_id);
31
+ } catch (error) { throw not_sent(error); }
32
+ const path = join(directory, `${task.id}.request.json`);
33
+ if (existsSync(path)) throw new Error('Sidecar 请求已存在;必须核对回执,不自动重复发送');
34
+ atomic_json(path, { id: task.id, mode: 'free',
35
+ event_id: Number(task.id.replace(/^free-/, '')), prompt: task.prompt,
36
+ session_id: binding.session_id || '', access_path: task.access_path });
37
+ return { session_id: binding.session_id || '', status: 'sidecar_pending', capabilities: IN_APP };
38
+ }
39
+
40
+ /** The App keeps its own conversation store, apart from agy's. */
41
+ function session_path(session_id, env = process.env, home = homedir()) {
42
+ const file = join(home, '.gemini', 'antigravity', 'conversations', `${session_id}.db`);
43
+ return existsSync(file) ? file : '';
44
+ }
45
+
46
+ /** Whether the App stored a conversation by that ID on this machine. */
47
+ function session_exists(session_id, { env = process.env, home = homedir() } = {}) {
48
+ return Boolean(session_id && session_path(session_id, env, home));
49
+ }
50
+
51
+ /** Read a Sidecar's durable receipt for one task without resubmitting an uncertain request; null until it answers. */
52
+ function receipt(config, id) {
53
+ const path = join(resolve(config.ipc_directory), `${id}.result.json`);
54
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
55
+ }
56
+
57
+ /** Explicitly install an App-managed Sidecar; preserve unrelated application settings. */
58
+ export function setup_sidecar(config, root = join(homedir(), '.gemini', 'config')) {
59
+ const project = antigravity_project(config, root);
60
+ const directory = resolve(config.ipc_directory);
61
+ mkdirSync(directory, { recursive: true });
62
+ const id = `codiscuss-${createHash('sha256').update(directory).digest('hex').slice(0, 12)}`;
63
+ const sidecar = join(root, 'sidecars', id);
64
+ mkdirSync(sidecar, { recursive: true });
65
+ // Installed Sidecars keep this entry path, so it stays at runner/discussion_sidecar.mjs.
66
+ atomic_json(join(sidecar, 'sidecar.json'), { command: process.execPath,
67
+ args: [fileURLToPath(new URL('../discussion_sidecar.mjs', import.meta.url))], restart_policy: 'on-failure',
68
+ display_name: 'CoDiscuss', env: { CODISCUSS_IPC_DIRECTORY: directory } });
69
+ const path = join(root, 'config.json');
70
+ const current = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
71
+ // App 2.23 upgrades legacy entries by cloning their project. Use its observed current
72
+ // schema so the explicitly selected existing project remains the actual destination.
73
+ atomic_json(path, { ...current, sidecars: { ...current.sidecars,
74
+ [id]: { ...current.sidecars?.[id], enabled: true, projectId: project.id, schemaVersion: 1 } } });
75
+ return { sidecar_id: id, project_id: project.id, cwd: config.cwd || null };
76
+ }
77
+
78
+ /** Antigravity App's behaviour; its static facts are in profiles.mjs. */
79
+ export const antigravity = {
80
+ id: 'antigravity',
81
+ validate(participant) {
82
+ if (!participant.ipc_directory) throw new Error('缺少 Antigravity Sidecar IPC 目录');
83
+ },
84
+ // Explicit install of the Sidecar the App runs for this member (the setup-sidecar command).
85
+ setup: setup_sidecar,
86
+ deliver, receipt,
87
+ // The App's conversations are read like agy's for the model they ran with; they cannot be branched from here.
88
+ session: { path: session_path, exists: session_exists, owns: session_exists, branch: null,
89
+ settings: (id, { env = process.env, home = homedir() } = {}) => database_settings(session_path(id, env, home)) },
90
+ models: null,
91
+ };