ai_co_discuss 0.4.3 → 0.4.5

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.
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: local-stt-setup
3
+ description: "为 AI CoDiscuss 安装本机语音识别:下载 CrispASR 与 Qwen3-ASR 模型,在这台电脑上把语音输入转成文字,并配置为网页“设置 → 语音 → 语音输入”里的“本机”服务。用户要求安装、修复、升级或卸载本地语音识别、本机语音输入、离线 ASR 时使用;不用于填写火山引擎等云端语音的密钥,本机朗读见 local-tts-setup。"
4
+ ---
5
+
6
+ # 本机语音识别(local-stt-setup)
7
+
8
+ 装好后的样子:
9
+
10
+ - `SERVICE_HOME/crispasr/cli/`:CrispASR 程序(`crispasr.exe` 及其 DLL);`SERVICE_HOME/crispasr/models/qwen3-asr-0.6b-q8_0.gguf`:识别模型(约 1 GB)。`SERVICE_HOME` 见“前提”第 2 条。
11
+ - 访问令牌存在 CoDiscuss 的本机私有密钥里(地址 `http://127.0.0.1:18766`),不写进其他文件或命令行。
12
+ - 语音输入切到“本机”。CrispASR 由 CoDiscuss 本机程序在识别时自动启动,只监听 `127.0.0.1:18766`;1 分钟没有识别请求自动结束并释放显存(CoDiscuss 崩溃也一样),不需要开机自启。
13
+
14
+ 全程只在用户电脑上操作:不改 CoDiscuss 源码,不把令牌写进回复、日志或命令行参数;下载与安装前把预计占用(下文“前提”)告诉用户。
15
+
16
+ ## 前提
17
+
18
+ 依次检查,任何一项不满足就停下说明,不硬装:
19
+
20
+ 1. **CoDiscuss 版本**:运行 `codiscuss launch tts-settings`,输出的 `stt_catalog` 里要有 `"id": "local"`。没有说明版本过旧,请用户先更新(`npm install -g ai_co_discuss@latest`,再 `codiscuss service restart`)后继续。
21
+ 2. **服务目录**:设置了环境变量 `DISCUSS_SERVICE_HOME` 时用它,否则是 `~/.codiscuss`。下文记为 `SERVICE_HOME`。
22
+ 3. **程序包**(GitHub `CrispStrobe/CrispASR` 的 Release 资产):
23
+
24
+ | 平台 | 判断 | 资产 |
25
+ |---|---|---|
26
+ | Windows + NVIDIA | `nvidia-smi` 的驱动主版本 ≥ 580 | `crispasr-windows-x86_64-cuda13.zip`(RTX 50 系可用) |
27
+ | Windows + NVIDIA | 驱动更旧 | `crispasr-windows-x86_64-cuda.zip`(CUDA 12.8,也支持 RTX 50 系),驱动太旧则 `-cuda126.zip` |
28
+ | Windows + AMD / Intel 独显 | — | `crispasr-windows-x86_64-vulkan.zip` |
29
+ | Windows 无独显 | — | `crispasr-windows-x86_64-cpu.zip` |
30
+ | macOS Apple Silicon | — | `crispasr-macos-arm64.tar.gz` |
31
+ | Linux x64 | NVIDIA / 其他独显 / 无 | `crispasr-linux-x86_64-cuda13.tar.gz`(或 `-cuda`)/ `-vulkan.tar.gz` / `crispasr-linux-x86_64.tar.gz` |
32
+
33
+ 只能用 CPU 时先告诉用户:识别速度约等于录音时长(一段 1 分钟的话要等约 1 分钟),用户同意再装。
34
+ 4. **显存与磁盘**:识别服务运行时约占 2.5 GB 显存;与本机朗读(约 3 GB)同时运行时,8 GB 显卡余量很紧,两者都是用完 1 分钟后自动退出。磁盘约 1.7 GB(Windows CUDA 13 程序约 0.7 GB + 模型约 1 GB)。
35
+
36
+ ## 安装
37
+
38
+ 命令以 Windows 为例,macOS / Linux 的可执行文件是 `crispasr`。下载走用户当前的网络设置;经代理访问 GitHub 时偶尔会 TLS 握手失败(`schannel: failed to receive handshake`),用 `curl --retry 3 --retry-all-errors` 重试即可,仍失败再问用户是否换网络或代理。
39
+
40
+ 1. **下载程序**:读 `https://api.github.com/repos/CrispStrobe/CrispASR/releases/latest`,取上表对应资产,记下资产的 `digest`(`sha256:...`)。下载到空目录 `SERVICE_HOME/crispasr/download/`,核对 SHA-256 与 `digest` 一致后解压。压缩包里有一层以资产名命名的文件夹,把其中全部文件移到 `SERVICE_HOME/crispasr/cli/`,使 `cli/crispasr.exe` 与 DLL 在同一层。已装过时先结束正在运行的 crispasr 进程再覆盖。用 `crispasr.exe --version` 确认。
41
+ 2. **模型**:下载 `https://huggingface.co/cstr/qwen3-asr-0.6b-GGUF/resolve/main/qwen3-asr-0.6b-q8_0.gguf` 到 `SERVICE_HOME/crispasr/models/`(约 1 GB)。不要用文件名带 `imatrix` 的版本(长录音可能陷入重复)。
42
+ 3. **令牌**:生成 64 位十六进制随机串,写入临时文件 `{"base_url": "http://127.0.0.1:18766", "api_key": "<令牌>"}`,运行 `codiscuss launch api-key --file <临时文件>`,随即删除临时文件。
43
+ 4. **切换语音输入**:把 `codiscuss launch tts-settings` 的输出存成临时文件,只改 `stt_provider` 为 `"local"`,其余字段不动(读出的 `catalog`、`stt_catalog` 等字段保存时会被忽略),用 `codiscuss launch tts-settings --file <临时文件>` 保存后删除临时文件。用户只要求安装、不想马上切换时跳过这一步,之后可在网页“设置 → 语音 → 语音输入”里点“本机”。
44
+ 5. **验证**:先结束正在运行的 crispasr 进程(旧令牌启动的服务会拒绝新令牌)。准备一段 16 kHz 单声道 16 位 WAV(可用本机朗读或任意录音转换),去掉 44 字节文件头得到 PCM,base64 后写入临时文件 `{"upload_id": "<新 UUID>", "index": 0, "data": "<base64>", "final": true}`(单段不超过 256 KB,约 8 秒;更长的录音按序号分段,只有最后一段带 `"final": true`),运行 `codiscuss launch stt --file <临时文件>`:返回 `text` 即成功,`SERVICE_HOME/crispasr/serve.log` 会出现 `crispasr-server: transcribed ...`。第一次要启动服务并加载模型,约 5–15 秒。参考:RTX 5060 8 GB + `cuda13`,服务启动后约 35 倍实时,5 分钟录音约 8.5 秒。
45
+ 6. **清理与汇报**:删除 `SERVICE_HOME/crispasr/download/` 与所有临时文件。向用户报告程序包、模型与实测耗时,并说明:中英混说、标点与英文大小写识别较好;逐字母念的缩写(如 npm 念成 N-P-M)和代码标识符(如 `tts_providers`)容易识别成别的写法;切回火山引擎只需在设置里点选。
46
+
47
+ ## 修复、升级与卸载
48
+
49
+ - **修复**:读 `SERVICE_HOME/crispasr/serve.log`,对照下表处理;必要时从出问题的那一步重做,令牌变了就重做第 3 步并结束旧进程。
50
+ - **升级 CrispASR**:结束 crispasr 进程,重做第 1 步。
51
+ - **卸载**:先在设置里把语音输入切回火山引擎(或用第 4 步的方法改 `stt_provider`),结束 crispasr 进程,删除 `SERVICE_HOME/crispasr/`。
52
+
53
+ | 现象 | 原因与处理 |
54
+ |---|---|
55
+ | 本机语音识别尚未安装 | 没找到令牌或 `cli/crispasr.exe`:重做第 1、3 步 |
56
+ | 服务拒绝了令牌 | 18766 端口上是旧令牌启动的 crispasr:结束该进程后重试 |
57
+ | 本机服务启动超时 | 看 `serve.log`:缺 DLL 时重做第 1 步(确认文件已移到 `cli/` 同一层);CUDA 报错时检查驱动,或换 `-vulkan` / `-cpu` 包;端口被占用时请用户释放 18766 |
58
+ | 识别失败(HTTP 500) | 看 `serve.log`;显存不足(out of memory)时关闭占显存的程序,或等本机朗读用完自动退出后再试 |
@@ -123,7 +123,7 @@ project_access: write
123
123
  - **工作流 ID**(文件夹名 = SKILL.md 的 `name`):小写英文和数字,可用单个连字符连接(不在首尾),1—40 位。`label` 最多 32 字。
