codexteer 0.18.1 → 0.18.2
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/CHANGELOG.md +5 -0
- package/README.md +4 -0
- package/docs/distribution.md +2 -0
- package/package.json +1 -1
- package/src/cli.mjs +3 -7
- package/src/cloud-cli.mjs +3 -4
- package/src/supervision.mjs +58 -16
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
公開日は日本時間です。
|
|
4
4
|
|
|
5
|
+
## 0.18.2 — 2026-11-01公開
|
|
6
|
+
|
|
7
|
+
- 停止済みの監督IDが、別の監督への交代後に古い本文から再登録できる問題を修正しました。停止履歴を監督記録に保持し、登録前のdoctor・再登録・観測・送信で`SUPERVISOR_STOPPED`を返します。旧形式の停止済み記録も、交代時に履歴へ引き継ぎます。保存済み旧CLIを使う監督には、更新後のCLIでプロンプトを生成し直してください。
|
|
8
|
+
- 同じ監督IDの別タスクへの登録を拒否します。既存データでIDが重複している場合も`supervise list --json`は全件を返し、`session_conflict.thread_ids`と、他タスクの観測記録を除外した項目の`observation_error`で衝突を示します。各タスクの`supervise stop`で停止できます。
|
|
9
|
+
|
|
5
10
|
## 0.18.1 — 2026-10-31
|
|
6
11
|
|
|
7
12
|
- ローカルロックの保持者と回収者が続けてクラッシュした後、`RECORD_BUSY`から自動回復できなくなる問題を修正しました。回収処理にもPIDと世代トークンを持つロックを使い、生きた保持者・回収者の排他を維持します。
|
package/README.md
CHANGED
|
@@ -253,6 +253,10 @@ codexteer supervise stop <thread-id>
|
|
|
253
253
|
|
|
254
254
|
直接起動はエージェントを起動する前に登録し、終了時に停止を記録します。本文だけの生成では未登録で、貼り付け先が本文中の`supervise register`を実行して開始します。一つのタスクに一つの監督を登録でき、競合は`SUPERVISOR_CONFLICT`になります。異常終了などで残った登録は、`status`で確認して明示的に`stop`してから開始し直してください。停止済みIDや古い本文で再開はできません。同じ本文の再登録で一時停止が解除されることもありません。
|
|
255
255
|
|
|
256
|
+
停止済みIDは交代後も監督記録の履歴に保持し、登録前の`doctor`・再登録・観測・送信を`SUPERVISOR_STOPPED`で拒否します。更新前の停止済み記録も、交代時に履歴へ引き継ぎます。更新前にすでに上書きされて失われた停止履歴は復元できません。保存済み旧CLIには修正が反映されないため、更新後のCLIでプロンプトを生成し直してください。
|
|
257
|
+
|
|
258
|
+
同じ`CODEX_HOME`・接続先ホストの中では、監督IDを別タスクへ登録できません(`SUPERVISOR_CONFLICT`。停止済みなら`SUPERVISOR_STOPPED`)。旧版で同じIDを複数タスクに登録したデータも、`supervise list --json`は全件を返し、該当項目の`session_conflict.thread_ids`に衝突先を示します。他タスクの観測記録は`observation: null`と`observation_error`で明示し、そのタスクの観測として扱いません。重複したIDでの観測・送信は拒否されます。ユーザーの端末から各タスクを`supervise stop`し、新しいIDの監督を開始してください。
|
|
259
|
+
|
|
256
260
|
送信中の`pause`/`stop`は`RECORD_BUSY`で失敗します。配送結果を確認して制御を再実行し、成功するまでは停止したと扱わないでください。監督役が自分で`resume`したり、生成コマンドから`--supervisor`を外して回避したりしないよう、必須テンプレートにも指示します。
|
|
257
261
|
|
|
258
262
|
この制御は新しい監督ID付きコマンドに適用します。以前の版で開始した監督、IDなしの手動送信、同じOSユーザーが実行する任意のコードを隔離する機能ではありません。監督IDは秘密情報ではなく、古いセッションや操作の競合を検出する識別子です。
|
package/docs/distribution.md
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
`0.18.1`は、保持者と回収者の連続クラッシュでローカルロックが永久に`RECORD_BUSY`になる問題を修正します。更新前に、同じ`CODEX_HOME`を使う旧版CLIの操作と、保存済み旧CLIで動く監督を終了してください。旧版と修正版の回収方式の混在は非対応です。旧`.recover`ディレクトリは修正版では無視されます。監督は更新後のCLIで起動し直すか、プロンプトを生成し直してください。
|
|
7
7
|
|
|
8
|
+
`0.18.2`は、監督が交代した後も停止済みIDを履歴に保持し、古い本文からの再登録・観測・送信を`SUPERVISOR_STOPPED`で拒否します。登録前の`doctor`にも同じ判定を適用します。同じIDの別タスクへの登録を拒否し、旧版で重複したIDを持つ記録も`supervise list --json`で全件と衝突先を返します。該当タスクはそれぞれ`supervise stop`で停止できます。更新前の停止済み記録は交代時に引き継ぎますが、すでに上書きされて失われた停止履歴は復元できません。保存済み旧CLIには修正が反映されないため、更新後のCLIで監督を起動し直すか、プロンプトを生成し直してください。runtime protocolとcapability契約はv1を維持し、Desktopの再起動は必要ありません。
|
|
9
|
+
|
|
8
10
|
`package.json` は `private` を持たず、`publishConfig` でnpm公式レジストリへのpublic公開を指定しています。公開可能な設定であることと、実際に公開済みであることは別です。
|
|
9
11
|
|
|
10
12
|
公開後の基本的な実行形式は次のとおりです。通常はバージョン指定なしで使えます。監督開始後は保存したCLIを使い続けるため、監督中の一貫性を保つために版を明示する必要はありません。起動側と操作側の製品バージョンも、必要な通信仕様が対応していれば異なっていても使えます。
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { acquireResource, resourceStatus, renewResource, releaseResource, listRe
|
|
|
15
15
|
import { playSendSound } from "./sound.mjs";
|
|
16
16
|
import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
|
|
17
17
|
import { prepareOrchestrationPrompt, orchestrateAgent } from "./orchestrate.mjs";
|
|
18
|
-
import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
|
|
18
|
+
import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, assertSupervisorRegistration, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
|
|
19
19
|
import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
|
|
20
20
|
import { connectionMode, observationConnection, desktopReadOnly } from "./connection.mjs";
|
|
21
21
|
import { repairTurns } from "./turn-repair.mjs";
|
|
@@ -141,7 +141,7 @@ function printThreads(threads) {
|
|
|
141
141
|
function printSupervisor(data) {
|
|
142
142
|
if (Array.isArray(data)) {
|
|
143
143
|
if (!data.length) console.log("No supervisor sessions registered.");
|
|
144
|
-
for (const entry of data) console.log(`${entry.thread_id} ${entry.state} ${entry.owner} ${entry.connection} observed:${entry.observation?.last_observed_at ?? "none"}`);
|
|
144
|
+
for (const entry of data) console.log(`${entry.thread_id} ${entry.state} ${entry.owner} ${entry.connection} observed:${entry.observation?.last_observed_at ?? "none"}${entry.session_conflict ? ` session-conflict:${entry.session_conflict.thread_ids.join(",")}` : ""}${entry.observation_error ? ` observation-error:${entry.observation_error.code}` : ""}`);
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
147
|
console.log(`${data.thread_id} ${data.state}${data.owner ? ` ${data.owner} ${data.connection}` : ""}`);
|
|
@@ -459,11 +459,7 @@ async function mainOnTarget(argv) {
|
|
|
459
459
|
const target = takeOption(args, "--thread", undefined);
|
|
460
460
|
if (target !== undefined && backend !== "app-server") throw new Error("doctor --thread requires --backend app-server.");
|
|
461
461
|
const threadId = target === undefined ? undefined : normalizeThreadId(target);
|
|
462
|
-
if (supervisor && threadId) {
|
|
463
|
-
const record = await getSupervisor(threadId);
|
|
464
|
-
if (record && record.session_id !== supervisor && record.state !== "stopped") throw Object.assign(new Error("Another supervisor is registered for this task."), { code: "SUPERVISOR_CONFLICT" });
|
|
465
|
-
if (record?.session_id === supervisor) await assertSupervisor(threadId, supervisor, { connection });
|
|
466
|
-
}
|
|
462
|
+
if (supervisor && threadId) await assertSupervisorRegistration(threadId, supervisor, { connection });
|
|
467
463
|
if (args.length) throw new Error(`Unexpected argument: ${args[0]}`);
|
|
468
464
|
if (targetHost() !== "local" && backend === "ui") throw remoteReadOnly();
|
|
469
465
|
const report = backend === "ui" ? desktopDoctor() : await appServerDoctor({ threadId, ...connectionDependencies });
|
package/src/cloud-cli.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { observeThread, printObservation } from "./observe.mjs";
|
|
|
10
10
|
import { monitorCommand } from "./monitor.mjs";
|
|
11
11
|
import { notificationOptions } from "./notify.mjs";
|
|
12
12
|
import { prepareSupervisorPrompt, superviseAgent } from "./supervise.mjs";
|
|
13
|
-
import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
|
|
13
|
+
import { supervisorId, registerSupervisor, getSupervisor, assertSupervisor, assertSupervisorRegistration, controlSupervisor, supervisorStatus, listSupervisors } from "./supervision.mjs";
|
|
14
14
|
import { createFinding, findingStatus, listFindings, resolveFinding, dismissFinding, reopenFinding, evaluateFinding, findingStats } from "./findings.mjs";
|
|
15
15
|
import { sendTrackedMessage, listMessages, getMessage, messageSummary, reconcileMessages, markMessage, listInstructions } from "./journal.mjs";
|
|
16
16
|
import { createCloudTask, cloudTaskDiff, listCloudTasks, cloudCreationReceipt } from "./cloud.mjs";
|
|
@@ -84,9 +84,8 @@ export async function mainCloud(argv) {
|
|
|
84
84
|
const bound = async (target, { allowUnregistered = false } = {}) => {
|
|
85
85
|
const id = targetId(target);
|
|
86
86
|
if (supervisor) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
else if (record && record.state !== "stopped") throw failure("Another supervisor owns this cloud task.", "SUPERVISOR_CONFLICT");
|
|
87
|
+
if (allowUnregistered) await assertSupervisorRegistration(id, supervisor, { connection });
|
|
88
|
+
else await assertSupervisor(id, supervisor, { connection });
|
|
90
89
|
}
|
|
91
90
|
return id;
|
|
92
91
|
};
|
package/src/supervision.mjs
CHANGED
|
@@ -21,12 +21,35 @@ export function supervisorId(value) {
|
|
|
21
21
|
return value.toLowerCase();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// Keep retired generations in the same atomic record as the current owner.
|
|
25
|
+
// A legacy stopped record contributes its ID even before its first rewrite.
|
|
26
|
+
function stoppedSessionIds(record) {
|
|
27
|
+
return [...new Set([...(record?.stopped_session_ids ?? []), ...(record?.state === "stopped" ? [record.session_id] : [])])];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assertNotStopped(record, sessionId) {
|
|
31
|
+
if (stoppedSessionIds(record).includes(sessionId)) throw failure("This supervisor session has stopped. Start a new supervisor when needed.", "SUPERVISOR_STOPPED");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function supervisorRecords(storage) {
|
|
35
|
+
return (await Promise.all([listRecords("supervisors", storage), listRecords("cloud-supervisors", storage)])).flat().filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function assertSessionBinding(threadId, sessionId, storage) {
|
|
39
|
+
const others = (await supervisorRecords(storage)).filter(record => record.thread_id !== threadId);
|
|
40
|
+
// A stopped ID stays retired even when a caller changes its target task.
|
|
41
|
+
for (const record of others) assertNotStopped(record, sessionId);
|
|
42
|
+
if (others.some(record => record.session_id === sessionId)) throw failure("This supervisor session is already registered for another task. Start a new supervisor for this task.", "SUPERVISOR_CONFLICT");
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
export async function getSupervisor(threadInput, storage = {}) {
|
|
25
46
|
const threadId = normalizeTaskId(threadInput), record = await readRecord(supervisorBucket(threadId), threadId, storage);
|
|
26
47
|
if (!record) return null;
|
|
27
48
|
if (record.schema !== 1 || record.thread_id !== threadId || !states.has(record.state)
|
|
28
49
|
|| !["shared", "desktop", "cloud", "desktop-cloud"].includes(record.connection)
|
|
29
50
|
|| supervisorId(record.session_id) !== record.session_id || typeof record.owner !== "string"
|
|
51
|
+
|| (record.stopped_session_ids !== undefined && (!Array.isArray(record.stopped_session_ids)
|
|
52
|
+
|| record.stopped_session_ids.some(id => supervisorId(id) !== id)))
|
|
30
53
|
|| (record.launcher_pid !== null && (!Number.isSafeInteger(record.launcher_pid) || record.launcher_pid <= 0))) {
|
|
31
54
|
throw failure("Invalid supervisor state. Inspect the local record before continuing.", "SUPERVISOR_INVALID");
|
|
32
55
|
}
|
|
@@ -34,35 +57,45 @@ export async function getSupervisor(threadInput, storage = {}) {
|
|
|
34
57
|
}
|
|
35
58
|
|
|
36
59
|
export async function assertSupervisor(threadInput, sessionInput, { sending = false, connection, ...storage } = {}) {
|
|
37
|
-
const sessionId = supervisorId(sessionInput), record = await getSupervisor(
|
|
60
|
+
const threadId = normalizeTaskId(threadInput), sessionId = supervisorId(sessionInput), record = await getSupervisor(threadId, storage);
|
|
61
|
+
assertNotStopped(record, sessionId);
|
|
62
|
+
await assertSessionBinding(threadId, sessionId, storage);
|
|
38
63
|
if (!record) throw failure("Register this supervisor session before observing or sending.", "SUPERVISOR_NOT_REGISTERED");
|
|
39
64
|
if (record.session_id !== sessionId) throw failure("Another supervisor session owns this task. This command belongs to an older or different session.", "SUPERVISOR_MISMATCH");
|
|
40
65
|
if (connection !== undefined && record.connection !== connection) throw failure("The supervisor is registered for another connection. Keep the original command intact.", "SUPERVISOR_MISMATCH");
|
|
41
66
|
if (targetHost() !== "local" && (record.remote_transport ?? "native") !== remoteTransport()) throw failure("The supervisor belongs to a different remote transport. Keep the generated route intact.", "SUPERVISOR_MISMATCH");
|
|
42
|
-
if (record.state === "stopped") throw failure("This supervisor session has stopped. Start a new supervisor when needed.", "SUPERVISOR_STOPPED");
|
|
43
67
|
if (launcherAlive(record) === false) throw failure("The supervisor launcher exited. Stop the stale session before starting another supervisor.", "SUPERVISOR_ORPHANED");
|
|
44
68
|
if (sending && record.state === "paused") throw failure("Automatic intervention is paused. Observation can continue; only the user should resume intervention.", "SUPERVISOR_PAUSED");
|
|
45
69
|
return record;
|
|
46
70
|
}
|
|
47
71
|
|
|
72
|
+
// Read-only bootstrap check shared by doctor and registration. Registration
|
|
73
|
+
// repeats it under the task lock; a successful doctor does not reserve an ID.
|
|
74
|
+
export async function assertSupervisorRegistration(threadInput, sessionInput, { connection, ...storage } = {}) {
|
|
75
|
+
const threadId = normalizeTaskId(threadInput), sessionId = supervisorId(sessionInput), current = await getSupervisor(threadId, storage);
|
|
76
|
+
assertNotStopped(current, sessionId);
|
|
77
|
+
await assertSessionBinding(threadId, sessionId, storage);
|
|
78
|
+
if (current?.session_id === sessionId) return assertSupervisor(threadInput, sessionId, { connection, ...storage });
|
|
79
|
+
if (current && current.state !== "stopped") throw failure("A supervisor already owns this task. Use supervise status, then stop it explicitly before replacing it.", "SUPERVISOR_CONFLICT");
|
|
80
|
+
return current;
|
|
81
|
+
}
|
|
82
|
+
|
|
48
83
|
export async function registerSupervisor(threadInput, sessionInput, { owner = "external", launcherPid = null, connection = "shared", ...storage } = {}) {
|
|
49
84
|
const threadId = normalizeTaskId(threadInput), sessionId = supervisorId(sessionInput);
|
|
50
85
|
if (typeof owner !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/.test(owner)) throw failure("Supervisor owner must be a short agent name.", "SUPERVISOR_INVALID");
|
|
51
86
|
if (launcherPid !== null && (!Number.isSafeInteger(launcherPid) || launcherPid <= 0)) throw failure("Invalid supervisor launcher PID.", "SUPERVISOR_INVALID");
|
|
52
87
|
if (!["shared", "desktop", "cloud", "desktop-cloud"].includes(connection)) throw failure("Invalid supervisor connection.", "SUPERVISOR_INVALID");
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
if (current && current.state !== "stopped") throw failure("A supervisor already owns this task. Use supervise status, then stop it explicitly before replacing it.", "SUPERVISOR_CONFLICT");
|
|
88
|
+
// Registrations take the namespace-wide lock before the task lock. Stop,
|
|
89
|
+
// pause and sends only take the task lock and never acquire in reverse order.
|
|
90
|
+
return withStoreLock("supervisor-registration", () => withStoreLock(`supervisor-${threadId}`, async () => {
|
|
91
|
+
const current = await assertSupervisorRegistration(threadId, sessionId, { connection, ...storage });
|
|
92
|
+
if (current?.session_id === sessionId) return current;
|
|
60
93
|
const now = new Date().toISOString();
|
|
61
94
|
const record = { schema: 1, thread_id: threadId, session_id: sessionId, owner, launcher_pid: launcherPid, connection,
|
|
62
|
-
state: "active", registered_at: now, updated_at: now, last_delivery: null };
|
|
95
|
+
state: "active", stopped_session_ids: stoppedSessionIds(current), registered_at: now, updated_at: now, last_delivery: null };
|
|
63
96
|
await writeRecord(supervisorBucket(threadId), threadId, record, storage);
|
|
64
97
|
return record;
|
|
65
|
-
}, storage);
|
|
98
|
+
}, storage), storage);
|
|
66
99
|
}
|
|
67
100
|
|
|
68
101
|
export async function controlSupervisor(threadInput, action, { expectedSession, ...storage } = {}) {
|
|
@@ -76,23 +109,32 @@ export async function controlSupervisor(threadInput, action, { expectedSession,
|
|
|
76
109
|
if (record.state === "stopped" && action !== "stop") throw failure("A stopped supervisor cannot be resumed. Start a new supervisor.", "SUPERVISOR_STOPPED");
|
|
77
110
|
if (action === "resume") await assertSupervisor(threadId, record.session_id, storage);
|
|
78
111
|
const updated = { ...record, state: { pause: "paused", resume: "active", stop: "stopped" }[action], updated_at: new Date().toISOString() };
|
|
112
|
+
updated.stopped_session_ids = stoppedSessionIds(updated);
|
|
79
113
|
await writeRecord(supervisorBucket(threadId), threadId, updated, storage);
|
|
80
114
|
return updated;
|
|
81
115
|
}, storage);
|
|
82
116
|
}
|
|
83
117
|
|
|
84
|
-
export async function supervisorStatus(threadInput, storage = {}) {
|
|
118
|
+
export async function supervisorStatus(threadInput, { allowObservationConflict = false, ...storage } = {}) {
|
|
85
119
|
const threadId = normalizeTaskId(threadInput), record = await getSupervisor(threadId, storage);
|
|
86
120
|
if (!record) return { thread_id: threadId, state: "unregistered", observation: null };
|
|
87
|
-
|
|
121
|
+
let observation = await readRecord("supervisor-observations", record.session_id, storage), observationError;
|
|
122
|
+
if (allowObservationConflict && observation?.schema === 1 && observation.session_id === record.session_id && observation.thread_id !== threadId) {
|
|
123
|
+
observationError = { code: "SUPERVISOR_INVALID", message: "The observation belongs to another task.", thread_id: observation.thread_id };
|
|
124
|
+
observation = null;
|
|
125
|
+
}
|
|
88
126
|
if (observation && (observation.schema !== 1 || observation.thread_id !== threadId || observation.session_id !== record.session_id)) throw failure("Invalid supervisor observation record.", "SUPERVISOR_INVALID");
|
|
89
|
-
return { ...record, launcher_alive: launcherAlive(record),
|
|
127
|
+
return { ...record, launcher_alive: launcherAlive(record), ...(observationError ? { observation_error: observationError } : {}),
|
|
90
128
|
observation, observation_age_ms: observation?.last_observed_at ? Math.max(0, Date.now() - Date.parse(observation.last_observed_at)) : null };
|
|
91
129
|
}
|
|
92
130
|
|
|
93
131
|
export async function listSupervisors(storage = {}) {
|
|
94
|
-
const records =
|
|
95
|
-
return Promise.all(records.
|
|
132
|
+
const records = await supervisorRecords(storage);
|
|
133
|
+
return Promise.all(records.map(async record => {
|
|
134
|
+
const status = await supervisorStatus(record.thread_id, { ...storage, allowObservationConflict: true });
|
|
135
|
+
const conflicts = records.filter(other => other.session_id === record.session_id).map(other => other.thread_id);
|
|
136
|
+
return conflicts.length > 1 ? { ...status, session_conflict: { thread_ids: conflicts.sort() } } : status;
|
|
137
|
+
}));
|
|
96
138
|
}
|
|
97
139
|
|
|
98
140
|
// Observation has its own record so a concurrent read never overwrites pause/stop.
|