ai_co_discuss 0.3.80 → 0.3.82
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/docs/runner.md +1 -1
- package/package.json +1 -1
- package/runner/adapters/claude.mjs +7 -5
- package/runner/adapters/index.mjs +3 -2
- package/runner/discussion_launch.mjs +5 -2
- package/runner/free_runner.mjs +36 -2
- package/web/modern.css +1 -1
- package/web/modern.js +11 -11
package/docs/runner.md
CHANGED
|
@@ -116,7 +116,7 @@ API 成员可以担任专家或主持人。房间指定了项目目录时,它
|
|
|
116
116
|
- **保存**:在“工作流”一行点“添加”,选“新建”,或导入别人分享的 zip 或文件夹(手机、平板上同样可用)。导入的内容先在编辑页预览,点保存才写入。选中一个工作流后,下方显示它的描述,可“查看说明”(做法说明与成员卡片)或“编辑”,删除在编辑页底部。编辑页可以“从当前房间成员导入”卡片,提示词保持原文。想让主持人起草,打开下面的“允许主持人自建工作流”。
|
|
117
117
|
- **让主持人自己建**:建房时(第 3 节“协作方式”)或在“成员与设置 → 协作设置”勾选“允许主持人自建工作流”后,直接对主持人说要什么分工(或“把这个房间的做法存成工作流”),它会自己保存、在本房间启用,并邀请工作流里的成员,接着就按流程派活,不需要你导入或添加成员。之后让它改流程,改动从成员下一次执行起生效。打开这个开关后,也可以在输入框输入 `/` 选一个已保存的工作流,输入框里会写好“启用工作流「名称」(ID),”,补上要做的事发给主持人,它就启用这个工作流并带来成员(没打开时该项灰显)。主持人带来的成员项目权限最高为可写;只有本地软件担任的主持人能这样做,API 主持人不行。
|
|
118
118
|
- **分享**:工作流存在本机服务目录的 `workflows` 文件夹,一个工作流一个文件夹;编辑页可复制路径,把整个文件夹(或打成 zip)发给对方即可分享。导入时会列出每张卡片的提示词和项目权限;要求写文件的卡片默认按只读导入,逐张勾选后才放开。
|
|
119
|
-
-
|
|
119
|
+
- **修改与结束**:修改工作流会作用于所有启用它的房间,成员下一次执行就按新内容;新增的卡片不会自动进入房间,主持人用到时会自己补上,也可以在右栏工作流一行点刷新图标立即补齐(删掉的卡片,其成员留作普通成员;与库一致时不显示该图标);工作流卡片的提示词只读,在“工作流”一行点“编辑”修改。工作流进行中时,主持人只在请求和它的用途相符(或你说按工作流做)时才用,其他请求照常处理。用完在“成员与设置 → 协作设置”的工作流一行点请离成员图标(或逐个让成员离开):工作流带来的成员离开(主持人保留),正在进行的工作随之停止,工作流结束,右栏这一块消失;成员会话保留,下次在输入框打 `/` 选它就把原成员请回来,接着原来的上下文。换工作流时直接选另一个,原来的成员留作普通成员。
|
|
120
120
|
|
|
121
121
|
## 朗读与语音输入(可选)
|
|
122
122
|
|
package/package.json
CHANGED
|
@@ -227,20 +227,22 @@ function result_text(content) {
|
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
229
|
* One stream-json event as execution-detail entries: a tool call and its result share the call's id. Claude Code
|
|
230
|
-
* streams thinking as signed blocks with empty text, so thinking shows only when it carries text.
|
|
230
|
+
* streams thinking as signed blocks with empty text, so thinking shows only when it carries text. Its assistant and
|
|
231
|
+
* user events carry an ISO `timestamp` (seen on 2.1.312), which becomes a step's `at` and its result's `ended_at`.
|
|
231
232
|
*/
|
|
232
233
|
function claude_detail(event) {
|
|
233
234
|
const blocks = Array.isArray(event.message?.content) ? event.message.content : [];
|
|
235
|
+
const at = event.timestamp || undefined;
|
|
234
236
|
if (event.type === 'assistant') return blocks.flatMap(block => {
|
|
235
|
-
if (block.type === 'text' && block.text?.trim()) return [{ kind: 'message', body: block.text }];
|
|
236
|
-
if (block.type === 'thinking' && block.thinking?.trim()) return [{ kind: 'thinking', body: block.thinking }];
|
|
237
|
+
if (block.type === 'text' && block.text?.trim()) return [{ kind: 'message', body: block.text, at }];
|
|
238
|
+
if (block.type === 'thinking' && block.thinking?.trim()) return [{ kind: 'thinking', body: block.thinking, at }];
|
|
237
239
|
if (block.type !== 'tool_use') return [];
|
|
238
240
|
const kind = /^(Bash|PowerShell)$/.test(block.name) ? 'command' : EDIT_TOOL_NAMES.test(block.name) ? 'edit'
|
|
239
241
|
: /^Web(Search|Fetch)$/.test(block.name) ? 'search' : 'tool';
|
|
240
|
-
return [{ id: `tool-${block.id}`, kind, title: kind === 'command' ? tool_subject(block) : `${block.name} ${tool_subject(block)}`, state: 'running' }];
|
|
242
|
+
return [{ id: `tool-${block.id}`, kind, title: kind === 'command' ? tool_subject(block) : `${block.name} ${tool_subject(block)}`, state: 'running', at }];
|
|
241
243
|
});
|
|
242
244
|
if (event.type === 'user') return blocks.filter(block => block.type === 'tool_result')
|
|
243
|
-
.map(block => ({ id: `tool-${block.tool_use_id}`, body: result_text(block.content), state: block.is_error ? 'failed' : 'done' }));
|
|
245
|
+
.map(block => ({ id: `tool-${block.tool_use_id}`, body: result_text(block.content), state: block.is_error ? 'failed' : 'done', ended_at: at }));
|
|
244
246
|
if (event.type === 'result' && event.is_error) return [{ kind: 'error', body: String(event.result || event.subtype || '本轮失败') }];
|
|
245
247
|
return [];
|
|
246
248
|
}
|
|
@@ -95,8 +95,9 @@ const DETAIL_BODY_LIMIT = 4000;
|
|
|
95
95
|
/**
|
|
96
96
|
* What an agent did in one execution, rebuilt from the raw event log its adapter's process wrote: messages, commands
|
|
97
97
|
* with their output, searches, edits and tool calls in order. Each adapter's `events.detail(event)` turns one event into
|
|
98
|
-
* entries `{id?, kind, title?, body?, append?, state?}`; an entry with a known id updates the earlier
|
|
99
|
-
* (a command's result fills in its start), `append` extends its body.
|
|
98
|
+
* entries `{id?, kind, title?, body?, append?, state?, at?, ended_at?}`; an entry with a known id updates the earlier
|
|
99
|
+
* one in place (a command's result fills in its start), `append` extends its body. `at` / `ended_at` are ISO times, given
|
|
100
|
+
* only by software whose log carries them; without them no step time shows. Returns null when the adapter keeps no such log.
|
|
100
101
|
*/
|
|
101
102
|
export function execution_details(adapter_id, text) {
|
|
102
103
|
const adapter = adapter_of(adapter_id);
|
|
@@ -357,12 +357,15 @@ export async function room_models(home, raw, { read_defaults = profile => cached
|
|
|
357
357
|
api_max_requests: config.api_max_requests ?? DEFAULT_API_MAX_REQUESTS };
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
/**
|
|
360
|
+
/**
|
|
361
|
+
* The room's workflow as the right column shows it; `available` is false when its folder is gone or broken, and
|
|
362
|
+
* `cards` lists the card IDs the library has now, so the page offers a refresh only when the room differs from it.
|
|
363
|
+
*/
|
|
361
364
|
function room_workflow_status(config, home) {
|
|
362
365
|
if (!config.workflow?.id) return null;
|
|
363
366
|
const loaded = load_room_workflow({ ...config, api_home: config.api_home || home, workflow: { ...config.workflow, enabled: true } });
|
|
364
367
|
return { id: config.workflow.id, label: loaded.label || config.workflow.id, enabled: Boolean(config.workflow.enabled),
|
|
365
|
-
available: !loaded.error, members: config.workflow.members || {} };
|
|
368
|
+
available: !loaded.error, members: config.workflow.members || {}, cards: Object.keys(loaded.cards || {}) };
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
/**
|
package/runner/free_runner.mjs
CHANGED
|
@@ -45,6 +45,11 @@ export function review_capability_version(profile, member, config) {
|
|
|
45
45
|
return createHash('sha256').update(JSON.stringify(facts)).digest('hex');
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Whether a process with this pid exists; signal 0 only checks, and EPERM means it exists under another user. */
|
|
49
|
+
function process_alive(pid) {
|
|
50
|
+
try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; }
|
|
51
|
+
}
|
|
52
|
+
|
|
48
53
|
/** Identify a working directory consistently, so executions in the same cwd can be compared. */
|
|
49
54
|
export function cwd_key(directory) {
|
|
50
55
|
if (!directory) return '';
|
|
@@ -251,6 +256,8 @@ export class FreeRoomRunner {
|
|
|
251
256
|
this.members = new Map();
|
|
252
257
|
// An empty set proves every tracked handle for this execution closed; a missing set proves nothing after restart.
|
|
253
258
|
this.execution_processes = new Map();
|
|
259
|
+
// Marks the handles this runner process tracks (`owner:ID` in metadata), so a later runner can tell inherited executions.
|
|
260
|
+
this.instance = randomUUID();
|
|
254
261
|
// Execution id -> why its session ended on its own, such as a command refused in unattended mode ('' when unknown).
|
|
255
262
|
this.exited = new Map();
|
|
256
263
|
// Executions whose software failed before any turn ran, as its adapter reported; safe to deliver again.
|
|
@@ -886,6 +893,7 @@ export class FreeRoomRunner {
|
|
|
886
893
|
}
|
|
887
894
|
const execution = active.get(row.execution_id);
|
|
888
895
|
if (!execution) {
|
|
896
|
+
this.forget_owner(row.execution_id);
|
|
889
897
|
this.activity.delete(row.execution_id);
|
|
890
898
|
await this.report_segments(row.execution_id, true);
|
|
891
899
|
this.stop_member(member.name);
|
|
@@ -902,6 +910,10 @@ export class FreeRoomRunner {
|
|
|
902
910
|
}
|
|
903
911
|
// A restart between persisting and invoking cannot prove non-delivery, so it never resends.
|
|
904
912
|
if (row.status === 'delivering') return this.fail(member.name, execution.id, '上次投递结果不明,未自动重发;请核对原会话后重新发送');
|
|
913
|
+
// Inherited from a runner that stopped or crashed: once its software processes are gone, no answer can come.
|
|
914
|
+
if (row.status === 'running' && this.orphan(execution.id)?.alive === false) {
|
|
915
|
+
return this.fail(member.name, execution.id, '本机接入程序重启或异常退出,这次执行的软件进程已结束,未提交回答;请重新发送');
|
|
916
|
+
}
|
|
905
917
|
if (row.error) return this.retry_or_fail(member.name, execution, row.error);
|
|
906
918
|
if (this.exited.has(execution.id)) {
|
|
907
919
|
const latest = await api(this.config, `/executions/${execution.id}`);
|
|
@@ -954,7 +966,7 @@ export class FreeRoomRunner {
|
|
|
954
966
|
const value = this.store.db.prepare('SELECT value FROM metadata WHERE key=?').get(key)?.value;
|
|
955
967
|
if (!value) return;
|
|
956
968
|
const delivery = stored_delivery(value), processes = this.execution_processes.get(execution_id);
|
|
957
|
-
const ended = processes ? processes.size === 0 : this.exited.has(execution_id);
|
|
969
|
+
const ended = processes ? processes.size === 0 : this.exited.has(execution_id) || this.orphan(execution_id)?.alive === false;
|
|
958
970
|
const result = delivery.pending_result && delivery.pending_result !== 'unconfirmed' ? delivery.pending_result
|
|
959
971
|
: !delivery.stoppable ? 'unstoppable' : ended ? 'stopped' : 'unconfirmed';
|
|
960
972
|
if (delivery.stop_result === result) return;
|
|
@@ -985,8 +997,27 @@ export class FreeRoomRunner {
|
|
|
985
997
|
// The site's bounded hold now controls the next delivery; retain handles to observe and retry termination.
|
|
986
998
|
for (const child of processes || []) this.members.get(name)?.delete(child);
|
|
987
999
|
}
|
|
988
|
-
else { this.store.db.prepare('DELETE FROM metadata WHERE key=?').run(key); this.execution_processes.delete(execution_id); }
|
|
1000
|
+
else { this.store.db.prepare('DELETE FROM metadata WHERE key=?').run(key); this.execution_processes.delete(execution_id); this.forget_owner(execution_id); }
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* An execution whose handles an earlier runner process tracked and this one does not: `{alive}` says whether any of its
|
|
1004
|
+
* recorded software processes still runs. A handle without a pid lived inside that runner, so it ended with it.
|
|
1005
|
+
*/
|
|
1006
|
+
orphan(execution_id) {
|
|
1007
|
+
if (this.execution_processes.has(execution_id)) return null;
|
|
1008
|
+
const saved = JSON.parse(this.store.db.prepare('SELECT value FROM metadata WHERE key=?').get(`owner:${execution_id}`)?.value || 'null');
|
|
1009
|
+
if (!saved || saved.instance === this.instance) return null;
|
|
1010
|
+
return { alive: saved.pids.some(process_alive) };
|
|
1011
|
+
}
|
|
1012
|
+
/** Record that this runner process tracks a handle of the execution, with its pid when it is a separate process. */
|
|
1013
|
+
record_owner(execution_id, pid) {
|
|
1014
|
+
const key = `owner:${execution_id}`;
|
|
1015
|
+
const saved = JSON.parse(this.store.db.prepare('SELECT value FROM metadata WHERE key=?').get(key)?.value || 'null');
|
|
1016
|
+
const pids = saved?.instance === this.instance ? saved.pids : [];
|
|
1017
|
+
if (Number.isInteger(pid) && !pids.includes(pid)) pids.push(pid);
|
|
1018
|
+
this.store.db.prepare('INSERT OR REPLACE INTO metadata VALUES (?,?)').run(key, JSON.stringify({ instance: this.instance, pids }));
|
|
989
1019
|
}
|
|
1020
|
+
forget_owner(execution_id) { this.store.db.prepare('DELETE FROM metadata WHERE key=?').run(`owner:${execution_id}`); }
|
|
990
1021
|
/** Once a new task received the stop reminders, retire acknowledged unconfirmed reports for its old turns. */
|
|
991
1022
|
clear_stopped(name) {
|
|
992
1023
|
for (const id of this.stopped_executions(name)) {
|
|
@@ -1065,6 +1096,7 @@ export class FreeRoomRunner {
|
|
|
1065
1096
|
catch (error) { if (!/HTTP 409/.test(error.message)) throw error; }
|
|
1066
1097
|
this.save(name, { status: 'idle', error: reason });
|
|
1067
1098
|
this.stop_member(name);
|
|
1099
|
+
this.forget_owner(execution_id);
|
|
1068
1100
|
}
|
|
1069
1101
|
/**
|
|
1070
1102
|
* Hand the owner's pending interjections to the software running each execution, in order, through the steer handle
|
|
@@ -1167,6 +1199,8 @@ export class FreeRoomRunner {
|
|
|
1167
1199
|
if (!this.execution_processes.has(execution_id)) this.execution_processes.set(execution_id, new Set());
|
|
1168
1200
|
const processes = this.execution_processes.get(execution_id);
|
|
1169
1201
|
processes.add(child);
|
|
1202
|
+
// A handle only the owner may stop keeps running in the user's software, so a later runner must not fail it.
|
|
1203
|
+
if (Number.isInteger(execution_id) && child.stop_on_shutdown !== false) this.record_owner(execution_id, child.pid);
|
|
1170
1204
|
child.once('close', () => { processes.delete(child); this.children.delete(child); this.members.get(name).delete(child); });
|
|
1171
1205
|
}
|
|
1172
1206
|
/** Request termination; only a later close event proves it happened. */
|