124
124
  - **`description`** 最多 500 字:写清做什么、什么时候用。主持人只凭它判断一条请求该不该按这个工作流做,写含糊了,该用时会不用、不该用时会套用。
125
125
  - **卡片**:最多 30 张。`name` 是卡片 ID,等于文件名,只用小写英文、数字和下划线,最多 16 位。`platform` 是平台 ID:`Claude`、`Codex`、`Antigravity`、`DeepSeek`、`Qwen`、`Grok`、`OpenAI`、`Custom`(网页显示的 Gemini 写 `Antigravity`,自定义 API 写 `Custom`),须是本机已配置的软件,或已配置的模型 API(官方平台可按已保存的 API Key 加入;Custom 须先保存完整的本机 API 配置)。成员名由程序拼成 `平台-卡片ID`。
126
- - **卡片可选字段**:`label`、`description`、`project_access`(read / write / auto / full)、`model`、`effort`。不用的字段直接省略;模型和强度省略即用本机默认值。接入方式、可执行文件、密钥不写进卡片,由各自电脑的配置决定。
126
+ - **卡片可选字段**:`label`、`description`、`project_access`(read / write / auto / full)、`model`、`effort`。不用的字段直接省略;模型和强度省略即用本机默认值。接入方式、可执行文件、密钥不写进卡片,由各自电脑的配置决定。各角色挑哪个平台和模型,见 [给卡片挑平台和模型](references/model-selection.md)。
127
127
  - 头信息只支持单行 `key: value`;不支持多行值、列表和嵌套。
