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.
- package/.agents/skills/discuss-participant/SKILL.md +1 -1
- package/backend/free_room.py +2410 -2274
- package/backend/schema.sql +27 -0
- package/backend/server.py +21 -11
- package/docs/architecture.md +2 -1
- package/docs/deployment.md +10 -0
- package/docs/en/deployment.md +10 -0
- package/docs/en/workflow.md +1 -1
- package/docs/runner.md +1 -1
- package/docs/workflow.md +2 -2
- package/package.json +3 -2
- package/runner/adapters/antigravity_app.mjs +91 -0
- package/runner/adapters/antigravity_cli.mjs +320 -0
- package/runner/adapters/antigravity_sidecar.mjs +50 -0
- package/runner/{api_participant.mjs → adapters/api.mjs} +19 -5
- package/runner/adapters/claude.mjs +313 -0
- package/runner/adapters/cli.mjs +126 -0
- package/runner/adapters/codex_app.mjs +70 -0
- package/runner/adapters/codex_cli.mjs +286 -0
- package/runner/adapters/common.mjs +196 -0
- package/runner/adapters/index.mjs +143 -0
- package/runner/adapters/profiles.mjs +116 -0
- package/runner/admin_account.mjs +1 -1
- package/runner/discussion_launch.mjs +69 -79
- package/runner/discussion_local.mjs +1 -1
- package/runner/discussion_runner.mjs +14 -13
- package/runner/discussion_service.mjs +3 -3
- package/runner/discussion_setup.mjs +1 -1
- package/runner/discussion_sidecar.mjs +5 -50
- package/runner/discussion_skills.mjs +1 -1
- package/runner/execution_progress.mjs +99 -0
- package/runner/expert_prompts.mjs +19 -29
- package/runner/files.mjs +10 -0
- package/runner/free_runner.mjs +136 -30
- package/runner/remote_launch.mjs +10 -5
- package/runner/scheduler.mjs +124 -32
- package/runner/session_driver.mjs +5 -3
- package/web/app.js +116 -23
- package/web/backup.js +6 -6
- package/web/index.html +2 -0
- package/web/modern.css +1 -1
- package/web/modern.js +1699 -1353
- package/runner/discussion_adapters.mjs +0 -959
- package/runner/discussion_models.mjs +0 -271
- package/runner/session_cut.mjs +0 -167
- /package/runner/{codex_app_client.mjs → adapters/codex_app_client.mjs} +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What each supported software is and can do, as plain data: the adapter layer's static facts. Imports nothing, so the
|
|
3
|
+
* runner, the launch settings and the web page (bundled by Vite) all read the same table; behaviour lives in each
|
|
4
|
+
* software's own module (see index.mjs). Product code reads these fields, never a software name (AGENTS.md red line 9).
|
|
5
|
+
*
|
|
6
|
+
* Fields of one profile:
|
|
7
|
+
* - `platform`: the AI platform it serves (`Codex`, `Claude`, `Gemini`); '' serves any platform (model API).
|
|
8
|
+
* - `label`: display name. `kind`: `cli` (a process the runner starts), `app` (a desktop App) or `api` (the runner's own call).
|
|
9
|
+
* - `location`: the config field locating the installed software (`executable`, `ipc_directory`), or '' when none.
|
|
10
|
+
* - `fields`: further config fields the user may set for it.
|
|
11
|
+
* - `efforts`: thinking levels it accepts, or null when chosen inside the App; `efforts_per_model`: the levels are read
|
|
12
|
+
* from the software for the chosen model. `models`: whether its CLI lists models.
|
|
13
|
+
* - `access_levels`: project access levels it can enforce per run, or null when the App or API decides; `access_hints`
|
|
14
|
+
* says what each level does in it (measured headless: Claude Code 2.1.312, Codex CLI 0.163, agy 1.4.10) and
|
|
15
|
+
* `unsupported_hints` why a level it lacks is disabled.
|
|
16
|
+
* - `per_tool_allow`: a refused operation can be allowed one rule at a time.
|
|
17
|
+
* - `client`: the software a manual member says it runs in when it registers (`codex`, `claude`, `antigravity`).
|
|
18
|
+
* - `adopt`: an existing conversation can be taken over: `session`, or `own_cwd` when it must resume in the directory it
|
|
19
|
+
* was made in; '' when it cannot. `host_existing`: it may also host from, or be bound to, an existing conversation.
|
|
20
|
+
* `own_project_when_bound`: bound to an existing conversation, it keeps that conversation's project and permissions.
|
|
21
|
+
* `restore_manual`: a restored room keeps it manual, never taken over (a background resume would fork the session
|
|
22
|
+
* away from its original window).
|
|
23
|
+
* `resume_command(session_id)`: a command the owner can copy to reopen a restored manual conversation.
|
|
24
|
+
* - `defaults_by_query`: its default model is read by running it (seconds), so a quick read leaves it for later.
|
|
25
|
+
* - `capabilities`: `delivery` `{stoppable, progress}` of a usual delivery (each delivery reports its own); `rewind`: `cut`
|
|
26
|
+
* (the runner cuts the session), `fresh` (context rebuilt each time) or `kept` (cannot be cut here); `project` (works in
|
|
27
|
+
* a project directory); `access_env` (its process takes the credential file by environment); `enforces_read_only`
|
|
28
|
+
* (its sandbox enforces a read-only project); `ripgrep` (bundles rg for reading on Windows).
|
|
29
|
+
* - `session_family`: profiles sharing one session store; switching among them keeps the conversation.
|
|
30
|
+
* - `windows_command`: how its shell names the codiscuss CLI on Windows.
|
|
31
|
+
* - `task_rules(cli, {runs_commands})`: fixed task text this software needs, added by the runner so no member prompt drops it.
|
|
32
|
+
*/
|
|
33
|
+
const ALL_ACCESS = ['read', 'write', 'auto', 'full'];
|
|
34
|
+
const PROCESS = { stoppable: true, progress: true };
|
|
35
|
+
const IN_APP = { stoppable: false, progress: false };
|
|
36
|
+
|
|
37
|
+
export const ADAPTER_PROFILES = {
|
|
38
|
+
codex_cli: {
|
|
39
|
+
platform: 'Codex', label: 'Codex CLI', kind: 'cli', location: 'executable', fields: ['project_id', 'project_name'],
|
|
40
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], models: true, access_levels: ALL_ACCESS, per_tool_allow: false,
|
|
41
|
+
access_hints: { read: '只读沙箱', write: '项目可写的沙箱,命令在沙箱内运行、可联网',
|
|
42
|
+
auto: '沙箱外的操作由模型申请提权,Codex 自动审核代替人工批准', full: '无沙箱、无审批(--dangerously-bypass-approvals-and-sandbox)' },
|
|
43
|
+
client: 'codex', adopt: 'session', host_existing: true, session_family: 'codex',
|
|
44
|
+
capabilities: { delivery: PROCESS, rewind: 'cut', project: true, access_env: true, enforces_read_only: true, ripgrep: true },
|
|
45
|
+
},
|
|
46
|
+
codex: {
|
|
47
|
+
platform: 'Codex', label: 'Codex App', kind: 'app', location: 'executable', fields: ['source_thread_id', 'project_id', 'project_name'],
|
|
48
|
+
efforts: null, models: false, access_levels: null, per_tool_allow: false,
|
|
49
|
+
client: 'codex', adopt: 'session', host_existing: true, session_family: 'codex',
|
|
50
|
+
capabilities: { delivery: IN_APP, rewind: 'cut', project: true },
|
|
51
|
+
},
|
|
52
|
+
claude: {
|
|
53
|
+
platform: 'Claude', label: 'Claude Code', kind: 'cli', location: 'executable', fields: ['allowed_tools'],
|
|
54
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max'], models: true, access_levels: ALL_ACCESS, per_tool_allow: true,
|
|
55
|
+
access_hints: { read: '只放行网站命令与草稿,不改项目文件、不跑其他命令', write: '可改项目文件,Bash / PowerShell 命令全部放行(无沙箱)',
|
|
56
|
+
auto: 'Claude auto 模式:由分类器判断,实测项目外写入和联网都会放行', full: 'bypassPermissions:跳过全部权限检查' },
|
|
57
|
+
client: 'claude', adopt: 'own_cwd', host_existing: true, restore_manual: true,
|
|
58
|
+
resume_command: session_id => `claude --resume ${session_id}`,
|
|
59
|
+
// Claude Code runs commands through bash on Windows too, where the npm shim works without .cmd.
|
|
60
|
+
windows_command: 'codiscuss',
|
|
61
|
+
capabilities: { delivery: PROCESS, rewind: 'cut', project: true, access_env: true },
|
|
62
|
+
},
|
|
63
|
+
antigravity_cli: {
|
|
64
|
+
platform: 'Gemini', label: 'Antigravity CLI', kind: 'cli', location: 'executable', fields: ['project_id', 'project_name'],
|
|
65
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max'], efforts_per_model: true, models: true, defaults_by_query: true,
|
|
66
|
+
// Headless agy refuses `--mode auto` ("not available for this account", 1.4.10–1.4.11), so 自动 is not offered.
|
|
67
|
+
access_levels: ['read', 'write', 'full'], per_tool_allow: true,
|
|
68
|
+
access_hints: { read: '只放行网站命令;碰到未放行的命令会直接结束本轮', write: '可写项目,命令全部放行(在 agy 沙箱内)',
|
|
69
|
+
full: '跳过全部权限确认(--dangerously-skip-permissions),并放开沙箱外命令与 MCP 工具' },
|
|
70
|
+
unsupported_hints: { auto: 'agy 只在交互窗口提供自动模式,后台运行(-p)不支持' },
|
|
71
|
+
// Only a conversation this tool created gets its discussion project; an existing one keeps its own project and permissions.
|
|
72
|
+
client: 'antigravity', adopt: 'session', host_existing: false, own_project_when_bound: true,
|
|
73
|
+
capabilities: { delivery: PROCESS, rewind: 'cut', project: true, access_env: true },
|
|
74
|
+
// A read-only agy project allows only codiscuss commands and headless agy ends the turn on a denied one; agy streams no
|
|
75
|
+
// thinking text, so without a word at each turning point the page cannot show how far it got.
|
|
76
|
+
task_rules: (cli, { runs_commands }) => (runs_commands ? ''
|
|
77
|
+
: `无人值守下 run_command 只能运行 ${cli} 的 subcommands 子命令,其他命令会被自动拒绝并使会话直接结束;列目录、搜索或读取文件用 list_dir、find_by_name、grep_search、view_file。`)
|
|
78
|
+
+ '多步任务中,在关键转折处用一句话写出进展再继续:agy 不输出思考过程,网页只能靠这些话显示你做到哪一步。',
|
|
79
|
+
},
|
|
80
|
+
antigravity: {
|
|
81
|
+
platform: 'Gemini', label: 'Antigravity App', kind: 'app', location: 'ipc_directory', fields: ['project_id', 'project_name'],
|
|
82
|
+
efforts: null, models: false, access_levels: null, per_tool_allow: false,
|
|
83
|
+
client: 'antigravity', adopt: 'session', host_existing: true,
|
|
84
|
+
capabilities: { delivery: IN_APP, rewind: 'kept', project: true },
|
|
85
|
+
},
|
|
86
|
+
api: {
|
|
87
|
+
platform: '', label: '模型 API', kind: 'api', location: '', fields: ['base_url', 'api_protocol', 'api_key_ref'],
|
|
88
|
+
efforts: null, models: false, access_levels: null, per_tool_allow: false, adopt: '', host_existing: false,
|
|
89
|
+
capabilities: { delivery: PROCESS, rewind: 'fresh', project: false },
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** Every configured `adapter` value this version supports. */
|
|
94
|
+
export const ADAPTER_IDS = Object.keys(ADAPTER_PROFILES);
|
|
95
|
+
|
|
96
|
+
/** The profile of one adapter with its defaults filled in, or null for a value this version does not support. */
|
|
97
|
+
export function adapter_profile(id) {
|
|
98
|
+
const profile = ADAPTER_PROFILES[id];
|
|
99
|
+
return profile ? { id, session_family: id, windows_command: 'codiscuss.cmd', ...profile } : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Whether two different adapters share one session store, so switching between them keeps the conversation. */
|
|
103
|
+
export function same_session_family(left, right) {
|
|
104
|
+
const [a, b] = [adapter_profile(left), adapter_profile(right)];
|
|
105
|
+
return Boolean(a && b && left !== right && a.session_family === b.session_family);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The adapters serving one platform, in table order; a model API serves every platform. */
|
|
109
|
+
export function platform_adapters(platform) {
|
|
110
|
+
return ADAPTER_IDS.filter(id => ADAPTER_PROFILES[id].platform === platform || ADAPTER_PROFILES[id].platform === '');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** What an adapter can do; an unknown adapter can do nothing. */
|
|
114
|
+
export function adapter_capabilities(id) {
|
|
115
|
+
return ADAPTER_PROFILES[id]?.capabilities || { delivery: IN_APP, rewind: 'kept', project: false };
|
|
116
|
+
}
|
package/runner/admin_account.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
7
|
|
|
8
|
-
import { atomic_json } from './
|
|
8
|
+
import { atomic_json } from './files.mjs';
|
|
9
9
|
import { service_home } from './discussion_service.mjs';
|
|
10
10
|
|
|
11
11
|
const DEFAULT_CONFIG = join('.wrangler', 'deploy', 'wrangler.jsonc');
|
|
@@ -9,17 +9,17 @@ import { setTimeout as sleep } from 'node:timers/promises';
|
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
10
|
import { parseArgs } from 'node:util';
|
|
11
11
|
|
|
12
|
-
import { atomic_json
|
|
13
|
-
import {
|
|
12
|
+
import { atomic_json } from './files.mjs';
|
|
13
|
+
import { setup_sidecar } from './adapters/antigravity_app.mjs';
|
|
14
|
+
import { adapter_of, adoption_adapter, BRANCHABLE_ADAPTERS, branch_session, session_settings, software_defaults, software_effort, software_models } from './adapters/index.mjs';
|
|
15
|
+
import { ADAPTER_IDS, ADAPTER_PROFILES, adapter_profile, platform_adapters, same_session_family } from './adapters/profiles.mjs';
|
|
14
16
|
import { API_PROVIDERS, api_check, api_models, save_api_key, saved_api_keys, validate_api_profile } from './discussion_api.mjs';
|
|
15
|
-
import { SCHEDULER_PROVIDERS, scheduler_check } from './scheduler.mjs';
|
|
17
|
+
import { DEFAULT_SCHEDULER_MODE, SCHEDULER_MODES, SCHEDULER_PROVIDERS, scheduler_check } from './scheduler.mjs';
|
|
16
18
|
import { access_levels, BUILTIN_EXPERT_PRESETS, MAX_EXPERT_PROMPT, PROJECT_ACCESS } from './expert_prompts.mjs';
|
|
17
|
-
import {
|
|
18
|
-
import { acquire_lock, api, CODEX_ADAPTERS, DiscussionStore, service_drafts, validate_config } from './discussion_runner.mjs';
|
|
19
|
+
import { acquire_lock, api, DiscussionStore, service_drafts, validate_config } from './discussion_runner.mjs';
|
|
19
20
|
import { FreeRoomRunner, free_task_files } from './free_runner.mjs';
|
|
20
21
|
import { assert_shared_home, CONNECTION_FIELDS, configuration_snapshot, read_registry, registered_identity, register_room, service_home, service_pid, start_service } from './discussion_service.mjs';
|
|
21
22
|
|
|
22
|
-
const CLIENTS = { Codex: ['codex_cli', 'codex', 'api'], Claude: ['claude', 'api'], Gemini: ['antigravity_cli', 'antigravity', 'api'], DeepSeek: ['api'], Qwen: ['api'], Grok: ['api'] };
|
|
23
23
|
// A platform is a type; `Claude-评审` is another Claude instance with its own session, model and effort.
|
|
24
24
|
const INSTANCE_PATTERN = /^(Codex|Claude|Gemini|DeepSeek|Qwen|Grok)(?:-[0-9A-Za-z_一-鿿]{1,16})?$/;
|
|
25
25
|
|
|
@@ -33,10 +33,7 @@ export function next_instance(platform, taken) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/** Supported local clients of an instance's platform. */
|
|
36
|
-
function clients_of(name) { return
|
|
37
|
-
// The UI discovers each model's supported subset; omitted values keep the CLI's own default.
|
|
38
|
-
const EFFORT_LEVELS = { codex_cli: ['low', 'medium', 'high', 'xhigh', 'max', 'ultra'], claude: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
39
|
-
antigravity_cli: ['low', 'medium', 'high', 'xhigh', 'max'] };
|
|
36
|
+
function clients_of(name) { return platform_adapters(platform_of(name)); }
|
|
40
37
|
const PROFILE_FIELDS = ['adapter', 'executable', 'model', 'effort', 'source_thread_id', 'ipc_directory', 'allowed_tools', 'cwd', 'project_name', 'project_id', 'base_url', 'api_protocol', 'api_key_ref', 'system_prompt'];
|
|
41
38
|
const MAX_MINUTES = 1440;
|
|
42
39
|
const SESSION_ID_PATTERN = /^[0-9A-Za-z_-]{8,128}$/;
|
|
@@ -232,7 +229,7 @@ export async function cached_software_models(home, profile, { refresh = false, q
|
|
|
232
229
|
const cached = entry => !refresh && entry && Date.now() - entry.at < MODEL_CACHE_MS ? entry.result : null;
|
|
233
230
|
// Defaults are read on every request, so a changed software setting shows without waiting out the cache.
|
|
234
231
|
const [result, effort, defaults] = await Promise.all([cached(cache[key]) || query(profile),
|
|
235
|
-
profile.adapter
|
|
232
|
+
adapter_profile(profile.adapter)?.efforts_per_model ? cached(cache[effort_key]) || query_effort(profile) : null, query_defaults(profile)]);
|
|
236
233
|
const updates = {};
|
|
237
234
|
if (!cached(cache[key]) && !result.error && result.models.length) {
|
|
238
235
|
updates[key] = { at: Date.now(), result };
|
|
@@ -273,20 +270,22 @@ export async function room_models(home, raw, { read_defaults = software_defaults
|
|
|
273
270
|
let sessions = {};
|
|
274
271
|
try { if (paths.length) sessions = bound_sessions(config, paths[0]); } catch { /* No readable runtime yet: fall back to borrowed sessions. */ }
|
|
275
272
|
const participants = Object.fromEntries(await Promise.all(Object.entries(profiles).map(async ([name, profile]) => {
|
|
276
|
-
const
|
|
277
|
-
//
|
|
278
|
-
const
|
|
273
|
+
const facts = adapter_profile(profile.adapter);
|
|
274
|
+
// An App chooses its model itself; a CLI or API member's card sets it.
|
|
275
|
+
const card_model = facts?.kind !== 'app';
|
|
276
|
+
// A software read by running it to name its default model takes seconds; a quick read leaves that to a later full read.
|
|
277
|
+
const pending = input.quick === true && Boolean(facts?.defaults_by_query);
|
|
279
278
|
const defaults = pending ? { model: '', effort: '' } : await read_defaults(profile);
|
|
280
|
-
const actual =
|
|
281
|
-
return [name, { ...(pending ? { defaults_pending: true } : {}), adapter: profile.adapter, model:
|
|
282
|
-
effort:
|
|
279
|
+
const actual = read_session(profile.adapter, sessions[name] || profile.session_id);
|
|
280
|
+
return [name, { ...(pending ? { defaults_pending: true } : {}), adapter: profile.adapter, model: card_model ? profile.model || '' : '',
|
|
281
|
+
effort: facts?.efforts ? profile.effort || '' : '', default_model: defaults.model, default_effort: defaults.effort,
|
|
283
282
|
...(actual.model ? { actual_model: actual.model } : {}), ...(actual.effort ? { actual_effort: actual.effort } : {}),
|
|
284
|
-
project_access:
|
|
285
|
-
//
|
|
286
|
-
own_project:
|
|
283
|
+
project_access: !facts?.capabilities.project ? 'read' : profile.project_access || config.project_access || 'read',
|
|
284
|
+
// A member bound to its own conversation may keep that conversation's project, so this tool cannot set its access.
|
|
285
|
+
own_project: Boolean(facts?.own_project_when_bound && profile.session_id) }];
|
|
287
286
|
})));
|
|
288
287
|
// Smart scheduling is a local choice; only which model decides is shown, never its key.
|
|
289
|
-
const scheduler = config.scheduler ? { provider: config.scheduler.provider, model: config.scheduler.model } : null;
|
|
288
|
+
const scheduler = config.scheduler ? { provider: config.scheduler.provider, model: config.scheduler.model, mode: config.scheduler.mode || DEFAULT_SCHEDULER_MODE } : null;
|
|
290
289
|
return { room_id: input.room_id, registered: paths.length > 0, participants, scheduler };
|
|
291
290
|
}
|
|
292
291
|
|
|
@@ -390,10 +389,11 @@ function validate_profile(name, profile, binding = false) {
|
|
|
390
389
|
if (key === 'model' && value && !/^[A-Za-z0-9][A-Za-z0-9._:/@[\]-]*$/.test(value)) throw new Error(`${name}.model 只能是模型名称,例如 gpt-6-astra、opus 或 gemini-4-argon-high`);
|
|
391
390
|
if (key === 'system_prompt' && value.length > MAX_EXPERT_PROMPT) throw new Error(`${name} 的专属提示词最多 ${MAX_EXPERT_PROMPT} 字`);
|
|
392
391
|
}
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
392
|
+
const facts = adapter_profile(profile.adapter);
|
|
393
|
+
if (facts?.kind === 'api') validate_api_profile(profile);
|
|
394
|
+
// The UI discovers each model's supported subset; omitted values keep the CLI's own default.
|
|
395
|
+
if (profile.effort && facts?.kind !== 'api' && !facts?.efforts?.includes(profile.effort)) {
|
|
396
|
+
throw new Error(facts?.efforts ? `${name}.effort 只能是 ${facts.efforts.join(' / ')}` : `${name} 的思考档位在原软件中选择`);
|
|
397
397
|
}
|
|
398
398
|
}
|
|
399
399
|
|
|
@@ -494,17 +494,18 @@ function participant_profile(name, defaults, input, current_host) {
|
|
|
494
494
|
for (const field of ['cwd', 'model', 'effort', 'project_name', 'project_id', 'project_access']) if (profile[field] === '') delete profile[field];
|
|
495
495
|
validate_profile(name, profile, true);
|
|
496
496
|
if (!clients_of(name)?.includes(profile.adapter)) throw new Error(`请配置 ${name} 的 CLI / App 客户端`);
|
|
497
|
-
|
|
497
|
+
const facts = adapter_profile(profile.adapter);
|
|
498
|
+
if (facts?.kind === 'api') {
|
|
498
499
|
if (input.entry === 'conversation' && current_host) throw new Error('API 主持人请从网页启动;当前软件会话主持须选择对应 CLI / App');
|
|
499
500
|
return profile;
|
|
500
501
|
}
|
|
501
|
-
if (
|
|
502
|
+
if (facts?.location === 'executable' && (!existsSync(profile.executable || '') || !statSync(profile.executable).isFile()
|
|
502
503
|
|| (process.platform === 'win32' && !/\.exe$/i.test(profile.executable)))) throw new Error(`${name}:请指定实际原生可执行文件,Windows 使用 .exe`);
|
|
503
504
|
const cwd = profile.cwd || input.cwd;
|
|
504
505
|
if (cwd && (!existsSync(cwd) || !statSync(cwd).isDirectory())) throw new Error(`${name}:工作目录不存在或不是文件夹,请核对本次目录配置`);
|
|
505
506
|
if (input.entry === 'conversation' && current_host) profile.session_id = input.current_session_id;
|
|
506
507
|
// A new agy conversation always gets this tool's discussion project; only a manual agy expert the owner chose to host (input.adopt) keeps its own.
|
|
507
|
-
if (
|
|
508
|
+
if (!facts?.host_existing && profile.session_id && !input.adopt) throw new Error(`${name}:${facts?.label || profile.adapter} 不能绑定已有会话或从当前对话主持全自动讨论;请从网页或其他软件发起,它会由讨论服务新建会话`);
|
|
508
509
|
return profile;
|
|
509
510
|
}
|
|
510
511
|
|
|
@@ -528,21 +529,15 @@ function same_directory(left, right) {
|
|
|
528
529
|
export function borrowed_session(name, profile, room_cwd, env = process.env) {
|
|
529
530
|
const id = profile.session_id;
|
|
530
531
|
if (!SESSION_ID_PATTERN.test(id)) throw new Error(`${name}:会话 ID 格式不正确`);
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
if (['antigravity', 'antigravity_cli'].includes(profile.adapter)) {
|
|
537
|
-
if (!existsSync(agy_conversation(profile.adapter, id))) throw new Error(`${name}:本机找不到 agy 会话 ${id},请核对会话 ID`);
|
|
538
|
-
return profile;
|
|
532
|
+
const adapter = adapter_of(profile.adapter);
|
|
533
|
+
if (!adapter?.adopt || !adapter.session) {
|
|
534
|
+
const adoptable = ADAPTER_IDS.filter(item => ADAPTER_PROFILES[item].adopt).map(item => ADAPTER_PROFILES[item].label);
|
|
535
|
+
throw new Error(`${name}:只有 ${adoptable.join('、')} 可以接入已有会话`);
|
|
539
536
|
}
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
if (!
|
|
544
|
-
const cwd = readFileSync(file, 'utf8').split('\n').map(line => { try { return JSON.parse(line).cwd; } catch { return undefined; } }).find(Boolean);
|
|
545
|
-
if (!cwd) throw new Error(`${name}:Claude 会话 ${id} 没有记录原工作目录,无法续接`);
|
|
537
|
+
if (!adapter.session.exists(id, { env })) throw new Error(`${name}:本机找不到 ${adapter.label} 会话 ${id},请核对会话 ID`);
|
|
538
|
+
if (adapter.adopt !== 'own_cwd') return profile;
|
|
539
|
+
const cwd = adapter.session.cwd(id, { env });
|
|
540
|
+
if (!cwd) throw new Error(`${name}:${adapter.label} 会话 ${id} 没有记录原工作目录,无法续接`);
|
|
546
541
|
const wanted = profile.cwd || room_cwd;
|
|
547
542
|
if (wanted && !same_directory(wanted, cwd)) throw new Error(`${name}:这个 Claude 会话属于 ${cwd},只能在原目录续接;请让项目目录与它一致,或留空会话 ID 新建会话`);
|
|
548
543
|
return { ...profile, cwd: wanted || cwd };
|
|
@@ -628,8 +623,6 @@ export async function room_member(home, raw, locked = false) {
|
|
|
628
623
|
} finally { release(); }
|
|
629
624
|
}
|
|
630
625
|
|
|
631
|
-
// CLIs whose project file access this tool sets per run; App members follow the App's own settings, API members only read.
|
|
632
|
-
const ACCESS_ADAPTERS = ['codex_cli', 'claude', 'antigravity_cli'];
|
|
633
626
|
|
|
634
627
|
/**
|
|
635
628
|
* Switch one woken member's project files between read-only and writable after the room is created. Every run builds its
|
|
@@ -645,9 +638,11 @@ export async function room_member_access(home, raw) {
|
|
|
645
638
|
const profile = config.participants?.[name];
|
|
646
639
|
if (!profile) throw new Error(`${name} 不由本机唤醒,项目权限由它自己的会话决定`);
|
|
647
640
|
if (profile.project_access === 'own') throw new Error(`${name} 是手动接入的成员,权限沿用它自己会话的软件设置`);
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
if (
|
|
641
|
+
const facts = adapter_profile(profile.adapter);
|
|
642
|
+
// A CLI's project access is set per run; an App follows its own settings and an API member only reads.
|
|
643
|
+
if (!facts?.access_levels) throw new Error(!facts?.capabilities.project ? `${name} 没有本机文件工具,项目固定只读` : `${name} 的项目权限在 App 内设置`);
|
|
644
|
+
if (!facts.access_levels.includes(project_access)) throw new Error(facts.unsupported_hints?.[project_access] || `${facts.label} 不支持此档位`);
|
|
645
|
+
if (facts.own_project_when_bound && profile.session_id) throw new Error(`${name} 续接的是它自己的 ${facts.label} 对话,项目权限沿用该对话所在项目,请在原软件中设置`);
|
|
651
646
|
if (project_access === 'write' && !(profile.cwd || config.cwd)) throw new Error('本房间没有指定项目目录,可写没有作用;请在新建房间时选择项目目录');
|
|
652
647
|
const next = { ...config, participants: { ...config.participants, [name]: { ...profile, project_access } } };
|
|
653
648
|
validate_config(next, dirname(path));
|
|
@@ -674,8 +669,9 @@ export async function room_member_allow(home, raw) {
|
|
|
674
669
|
const config = configuration_snapshot(path).config;
|
|
675
670
|
const profile = config.participants?.[name];
|
|
676
671
|
if (!profile) throw new Error(`${name} 不由本机唤醒,权限由它自己的会话决定`);
|
|
677
|
-
|
|
678
|
-
if (
|
|
672
|
+
const facts = adapter_profile(profile.adapter);
|
|
673
|
+
if (!facts?.per_tool_allow) throw new Error(`${name} 的软件不支持逐条放行,请改用更高的权限档位`);
|
|
674
|
+
if (facts.own_project_when_bound && profile.session_id) throw new Error(`${name} 续接的是它自己的 ${facts.label} 对话,权限沿用该对话所在项目,请在原软件中设置`);
|
|
679
675
|
const allowed_tools = [...new Set([...(profile.allowed_tools || []), ...rules.map(rule => rule.trim())])];
|
|
680
676
|
if (allowed_tools.length > MAX_ALLOWED_TOOLS) throw new Error(`每位成员最多放行 ${MAX_ALLOWED_TOOLS} 条规则`);
|
|
681
677
|
const next = { ...config, participants: { ...config.participants, [name]: { ...profile, allowed_tools } } };
|
|
@@ -694,7 +690,7 @@ export async function room_member_adapter(home, raw) {
|
|
|
694
690
|
const { input, path, config: registered } = await member_room_context(home, raw);
|
|
695
691
|
const { name, adapter } = input;
|
|
696
692
|
if (registered.mode !== 'free') throw new Error('讨论进行中不能切换;结束讨论后再切换,或暂停后在“接入”页修正');
|
|
697
|
-
if (!
|
|
693
|
+
if (!ADAPTER_IDS.some(other => same_session_family(other, adapter))) throw new Error('只能在共用会话的 App 与 CLI 接入之间切换');
|
|
698
694
|
const room = await site_request(input.site, `/api/rooms/${input.room_id}`, input.token);
|
|
699
695
|
const member = room.free.members.find(item => item.name === name);
|
|
700
696
|
if (member?.pending > 0 || ['dispatched', 'started'].includes(member?.execution?.status)) throw new Error(`${name} 正在处理请求,请等它完成或先停止`);
|
|
@@ -702,12 +698,14 @@ export async function room_member_adapter(home, raw) {
|
|
|
702
698
|
try {
|
|
703
699
|
const config = configuration_snapshot(path).config;
|
|
704
700
|
const profile = config.participants?.[name];
|
|
705
|
-
if (!
|
|
701
|
+
if (profile?.adapter !== adapter && !same_session_family(profile?.adapter, adapter)) throw new Error(`${name} 不是本机唤醒、能切换到该接入方式的成员`);
|
|
706
702
|
if (profile.adapter === adapter) return { room_id: input.room_id, member: name, adapter };
|
|
707
703
|
const context = { config, path };
|
|
708
704
|
const next = { ...profile };
|
|
709
705
|
switch_codex_adapter(context, name, next, adapter);
|
|
710
|
-
|
|
706
|
+
// An App chooses its own model and effort.
|
|
707
|
+
if (!adapter_profile(adapter).models) delete next.model;
|
|
708
|
+
if (!adapter_profile(adapter).efforts) delete next.effort;
|
|
711
709
|
const updated = { ...config, participants: { ...config.participants, [name]: next } };
|
|
712
710
|
validate_config(updated, dirname(path));
|
|
713
711
|
atomic_json(path, updated);
|
|
@@ -715,19 +713,6 @@ export async function room_member_adapter(home, raw) {
|
|
|
715
713
|
} finally { release(); }
|
|
716
714
|
}
|
|
717
715
|
|
|
718
|
-
/** A Codex desktop chat is resumed through the app, any other Codex session through the CLI. */
|
|
719
|
-
function codex_session_adapter(session_id, env = process.env) {
|
|
720
|
-
const sessions = join(env.CODEX_HOME || join(homedir(), '.codex'), 'sessions');
|
|
721
|
-
const file = session_files(sessions).find(path => path.endsWith(`-${session_id}.jsonl`));
|
|
722
|
-
const meta = file ? readFileSync(file, 'utf8').split('\n', 1)[0] : '';
|
|
723
|
-
return /"originator":"Codex Desktop"/.test(meta) ? 'codex' : 'codex_cli';
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/** Where the Antigravity App or agy CLI keeps one conversation; the two keep separate stores. */
|
|
727
|
-
function agy_conversation(adapter, session_id) {
|
|
728
|
-
return join(homedir(), '.gemini', adapter === 'antigravity' ? 'antigravity' : 'antigravity-cli', 'conversations', `${session_id}.db`);
|
|
729
|
-
}
|
|
730
|
-
|
|
731
716
|
/**
|
|
732
717
|
* Register a manual Claude, Codex or agy expert's own session as a hosted participant, so the service resumes it for later turns.
|
|
733
718
|
* Returns the adapter used. Throws when the local software is not configured or the session is not found on this machine; the member then stays manual.
|
|
@@ -736,10 +721,11 @@ export function adopt_member_profile(home, path, name, session_id, client = 'cla
|
|
|
736
721
|
const release = acquire_lock(join(dirname(path), 'configuration-edit'));
|
|
737
722
|
try {
|
|
738
723
|
const config = configuration_snapshot(path).config;
|
|
739
|
-
|
|
740
|
-
|
|
724
|
+
// The adapter whose store holds the session: a desktop chat is resumed through its App, others through the CLI.
|
|
725
|
+
const adapter = adoption_adapter(client, session_id);
|
|
726
|
+
if (!adapter) throw new Error(`${name}:本机不能接管 ${client} 的会话`);
|
|
741
727
|
// An App conversation is reached through this room's own Sidecar, which the App starts as soon as it is configured.
|
|
742
|
-
const sidecar = adapter === '
|
|
728
|
+
const sidecar = adapter_profile(adapter).location === 'ipc_directory' ? { ipc_directory: join(dirname(path), 'agy-app-sidecar') } : {};
|
|
743
729
|
// A manually started member keeps its own session's permissions; the room's 只读 / 可写 does not apply to it.
|
|
744
730
|
const input = { cwd: config.cwd, adopt: true, participants: { [name]: { adapter, session_id, project_access: 'own', ...sidecar } } };
|
|
745
731
|
// Its model, effort, prompt and tools stay as the user set them in that session; machine defaults never override them.
|
|
@@ -812,17 +798,21 @@ export async function restore_room(home, raw) {
|
|
|
812
798
|
const room = await site_request(input.site, `/api/rooms/${input.room_id}`, input.token);
|
|
813
799
|
if (registered_room_path(home, input.site, input.room_id)) throw new Error('本房间已在本机登记,不重复恢复');
|
|
814
800
|
if (local.cwd && (!isAbsolute(local.cwd) || !existsSync(local.cwd) || !statSync(local.cwd).isDirectory())) throw new Error(`房间项目目录 ${local.cwd} 在本机不存在`);
|
|
815
|
-
const keep_manual = room.free.members.filter(member => member.access === 'manual' && member.status !== 'removed'
|
|
801
|
+
const keep_manual = room.free.members.filter(member => member.access === 'manual' && member.status !== 'removed'
|
|
802
|
+
&& platform_adapters(platform_of(member.name)).some(id => ADAPTER_PROFILES[id].restore_manual)).map(member => member.name);
|
|
816
803
|
const participants = {}, members = [];
|
|
817
804
|
for (const [name, exported] of Object.entries(local.participants)) {
|
|
818
805
|
try {
|
|
819
806
|
const profile = participant_profile(name, {}, { cwd: local.cwd, adopt: true, participants: { [name]: exported } }, false);
|
|
820
|
-
//
|
|
821
|
-
if (profile.adapter
|
|
807
|
+
// A member without a software session store (an API member) kept its binding in the old runtime directory, so it starts fresh.
|
|
808
|
+
if (!adapter_of(profile.adapter)?.session) delete profile.session_id;
|
|
822
809
|
const resumed = profile.session_id ? borrowed_session(name, profile, local.cwd) : profile;
|
|
823
|
-
if (profile.adapter
|
|
810
|
+
if (adapter_profile(profile.adapter)?.restore_manual) {
|
|
824
811
|
keep_manual.push(name);
|
|
825
|
-
|
|
812
|
+
const cwd = resumed.cwd || local.cwd || '';
|
|
813
|
+
const command = resumed.session_id && adapter_profile(profile.adapter).resume_command?.(resumed.session_id);
|
|
814
|
+
members.push({ name, status: 'manual', session_id: resumed.session_id || '', cwd,
|
|
815
|
+
resume: command ? { command, cwd } : null });
|
|
826
816
|
} else {
|
|
827
817
|
participants[name] = resumed;
|
|
828
818
|
members.push({ name, status: 'resumed', session_id: resumed.session_id || '' });
|
|
@@ -888,7 +878,7 @@ export async function fork_room(home, raw) {
|
|
|
888
878
|
continue;
|
|
889
879
|
}
|
|
890
880
|
// Claude resumes a session only in its original directory, so the forked member works there too.
|
|
891
|
-
const located = profile.adapter === '
|
|
881
|
+
const located = adapter_profile(profile.adapter)?.adopt === 'own_cwd' ? borrowed_session(name, profile, local.cwd) : profile;
|
|
892
882
|
const branch = await branch_session(located, profile.session_id, (left_out[name] || []).filter(Number.isInteger), { always: true });
|
|
893
883
|
const { session_id, ...forked } = located;
|
|
894
884
|
participants[name] = forked;
|
|
@@ -1051,7 +1041,7 @@ function connection_view(context) {
|
|
|
1051
1041
|
|
|
1052
1042
|
/** Switch only Codex's execution entrypoint, keeping its actual original conversation. */
|
|
1053
1043
|
function switch_codex_adapter(context, name, profile, adapter) {
|
|
1054
|
-
if (!
|
|
1044
|
+
if (!same_session_family(profile.adapter, adapter)) throw new Error('本场仅支持共用会话的 App / CLI 接入之间切换');
|
|
1055
1045
|
const current = validate_config(context.config, dirname(context.path));
|
|
1056
1046
|
let session = profile.session_id;
|
|
1057
1047
|
if (existsSync(join(current.directory, 'runtime.sqlite3'))) {
|
|
@@ -1072,8 +1062,8 @@ function corrected_configuration(context) {
|
|
|
1072
1062
|
const config = structuredClone(context.config);
|
|
1073
1063
|
for (const [name, patch] of Object.entries(context.input.participants)) {
|
|
1074
1064
|
const profile = config.participants[name];
|
|
1075
|
-
const field = profile?.adapter
|
|
1076
|
-
const fields =
|
|
1065
|
+
const field = adapter_profile(profile?.adapter)?.location || 'executable';
|
|
1066
|
+
const fields = ADAPTER_IDS.some(other => same_session_family(other, profile?.adapter)) ? [field, 'adapter'] : [field];
|
|
1077
1067
|
if (!profile || !patch || typeof patch !== 'object' || Array.isArray(patch) || Object.keys(patch).some(key => !fields.includes(key))) throw new Error('本场修正仅支持软件路径、Sidecar 目录及 Codex App / CLI;参与者、项目及会话保持不变');
|
|
1078
1068
|
validate_profile(name, patch);
|
|
1079
1069
|
if (patch.adapter && patch.adapter !== profile.adapter) switch_codex_adapter(context, name, profile, patch.adapter);
|
|
@@ -1242,14 +1232,14 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
1242
1232
|
assert_shared_home(home);
|
|
1243
1233
|
let result;
|
|
1244
1234
|
if (positionals[0] === 'settings') result = { ...(input ? save_launch_settings(home, input) : launch_settings(home)), api_providers: API_PROVIDERS,
|
|
1245
|
-
scheduler_providers: SCHEDULER_PROVIDERS, api_keys: saved_api_keys(home), projects: launch_projects(home).projects, ...expert_presets(home) };
|
|
1235
|
+
scheduler_providers: SCHEDULER_PROVIDERS, scheduler_modes: SCHEDULER_MODES, scheduler_default_mode: DEFAULT_SCHEDULER_MODE, api_keys: saved_api_keys(home), projects: launch_projects(home).projects, ...expert_presets(home) };
|
|
1246
1236
|
else if (positionals[0] === 'projects') result = input ? save_launch_projects(home, input) : launch_projects(home);
|
|
1247
1237
|
else if (positionals[0] === 'presets') result = input ? save_expert_presets(home, input) : expert_presets(home);
|
|
1248
1238
|
else if (positionals[0] === 'api-key' && input) result = save_api_key(home, input);
|
|
1249
1239
|
else if (positionals[0] === 'api-check' && input) result = await api_check(home, input);
|
|
1250
1240
|
else if (positionals[0] === 'scheduler-check' && input) result = await scheduler_check(home, input);
|
|
1251
1241
|
else if (positionals[0] === 'room-models' && input) result = await room_models(home, input);
|
|
1252
|
-
else if (positionals[0] === 'models' && input?.adapter === 'api') result = await api_models(home, input);
|
|
1242
|
+
else if (positionals[0] === 'models' && adapter_profile(input?.adapter)?.kind === 'api') result = await api_models(home, input);
|
|
1253
1243
|
else if (positionals[0] === 'models' && input) {
|
|
1254
1244
|
const saved = Object.values(launch_settings(home).participants).find(profile => profile.adapter === input.adapter);
|
|
1255
1245
|
result = await cached_software_models(home, { adapter: input.adapter, executable: input.executable || saved?.executable,
|
|
@@ -7,7 +7,7 @@ import { setTimeout as sleep } from 'node:timers/promises';
|
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
9
|
|
|
10
|
-
import { atomic_json } from './
|
|
10
|
+
import { atomic_json } from './files.mjs';
|
|
11
11
|
import { acquire_lock } from './discussion_runner.mjs';
|
|
12
12
|
import { assert_shared_home, main as service_main, service_home, service_pid, service_ready, start_service } from './discussion_service.mjs';
|
|
13
13
|
import { detect_python, local_settings } from './discussion_setup.mjs';
|
|
@@ -7,7 +7,8 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
import { parseArgs } from 'node:util';
|
|
9
9
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
10
|
-
import {
|
|
10
|
+
import { adapter_of, capabilities_of, validate_participant } from './adapters/index.mjs';
|
|
11
|
+
import { adapter_profile, same_session_family } from './adapters/profiles.mjs';
|
|
11
12
|
import { read_api_key } from './discussion_api.mjs';
|
|
12
13
|
import { access_levels, PROJECT_ACCESS, READ_RULE } from './expert_prompts.mjs';
|
|
13
14
|
import { validate_scheduler } from './scheduler.mjs';
|
|
@@ -22,11 +23,9 @@ let rg_available;
|
|
|
22
23
|
export function read_rule(adapter) {
|
|
23
24
|
if (process.platform !== 'win32') return '';
|
|
24
25
|
rg_available ??= spawnSync('rg', ['--version'], { stdio: 'ignore', windowsHide: true }).status === 0;
|
|
25
|
-
return rg_available || adapter
|
|
26
|
+
return rg_available || capabilities_of(adapter).ripgrep ? READ_RULE : '';
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const ADAPTER_NAMES = ['codex', 'claude', 'antigravity', 'codex_cli', 'antigravity_cli', 'api'];
|
|
29
|
-
export const CODEX_ADAPTERS = ['codex', 'codex_cli'];
|
|
30
29
|
const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
31
30
|
/** The shared prompt for rounds bridged without named follow-ups: experts answer and question each other. */
|
|
32
31
|
export function peer_round_question(round) {
|
|
@@ -55,7 +54,7 @@ export function validate_config(input, base_directory) {
|
|
|
55
54
|
// A free room of manual members runs here only for smart scheduling or a preset project directory, which need no local software.
|
|
56
55
|
if (!config.participants || (!Object.keys(config.participants).length && !(config.mode === 'free' && (config.scheduler || config.cwd)))) throw new Error('请配置本机参与软件');
|
|
57
56
|
for (const participant of Object.values(config.participants)) {
|
|
58
|
-
if (participant.adapter === 'api') {
|
|
57
|
+
if (adapter_profile(participant.adapter)?.kind === 'api') {
|
|
59
58
|
read_api_key(config.api_home, participant);
|
|
60
59
|
for (const field of ['cwd', 'project_name', 'project_id', 'executable', 'effort']) delete participant[field];
|
|
61
60
|
participant.project_access = 'read';
|
|
@@ -66,12 +65,11 @@ export function validate_config(input, base_directory) {
|
|
|
66
65
|
participant.project_access = participant.project_access || config.project_access || 'read';
|
|
67
66
|
// `own` is set only when the service takes over a manual member: it keeps its own session's permissions.
|
|
68
67
|
if (![...PROJECT_ACCESS, 'own'].includes(participant.project_access)) throw new Error('project_access 只能为 read、write、auto 或 full');
|
|
69
|
-
if (participant.project_access !== 'own' && !access_levels(participant.adapter).includes(participant.project_access))
|
|
68
|
+
if (participant.project_access !== 'own' && !access_levels(participant.adapter).includes(participant.project_access)) {
|
|
69
|
+
throw new Error(adapter_profile(participant.adapter)?.unsupported_hints?.[participant.project_access] || '该软件不支持此项目权限档位');
|
|
70
|
+
}
|
|
70
71
|
if (participant.cwd && !statSync(participant.cwd).isDirectory()) throw new Error('cwd 必须是已有本机目录');
|
|
71
|
-
|
|
72
|
-
if (participant.adapter === 'antigravity' && !participant.ipc_directory) throw new Error('缺少 Antigravity Sidecar IPC 目录');
|
|
73
|
-
if (participant.adapter !== 'antigravity' && !existsSync(participant.executable ?? '')) throw new Error('请指定实际原生可执行文件');
|
|
74
|
-
if (participant.adapter === 'codex' && !participant.source_thread_id && !participant.session_id) throw new Error('Codex 新建入口需要实际来源聊天 ID');
|
|
72
|
+
validate_participant(participant);
|
|
75
73
|
}
|
|
76
74
|
if (![undefined, 'free', 'brainstorm'].includes(config.mode)) throw new Error('房间模式只能为 free 或 brainstorm');
|
|
77
75
|
if (config.mode === 'free' && Object.keys(config.participants).some(name => typeof config.members?.[name]?.token !== 'string' || config.members[name].token.length < 32)) {
|
|
@@ -124,8 +122,8 @@ export class DiscussionStore {
|
|
|
124
122
|
const next = entries[index];
|
|
125
123
|
if (JSON.stringify(item) === JSON.stringify(next)) return true;
|
|
126
124
|
const session = this.binding(item[0]).session_id || item[6];
|
|
127
|
-
return item[0] === next[0] && item[1]
|
|
128
|
-
&&
|
|
125
|
+
return item[0] === next[0] && same_session_family(item[1], next[1])
|
|
126
|
+
&& session && next[6] === session && (!item[6] || item[6] === session)
|
|
129
127
|
&& JSON.stringify(item.slice(2, 6)) === JSON.stringify(next.slice(2, 6));
|
|
130
128
|
});
|
|
131
129
|
// Experts invited during a run only add entries; every existing binding must stay as it was.
|
|
@@ -191,7 +189,10 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
191
189
|
}
|
|
192
190
|
const config = load_config(values.config);
|
|
193
191
|
if (positionals[0] === 'setup-sidecar') {
|
|
194
|
-
for (const participant of Object.values(config.participants)
|
|
192
|
+
for (const participant of Object.values(config.participants)) {
|
|
193
|
+
const setup = adapter_of(participant.adapter)?.setup;
|
|
194
|
+
if (setup) process.stdout.write(JSON.stringify(setup(participant)) + '\n');
|
|
195
|
+
}
|
|
195
196
|
return;
|
|
196
197
|
}
|
|
197
198
|
const run = await api(config, '/discussion');
|
|
@@ -8,7 +8,8 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
9
|
import { parseArgs } from 'node:util';
|
|
10
10
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
11
|
-
import {
|
|
11
|
+
import { same_session_family } from './adapters/profiles.mjs';
|
|
12
|
+
import { atomic_json } from './files.mjs';
|
|
12
13
|
import { DiscussionStore, acquire_lock, api, load_config, pause_if_running, service_drafts, validate_config } from './discussion_runner.mjs';
|
|
13
14
|
import { FreeRoomRunner } from './free_runner.mjs';
|
|
14
15
|
|
|
@@ -222,8 +223,7 @@ class RoomWorker {
|
|
|
222
223
|
}
|
|
223
224
|
const codex_switches = Object.entries(this.config.participants).filter(([name, previous]) => {
|
|
224
225
|
const next = updated.participants[name];
|
|
225
|
-
return next && previous.adapter
|
|
226
|
-
&& ['codex', 'codex_cli'].includes(previous.adapter) && ['codex', 'codex_cli'].includes(next.adapter);
|
|
226
|
+
return next && same_session_family(previous.adapter, next.adapter);
|
|
227
227
|
});
|
|
228
228
|
const codex_switch = codex_switches.length > 0;
|
|
229
229
|
const discussion = codex_switch ? await api(this.config, '/discussion') : null;
|
|
@@ -6,7 +6,7 @@ import { basename, delimiter, dirname, isAbsolute, join, resolve } from 'node:pa
|
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { parseArgs } from 'node:util';
|
|
8
8
|
|
|
9
|
-
import { atomic_json } from './
|
|
9
|
+
import { atomic_json } from './files.mjs';
|
|
10
10
|
import { launch_settings, save_launch_settings } from './discussion_launch.mjs';
|
|
11
11
|
import { assert_shared_home, service_home } from './discussion_service.mjs';
|
|
12
12
|
import { SKILLS, install_skills, remove_legacy_skills } from './discussion_skills.mjs';
|