128
128
  - 卡片正文留空时使用该软件的默认提示。
129
129
 
@@ -0,0 +1,23 @@
1
+ # 给卡片挑平台和模型
2
+
3
+ > 依据 2026-11 的社区口碑整理,不是实测。模型迭代快,拿不准时问用户,用户的指定优先。
4
+
5
+ 按角色要做的活挑,不按“谁最强”挑:贵模型放在规划、审查等决定质量的位置,便宜模型放在量大、规则明确的执行位置。
6
+
7
+ | 平台 | 擅长 | 注意 | 适合的角色 |
8
+ |---|---|---|---|
9
+ | Claude | 写代码执行、前端 UI、长任务先拆再做、叙事与文案 | 额度消耗快,部分地区账号不稳定 | 执行者、写作者、编剧 |
10
+ | OpenAI / Codex(GPT) | 推理、审查挑错、复杂排障 | 慢;中文措辞防御性重(“并不代表”),不适合写正文 | 审稿、验收、规划 |
11
+ | Antigravity(Gemini) | 中文文笔、知识面广、幻觉低、读长文 | 用词偏西方;最强型号未公开 | 写作者、资料研究 |
12
+ | DeepSeek | 快、便宜、规则明确的执行稳定 | 复杂任务吃力;最高思考档效率低,用中高档 | 记录、整理、批量执行 |
13
+ | Qwen | 可本地部署 | 思考久、编程口碑一般 | 本地或离线场景 |
14
+ | Grok | 单价低、上下文长 | 修复与工具调用口碑差 | 一般不首选 |
15
+ | Custom:Kimi K3 | 国产上限、前端还原、规划 | 慢、贵、额度少;新版措辞变保守 | 规划、前端 |
16
+ | Custom:GLM-5.3 | 后端与严肃逻辑可靠;中文小说文笔有人推荐 | 慢、高峰限流 | 执行、审核、写作备选 |
17
+ | Custom:MiniMax | 价格低 | 能力口碑一般 | 便宜杂活 |
18
+
19
+ 写卡片时:
20
+
21
+ - 只写本机已配置的平台;不确定时 `model`、`effort` 省略,用本机默认值。
22
+ - 纯 API 平台(`OpenAI`、`DeepSeek`、`Qwen`、`Grok`、`Custom`)的成员不读 `effort`,没有命令行,只能用文件工具和 `commands.json` 登记的命令;需要跑脚本的角色,要么登记命令,要么用 `Claude`、`Codex`、`Antigravity` 这类本机软件。
23
+ - 同一件事要两个视角审时,两张卡片用不同平台,避免同一家模型的盲点重叠。
package/README.md CHANGED
@@ -30,7 +30,7 @@ Most multi-agent frameworks spin up fresh agents inside their own runtime. AI Co
30
30
  - **Structured discussions** — set a goal and round / time limits; the host schedules speakers and follow-ups, then the room returns to free collaboration with history and conclusions kept.
31
31
  - **Goal mode** — give the host an objective with `/goal`; whenever the room goes idle the host is woken to carry on, until it confirms the goal is complete, gets blocked, a budget runs out or you end it.
32
32
  - **Smart Scheduling** — optionally let a decision model handle routine routing instead of waking the host.
33
- - **Workflows** — save a way of working as a host playbook (`SKILL.md`) plus a set of agent cards and share it as a folder. Manage them (view, create, import, edit, rename, disable, delete) in the 工作流 panel opened from the channel tools, even with no room open; start one later by asking the host in the message box or selecting it with `/` and confirming its members. Next to its `SKILL.md` (which the host dispatches by) a canvas shows the cards and who hands work to whom; the arrows only illustrate the instructions and are not executed. A workflow brings the method and the people, not a guaranteed procedure. See [runner docs](docs/runner.md#工作流可选).
33
+ - **Workflows** — save a way of working as a host playbook (`SKILL.md`) plus a set of agent cards and share it as a folder. Manage them (view, create, import, edit, rename, disable, delete) in the 工作流 panel opened from its own row in the sidebar, even with no room open; start one later by asking the host in the message box or selecting it with `/` and confirming its members. Next to its `SKILL.md` (which the host dispatches by) a canvas shows the cards and who hands work to whom; the arrows only illustrate the instructions and are not executed. A workflow brings the method and the people, not a guaranteed procedure. See [runner docs](docs/runner.md#工作流可选).
34
34
  - **Mix any participant** — local CLIs and desktop apps, model APIs and manually connected sessions in one room.
35
35
  - **Shared workspace** — point a room at a project directory, grant each local agent read-only or writable access, and browse files with text, Markdown and static HTML previews from the room.
36
36
  - **Live roster** — invite, pause, recall or remove members at any time, including resuming an existing agent session.
@@ -51,6 +51,10 @@ ROOM_EVENT_PAGE = 500
51
51
  # and answer sit under that answer; later messages (the user asking something meanwhile) then stay above it.
52
52
  ANCHOR_SQL = """CASE WHEN r.purpose='handoff' THEN COALESCE((SELECT MAX(a.answer_id) FROM member_requests a
53
53
  WHERE a.room_id=r.room_id AND a.batch_id=r.message_id AND a.purpose<>'handoff'), r.message_id) ELSE r.message_id END"""
54
+ # The room's latest message when a request (aliased `r`) was made. A relay step dispatched from an answer that sits in an
55
+ # older message's slot, after the user posted meanwhile, goes under the latest top-level message rather than back up there.
56
+ DISPATCHED_AFTER_SQL = """(SELECT MAX(m.id) FROM messages m WHERE m.room_id=r.room_id AND m.id>=r.message_id
57
+ AND m.id<=COALESCE(r.answer_id,m.id) AND CAST(strftime('%s',m.created_at) AS INTEGER)<=r.created_at)"""
54
58
  # Rounds used and the owner's additions in the room's recent chains, for "第 2/3 轮"; parameter: the room.
55
59
  # A round is a host dispatch (a batch) or a continue the host asked for (`free_host_wakes` with a `continue:` key); a
56
60
  # relay's later steps in the same pass (`free_relay_steps`) ride on its first, so a whole pass is one round.
@@ -143,7 +147,7 @@ SESSION_MODES = ("new", "borrowed")
143
147
  FREE_OPERATIONS = ("messages", "attachments", "move", "folder", "rename", "archive", "restore", "delete")
144
148
  MEMBER_FIELDS = ("id", "name", "adapter", "role", "status", "access", "session_mode", "read_until")
145
149
  EXECUTION_FIELDS = ("id", "member_id", "status", "session_id", "lease_until", "created_at", "finished_at", "result",
146
- "activity", "activity_at")
150
+ "activity", "activity_at", "paused_at")
147
151
  # What the runner found when a voided execution's software was to stop; '' until it reports.
148
152
  STOP_RESULTS = ("stopped", "unstoppable", "unconfirmed")
149
153
  # How long a member voided where the runner could not stop it gets no new delivery unless its late answer shows the turn ended.
@@ -154,24 +158,28 @@ REWIND_EFFECTS = ("cut", "fresh", "kept")
154
158
  CAPABILITY_COLUMNS = """(SELECT stoppable FROM execution_capabilities c WHERE c.execution_id=e.id) AS stoppable,
155
159
  (SELECT progress FROM execution_capabilities c WHERE c.execution_id=e.id) AS progress,
156
160
  (SELECT stop_result FROM execution_capabilities c WHERE c.execution_id=e.id) AS stop_result,
157
- (SELECT steer FROM execution_capabilities c WHERE c.execution_id=e.id) AS steer"""
161
+ (SELECT steer FROM execution_capabilities c WHERE c.execution_id=e.id) AS steer,
162
+ (SELECT resume FROM execution_capabilities c WHERE c.execution_id=e.id) AS resume"""
158
163
 
159
164
 
160
165
  def reported_capabilities(row):
161
- """`{stoppable, progress, steer}` as the runner reported them for one execution, or None before it did (or an older runner).
162
- `steer`: the owner can add an interjection the running software takes without ending its turn."""
166
+ """`{stoppable, progress, steer, resume}` as the runner reported them for one execution, or None before it did (or an older runner).
167
+ `steer`: the owner can add an interjection the running software takes without ending its turn. `resume`: the runner
168
+ can stop the software's turn and continue the same session, so the owner may pause the execution without ending it."""
163
169
  if row["stoppable"] is None:
164
170
  return None
165
- return {"stoppable": bool(row["stoppable"]), "progress": bool(row["progress"]), "steer": bool(row["steer"])}
171
+ return {"stoppable": bool(row["stoppable"]), "progress": bool(row["progress"]), "steer": bool(row["steer"]),
172
+ "resume": bool(row["resume"])}
166
173
  # Columns added after the first free-room tables; Python databases created by that build gain them here.
167
174
  LATER_COLUMNS = {"room_members": {"access": "TEXT NOT NULL DEFAULT 'runner'", "session_mode": "TEXT NOT NULL DEFAULT 'new'"},
168
175
  "member_executions": {"changes": "TEXT NOT NULL DEFAULT ''", "edited": "TEXT NOT NULL DEFAULT ''", "cwd": "TEXT NOT NULL DEFAULT ''",
169
- "activity": "TEXT NOT NULL DEFAULT ''", "activity_at": "INTEGER NOT NULL DEFAULT 0"},
176
+ "activity": "TEXT NOT NULL DEFAULT ''", "activity_at": "INTEGER NOT NULL DEFAULT 0",
177
+ "paused_at": "INTEGER NOT NULL DEFAULT 0"},
170
178
  "member_requests": {"chain_id": "INTEGER NOT NULL DEFAULT 0", "batch_id": "INTEGER NOT NULL DEFAULT 0",
171
179
  "purpose": "TEXT NOT NULL DEFAULT 'ask'", "created_by": "TEXT NOT NULL DEFAULT 'user'"},
172
180
  "free_chain_holds": {"asked_rounds": "INTEGER NOT NULL DEFAULT 0", "asked_reason": "TEXT NOT NULL DEFAULT ''"},
173
181
  "free_sequences": {"gates": "TEXT NOT NULL DEFAULT '[]'"},
174
- "execution_capabilities": {"steer": "INTEGER"}}
182
+ "execution_capabilities": {"steer": "INTEGER", "resume": "INTEGER"}}
175
183
 
176
184
 
177
185
  def migrate_free_tables(connection):
@@ -742,6 +750,7 @@ def recent_executions(connection, room_id):
742
750
  (SELECT MAX(answer_id) FROM member_requests WHERE execution_id=e.id) AS answer_id,
743
751
  (SELECT MIN(message_id) FROM member_requests WHERE execution_id=e.id) AS request_message_id,
744
752
  (SELECT MIN({ANCHOR_SQL}) FROM member_requests r WHERE r.execution_id=e.id) AS anchor_message_id,
753
+ (SELECT MIN({DISPATCHED_AFTER_SQL}) FROM member_requests r WHERE r.execution_id=e.id) AS dispatched_after,
745
754
  (SELECT COUNT(*) FROM execution_notes WHERE execution_id=e.id) AS note_count,
746
755
  (SELECT denials FROM execution_denials WHERE execution_id=e.id) AS denials,{CAPABILITY_COLUMNS},
747
756
  (SELECT json_object('created_at',created_at) FROM late_answers WHERE execution_id=e.id) AS late_answer
@@ -754,7 +763,7 @@ def recent_executions(connection, room_id):
754
763
  executions.append({**public_execution(row), "member": row["member"], "cwd": row["cwd"], "changes": json.loads(row["changes"] or "[]"),
755
764
  "changes_recorded": bool(row["changes"]), "request_ids": json.loads(row["request_ids"]),
756
765
  "answer_id": row["answer_id"], "request_message_id": row["request_message_id"],
757
- "anchor_message_id": row["anchor_message_id"], "requests": requests,
766
+ "anchor_message_id": row["anchor_message_id"], "dispatched_after": row["dispatched_after"], "requests": requests,
758
767
  "notes": row["note_count"], "denials": json.loads(row["denials"] or "[]"),
759
768
  "capabilities": reported_capabilities(row), "stop_result": row["stop_result"] or "",
760
769
  "late_answer": json.loads(row["late_answer"]) if row["late_answer"] else None,
@@ -2551,13 +2560,16 @@ def add_interjection(connection, room_id, row, payload, now):
2551
2560
  if earlier["execution_id"] != row["id"] or earlier["text"] != text:
2552
2561
  raise FileExistsError("submission_id 已用于另一条插话")
2553
2562
  return public_interjection(earlier, row["status"])
2554
- capability = connection.execute("SELECT steer FROM execution_capabilities WHERE execution_id=?", (row["id"],)).fetchone()
2555
- if not capability or not capability["steer"]:
2563
+ capability = connection.execute("SELECT steer,resume FROM execution_capabilities WHERE execution_id=?", (row["id"],)).fetchone()
2564
+ if not capability or not (capability["steer"] or capability["resume"]):
2556
2565
  raise FileExistsError("这位成员的软件不支持执行中插话,可停止后带上这句话重问")
2557
2566
  if connection.execute("SELECT COUNT(*) FROM execution_interjections WHERE execution_id=?", (row["id"],)).fetchone()[0] >= MAX_INTERJECTIONS:
2558
2567
  raise FileExistsError(f"一次执行最多插话 {MAX_INTERJECTIONS} 条")
2559
2568
  cursor = connection.execute("""INSERT INTO execution_interjections (execution_id,room_id,submission_id,text,created_at,updated_at)
2560
2569
  VALUES (?,?,?,?,?,?)""", (row["id"], room_id, submission_id, text, now, now))
2570
+ # Software that cannot take it mid-turn hears it by a pause: the runner stops the turn and resumes with it.
2571
+ if not capability["steer"] and not row["paused_at"]:
2572
+ connection.execute("UPDATE member_executions SET paused_at=? WHERE id=?", (now, row["id"]))
2561
2573
  return public_interjection(connection.execute("SELECT * FROM execution_interjections WHERE id=?", (cursor.lastrowid,)).fetchone(), row["status"])
2562
2574
 
2563
2575
 
@@ -2639,12 +2651,13 @@ def execution_action(connection, room_id, execution_id, token, payload):
2639
2651
  # What this delivery can do, as the runner's adapter reported; a retried delivery may differ, so the latest counts.
2640
2652
  if type(payload.get("stoppable")) is not bool or type(payload.get("progress")) is not bool:
2641
2653
  raise ValueError("delivery 须带布尔值 stoppable 与 progress")
2642
- # Older runners report no `steer`; their deliveries take no interjection.
2643
- if type(payload.get("steer", False)) is not bool:
2644
- raise ValueError("delivery 的 steer 须为布尔值")
2645
- connection.execute("""INSERT INTO execution_capabilities (execution_id,room_id,stoppable,progress,steer) VALUES (?,?,?,?,?)
2646
- ON CONFLICT(execution_id) DO UPDATE SET stoppable=excluded.stoppable,progress=excluded.progress,steer=excluded.steer""",
2647
- (execution_id, room_id, int(payload["stoppable"]), int(payload["progress"]), int(payload.get("steer", False))))
2654
+ # Older runners report no `steer` or `resume`; their deliveries take no interjection and no pause.
2655
+ if type(payload.get("steer", False)) is not bool or type(payload.get("resume", False)) is not bool:
2656
+ raise ValueError("delivery 的 steer 与 resume 须为布尔值")
2657
+ connection.execute("""INSERT INTO execution_capabilities (execution_id,room_id,stoppable,progress,steer,resume) VALUES (?,?,?,?,?,?)
2658
+ ON CONFLICT(execution_id) DO UPDATE SET stoppable=excluded.stoppable,progress=excluded.progress,steer=excluded.steer,
2659
+ resume=excluded.resume""", (execution_id, room_id, int(payload["stoppable"]), int(payload["progress"]),
2660
+ int(payload.get("steer", False)), int(payload.get("resume", False))))
2648
2661
  elif action == "interjection":
2649
2662
  # The runner's report on one interjection: it reached the software, or why it could not; a repeated report changes nothing.
2650
2663
  record_interjection_result(connection, room_id, execution_id, payload, now)
@@ -2663,6 +2676,17 @@ def execution_action(connection, room_id, execution_id, token, payload):
2663
2676
  (now + LEASE_SECONDS, activity, activity_at, execution_id))
2664
2677
  connection.executemany("INSERT OR IGNORE INTO execution_notes (execution_id,seq,room_id,text,created_at) VALUES (?,?,?,?,?)",
2665
2678
  [(execution_id, note["seq"], room_id, note["text"], now) for note in notes])
2679
+ elif action == "pause":
2680
+ # The owner's pause from the private chat: the runner stops the software's turn, the execution stays live and
2681
+ # resumes in the same session with the owner's next line. Ending the turn is the member's interrupt instead.
2682
+ capability = connection.execute("SELECT resume FROM execution_capabilities WHERE execution_id=?", (execution_id,)).fetchone()
2683
+ if not capability or not capability["resume"]:
2684
+ raise FileExistsError("这位成员的接入方式不能暂停后接着做;要结束这次执行请用停止")
2685
+ if not row["paused_at"]:
2686
+ connection.execute("UPDATE member_executions SET paused_at=? WHERE id=?", (now, execution_id))
2687
+ elif action == "resume":
2688
+ # The runner resumed the paused turn in the same session.
2689
+ connection.execute("UPDATE member_executions SET paused_at=0 WHERE id=?", (execution_id,))
2666
2690
  elif action == "interject":
2667
2691
  interjection = add_interjection(connection, room_id, row, payload, now)
2668
2692
  detail = execution_detail(connection, connection.execute("SELECT * FROM member_executions WHERE id=?", (execution_id,)).fetchone())
@@ -2,7 +2,8 @@
2
2
  Private chat: the owner talks to one room member directly, in the member's own session, outside the room's messages.
3
3
 
4
4
  Each row of `private_chats` is one thing the owner said and the member's reply. While the member works on a room
5
- execution that takes interjections, a new line goes in as an interjection of that execution; otherwise it waits until
5
+ execution that takes interjections (mid-turn, or by pausing and resuming its turn), a new line goes in as an
6
+ interjection of that execution; otherwise it waits until
6
7
  the member is idle and the runner runs it as a turn of its own, taking the reply from the software's output. Only the
7
8
  owner reads or writes chats; the runner, which holds the owner credential, reports their progress.
8
9
  """
@@ -67,9 +68,11 @@ def member_chat(connection, room_id, member_id, token, payload=None):
67
68
  raise FileExistsError("submission_id 已用于另一句私聊")
68
69
  return public_chat(connection, earlier)
69
70
  now = int(time.time())
70
- # A room execution that takes interjections hears it now; one that cannot waits, like a turn already running.
71
+ # A room execution that takes interjections (mid-turn, or by a pause and resume) hears it now; one that cannot
72
+ # waits, like a turn already running.
71
73
  execution = connection.execute("""SELECT e.* FROM member_executions e JOIN execution_capabilities c ON c.execution_id=e.id
72
- WHERE e.member_id=? AND e.status IN ('dispatched','started') AND c.steer=1 ORDER BY e.id DESC LIMIT 1""", (member["id"],)).fetchone()
74
+ WHERE e.member_id=? AND e.status IN ('dispatched','started') AND (c.steer=1 OR c.resume=1) ORDER BY e.id DESC LIMIT 1""",
75
+ (member["id"],)).fetchone()
73
76
  status, execution_id = "pending", None
74
77
  if execution:
75
78
  add_interjection(connection, room_id, execution, {"text": text, "submission_id": INTERJECTION_PREFIX + submission_id}, now)
@@ -299,6 +299,9 @@ CREATE TABLE IF NOT EXISTS member_executions (
299
299
  cwd TEXT NOT NULL DEFAULT '',
300
300
  activity TEXT NOT NULL DEFAULT '',
301
301
  activity_at INTEGER NOT NULL DEFAULT 0,
302
+ -- When the owner paused it from the private chat (0 = not paused): the runner stopped its software turn and resumes
303
+ -- the same execution in the same session with the owner's next line; the execution itself stays live.
304
+ paused_at INTEGER NOT NULL DEFAULT 0,
302
305
  UNIQUE(room_id,client_id)
303
306
  );
304
307
  -- What the owner wrote about each member (strengths, division of work), by name so it survives leaving and rejoining.
@@ -445,7 +448,9 @@ CREATE TABLE IF NOT EXISTS execution_capabilities (
445
448
  stoppable INTEGER CHECK(stoppable IN (0,1)),
446
449
  progress INTEGER CHECK(progress IN (0,1)),
447
450
  stop_result TEXT NOT NULL DEFAULT '' CHECK(stop_result IN ('','stopped','unstoppable','unconfirmed')),
448
- steer INTEGER CHECK(steer IN (0,1))
451
+ steer INTEGER CHECK(steer IN (0,1)),
452
+ -- The runner can stop the software's turn and continue the same session, so a pause keeps the execution.
453
+ resume INTEGER CHECK(resume IN (0,1))
449
454
  );
450
455
  -- What the owner said into one running execution without ending it (an interjection from the private-chat page), and
451
456
  -- whether the runner got it into the member's software: pending until the runner reports delivered or failed. A retry