codexteer 0.18.1 → 0.19.0

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 CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  公開日は日本時間です。
4
4
 
5
+ ## 0.19.0 — 2026-11-01公開
6
+
7
+ - タスクIDと接続指定の不一致エラーに、修正用の`--connection`とJSONの`expected_connection`・`connection`を追加しました。Desktop Cloudには`desktop-cloud`、Web Cloudには`cloud`、ローカルには既定の`shared`を示します。拒否・終了コード・既存エラーコードを維持し、自動切り替えは行いません。
8
+ - `help`の全30トピックを同じ構成に揃えました。各トピックは使い所・使い方・オプション(既定値)・確認できること・エラーと対処・手順・例・補足・関連を持ち、`--json`でも同じ項目名で取得できます。記述は実装と照合し、実装と食い違っていた説明(接続ソケットのリンク、Cloudの版確認の範囲、`status`の`unregistered`など)を直しました。`help desktop-cloud`と`help environments`の「使い所: undefined」表示と、`checkpoint run`・`resource run`の`--json`を`--`の後ろに示していた使い方表示も直しました。トピック名は変わりません。
9
+ - 文言や内部実装に固定されていたテストを整理し、ヘルプの記述とエラーコード・オプションの対応を検査するテストを追加しました。
10
+ - Desktop Cloudで受付不明の送信・プロジェクト割り当てが残ると、以後の送信・`thread assign`が永久に拒否される問題を修正しました。Desktopのプロフィール状態で割り当てを確認できた記録は自動で確定します。確認できない記録は、ユーザーが`history mark <TASK> <MESSAGE-ID> --resolve-unknown --note TEXT`または`thread receipt <ATTEMPT> --resolve-unknown --note TEXT`で理由を残して解除できます(`--supervisor`付きは`SUPERVISOR_CONTROL_FORBIDDEN`)。記録がないことを未配送の証拠にはしません。解除した送信は、結び付いた指摘でも受付不明として扱いません。拒否エラーには解除対象の`existing_message_id`・`existing_attempt_id`を含めます。
11
+ - ローカルの`send`が、`--`より前の未知のオプション(`--kidn`などの打ち間違い)を本文として送っていた問題を修正し、`OPTIONS_INVALID`で拒否するようにしました。`--`より後ろは従来どおり本文です。
12
+ - ローカルの`resource`で、`--supervisor`付きの`acquire`・`renew`・`release`・`status`・`list`が監督状態を確認していなかった問題を修正しました。`--supervisor`付きは`--thread`が必須になり、停止済み・別セッションは拒否、一時停止中は`acquire`・`renew`・`run`を`SUPERVISOR_PAUSED`で拒否します。
13
+ - ローカルタスクへの`diff`が送信の省略形として解釈されていた問題を修正し、接続前に`OPTIONS_INVALID`で拒否します。
14
+ - コードを持たなかった利用者向けの失敗に、エラーコードを付けました(`OPTIONS_INVALID`、`MESSAGE_NOT_FOUND`、`CHECKPOINT_NOT_FOUND`、`INSTRUCTION_NOT_REPLACEABLE`、`RESOURCE_NOT_OWNED`、`TASK_IDLE`、`TASK_BUSY`、`DESKTOP_START_UNSUPPORTED`、`DESKTOP_ALREADY_RUNNING`、`DESKTOP_START_FAILED`、`DESKTOP_UI_NOT_READY`、`DESKTOP_UI_FAILED`、`CURSOR_TOO_LARGE`、`STORE_UNSAFE`など)。実行中のターンがない送信と、idleでないタスクへの`--new-turn`は、`NOT_SENT`ではなく`TASK_IDLE`・`TASK_BUSY`になります。中断したローカルの`checkpoint run`・`resource run`は`error_code: CANCELLED`を返します。
15
+
16
+ ## 0.18.2 — 2026-11-01公開
17
+
18
+ - 停止済みの監督IDが、別の監督への交代後に古い本文から再登録できる問題を修正しました。停止履歴を監督記録に保持し、登録前のdoctor・再登録・観測・送信で`SUPERVISOR_STOPPED`を返します。旧形式の停止済み記録も、交代時に履歴へ引き継ぎます。保存済み旧CLIを使う監督には、更新後のCLIでプロンプトを生成し直してください。
19
+ - 同じ監督IDの別タスクへの登録を拒否します。既存データでIDが重複している場合も`supervise list --json`は全件を返し、`session_conflict.thread_ids`と、他タスクの観測記録を除外した項目の`observation_error`で衝突を示します。各タスクの`supervise stop`で停止できます。
20
+
5
21
  ## 0.18.1 — 2026-10-31
6
22
 
7
23
  - ローカルロックの保持者と回収者が続けてクラッシュした後、`RECORD_BUSY`から自動回復できなくなる問題を修正しました。回収処理にもPIDと世代トークンを持つロックを使い、生きた保持者・回収者の排他を維持します。
package/README.md CHANGED
@@ -48,6 +48,8 @@ npx -y codexteer@0.18.0 supervise <thread-id>
48
48
 
49
49
  DesktopのCloud環境構成は、`codexteer --connection desktop-cloud environments list --json`で取得します。新規作成は`thread create --environment <ID_OR_EXACT_NAME>`、継続・監視・監督は`durable_`付きIDまたは`hostId=durable`付きリンクで共通コマンドを使います。所属情報と登録APIの可否は`doctor --thread ... --project ...`で区別します。公式の署名付き`thread assign`は対象がそのDesktopの実行環境を使う場合に限り利用でき、通常のCloud環境では`--project`付き作成を現在も事前拒否します。[操作・確認済み範囲と制限](docs/desktop-cloud.md)
50
50
 
51
+ タスクと明示した接続が合わない場合は拒否し、エラー文に修正用の`--connection`、JSONの`error.expected_connection`に修正候補、`error.connection`に今回の指定値を返します。`durable_`付きID・`hostId=durable`付きリンクには`--connection desktop-cloud`、Web Cloudの`task_`付きID・公式リンクには`--connection cloud`を使います。ローカルUUIDには既定の`--connection shared`を候補として示します。ローカルの観測には`--connection desktop`も利用できますが、送信には使えません。明示指定を自動で切り替えることはありません。
52
+
51
53
  ### Codex Cloudを使う
52
54
 
53
55
  CloudタスクのID・公式リンクで、同じ`supervise`・`read/watch`・`send`・`history/instructions/findings`を使えます。監督の`pause/resume/stop`や保存済みCLIも共通です。接続先をまだ選んでいない一覧と作成は`--connection cloud projects list`・`threads list`・`thread create --project <ENV_ID>`を使います。実行中のCloud送信は同じタスクの元ターンを中断して後続ターンを開始する方式です。[操作とサービス上の違い](docs/cloud.md)
@@ -253,6 +255,10 @@ codexteer supervise stop <thread-id>
253
255
 
254
256
  直接起動はエージェントを起動する前に登録し、終了時に停止を記録します。本文だけの生成では未登録で、貼り付け先が本文中の`supervise register`を実行して開始します。一つのタスクに一つの監督を登録でき、競合は`SUPERVISOR_CONFLICT`になります。異常終了などで残った登録は、`status`で確認して明示的に`stop`してから開始し直してください。停止済みIDや古い本文で再開はできません。同じ本文の再登録で一時停止が解除されることもありません。
255
257
 
258
+ 停止済みIDは交代後も監督記録の履歴に保持し、登録前の`doctor`・再登録・観測・送信を`SUPERVISOR_STOPPED`で拒否します。更新前の停止済み記録も、交代時に履歴へ引き継ぎます。更新前にすでに上書きされて失われた停止履歴は復元できません。保存済み旧CLIには修正が反映されないため、更新後のCLIでプロンプトを生成し直してください。
259
+
260
+ 同じ`CODEX_HOME`・接続先ホストの中では、監督IDを別タスクへ登録できません(`SUPERVISOR_CONFLICT`。停止済みなら`SUPERVISOR_STOPPED`)。旧版で同じIDを複数タスクに登録したデータも、`supervise list --json`は全件を返し、該当項目の`session_conflict.thread_ids`に衝突先を示します。他タスクの観測記録は`observation: null`と`observation_error`で明示し、そのタスクの観測として扱いません。重複したIDでの観測・送信は拒否されます。ユーザーの端末から各タスクを`supervise stop`し、新しいIDの監督を開始してください。
261
+
256
262
  送信中の`pause`/`stop`は`RECORD_BUSY`で失敗します。配送結果を確認して制御を再実行し、成功するまでは停止したと扱わないでください。監督役が自分で`resume`したり、生成コマンドから`--supervisor`を外して回避したりしないよう、必須テンプレートにも指示します。
257
263
 
258
264
  この制御は新しい監督ID付きコマンドに適用します。以前の版で開始した監督、IDなしの手動送信、同じOSユーザーが実行する任意のコードを隔離する機能ではありません。監督IDは秘密情報ではなく、古いセッションや操作の競合を検出する識別子です。
@@ -5,6 +5,10 @@
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
+
10
+ `0.19.0`は、`help`の全トピックを同じ構成に揃えて実装と照合し、Desktop Cloudで受付不明の記録が残ると以後の送信・割り当てが止まる問題、ローカル`send`の未知オプションの送信、`--supervisor`付きローカル`resource`の監督状態の未確認を修正します。利用者向けの失敗にはエラーコードを付けました。実行中のターンがない送信の`NOT_SENT`は`TASK_IDLE`・`TASK_BUSY`に変わるため、コードで分岐している呼び出し側は更新してください。runtime protocolとcapability契約はv1を維持し、Desktopの再起動は必要ありません。保存済み旧CLIで動く監督には反映されないため、更新後のCLIで監督を起動し直すか、プロンプトを生成し直してください。
11
+
8
12
  `package.json` は `private` を持たず、`publishConfig` でnpm公式レジストリへのpublic公開を指定しています。公開可能な設定であることと、実際に公開済みであることは別です。
9
13
 
10
14
  公開後の基本的な実行形式は次のとおりです。通常はバージョン指定なしで使えます。監督開始後は保存したCLIを使い続けるため、監督中の一貫性を保つために版を明示する必要はありません。起動側と操作側の製品バージョンも、必要な通信仕様が対応していれば異なっていても使えます。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexteer",
3
- "version": "0.18.1",
3
+ "version": "0.19.0",
4
4
  "description": "Send steering messages to running Codex Desktop threads from a terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -74,7 +74,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
74
74
  const input = [{ type: "text", text: message }];
75
75
  if (!newTurn) {
76
76
  const turn = activeTurn(thread);
77
- if (!turn) throw new Error("The task has no eligible active turn. Read it again before sending; use --new-turn only for an idle task.");
77
+ if (!turn) throw Object.assign(new Error("The task has no eligible active turn. Read it again before sending; use --new-turn only for an idle task."), { code: "TASK_IDLE" });
78
78
  await beforeSend?.(thread, client);
79
79
  // Desktop uses clientId on the resulting userMessage to render external
80
80
  // steering as a user bubble instead of just an anonymous "steered" marker.
@@ -84,7 +84,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
84
84
  if (accepted?.turnId !== turn.id) throw new RpcFailure("App Server returned an unexpected turn ID. Do not retry automatically.", { code: "PROTOCOL_ERROR", uncertain: true });
85
85
  return { turn_id: accepted.turnId, client_message_id: clientUserMessageId };
86
86
  }
87
- if (!["idle", "notLoaded"].includes(thread.status?.type) || thread.turns.some(turn => turn.status === "inProgress")) throw new Error("--new-turn requires an idle task. Nothing was sent.");
87
+ if (!["idle", "notLoaded"].includes(thread.status?.type) || thread.turns.some(turn => turn.status === "inProgress")) throw Object.assign(new Error("--new-turn requires an idle task. Nothing was sent."), { code: "TASK_BUSY" });
88
88
  // Resume through Desktop's long-lived connection so approval ownership
89
89
  // survives the short-lived sender. Never resume only on the sender connection.
90
90
  if (!subscribe) throw new Error("Desktop subscription support is required for --new-turn.");
@@ -93,7 +93,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
93
93
  await beforeSend?.(thread, client);
94
94
  await subscribe(threadId);
95
95
  const resumed = await readSendState(client, threadId);
96
- if (resumed.status?.type !== "idle" || resumed.turns.some(turn => turn.status === "inProgress")) throw new Error("Task became active while resuming. Nothing was sent.");
96
+ if (resumed.status?.type !== "idle" || resumed.turns.some(turn => turn.status === "inProgress")) throw Object.assign(new Error("Task became active while resuming. Nothing was sent."), { code: "TASK_BUSY" });
97
97
  await beforeSend?.(resumed, client);
98
98
  const clientUserMessageId = clientMessageId;
99
99
  const accepted = await client.request("turn/start", { threadId, input, clientUserMessageId }, { mutation: true });
@@ -110,7 +110,7 @@ export async function sendAppServerMessage(threadInput, message, { dryRun = fals
110
110
  const route = remote ? remoteControlConnection() : {};
111
111
  connect ??= route.connect ?? RpcClient.connect; discover ??= route.discover ?? discoverRuntime;
112
112
  const threadId = normalizeThreadId(threadInput);
113
- if (typeof message !== "string" || !message.trim()) throw new Error("Message must not be empty.");
113
+ if (typeof message !== "string" || !message.trim()) throw Object.assign(new Error("Message must not be empty."), { code: "OPTIONS_INVALID" });
114
114
  const plan = { thread_id: threadId, ...hostFields(), backend: "app-server", delivery_action: newTurn ? "new-turn" : "steer", message_characters: [...message].length, dry_run: dryRun };
115
115
  if (dryRun) return { ...plan, sent: false, delivery_status: "not_sent" };
116
116
  let client;
@@ -22,13 +22,13 @@ export async function captureInputs(roots, excludes = [], { allowMissing = false
22
22
  try { info = await lstat(file); } catch (e) { if (e.code === "ENOENT" && allowMissing) return; throw e; }
23
23
  if (info.isSymbolicLink()) {
24
24
  const target = await realpath(file), targetInfo = await lstat(target);
25
- if (targetInfo.isDirectory()) throw new Error("Directory symlinks are not snapshot inputs. Specify the real directory explicitly.");
25
+ if (targetInfo.isDirectory()) throw Object.assign(new Error("Directory symlinks are not snapshot inputs. Specify the real directory explicitly."), { code: "OPTIONS_INVALID" });
26
26
  records.set(file, { ...await fingerprintFile(file), link: await readlink(file), mtime_ms: targetInfo.mtimeMs, ctime_ms: targetInfo.ctimeMs });
27
27
  } else if (info.isDirectory()) {
28
28
  records.set(file, { type: "directory", ref: file, mode: info.mode & 0o777 });
29
29
  for (const name of (await readdir(file)).sort()) await visit(path.join(file, name));
30
30
  } else if (info.isFile()) records.set(file, { ...await fingerprintFile(file), mtime_ms: info.mtimeMs, ctime_ms: info.ctimeMs });
31
- else throw new Error("Snapshot inputs must be files or directories.");
31
+ else throw Object.assign(new Error("Snapshot inputs must be files or directories."), { code: "OPTIONS_INVALID" });
32
32
  }
33
33
  for (const root of roots) await visit(root);
34
34
  const files = [...records.values()].sort((a, b) => a.ref.localeCompare(b.ref));
@@ -42,18 +42,18 @@ function differences(before, after) {
42
42
 
43
43
  export async function getCheckpoint(threadInput, id, storage = {}) {
44
44
  const threadId = normalizeTaskId(threadInput), entry = await readRecord("checkpoints", id, storage);
45
- if (!entry || entry.thread_id !== threadId || entry.id !== id) throw new Error("Checkpoint not found for this task.");
45
+ if (!entry || entry.thread_id !== threadId || entry.id !== id) throw Object.assign(new Error("Checkpoint not found for this task."), { code: "CHECKPOINT_NOT_FOUND" });
46
46
  return entry;
47
47
  }
48
48
 
49
49
  export async function createCheckpoint(threadInput, name, { paths, excludes = [], cwd = process.cwd(), ...storage }) {
50
50
  const threadId = normalizeTaskId(threadInput);
51
- if (!name?.trim() || !paths?.length) throw new Error("capture requires a name and at least one --path.");
51
+ if (!name?.trim() || !paths?.length) throw Object.assign(new Error("capture requires a name and at least one --path."), { code: "OPTIONS_INVALID" });
52
52
  const scope = targetHost() === "local" ? { cwd: path.resolve(cwd), roots: [...new Set(paths.map(p => path.resolve(cwd, p)))], excludes: excludes.map(p => path.resolve(cwd, p)) }
53
53
  : await remoteCheckpointScope(threadId, paths, excludes);
54
54
  const { roots, excludes: omit } = scope;
55
55
  const snapshot = await captureInputs(roots, omit);
56
- if (!snapshot.files.some(f => f.type === "file")) throw new Error("No input files were selected.");
56
+ if (!snapshot.files.some(f => f.type === "file")) throw Object.assign(new Error("No input files were selected."), { code: "OPTIONS_INVALID" });
57
57
  const entry = { schema: 1, id: randomUUID(), thread_id: threadId, name, cwd: scope.cwd, created_at: new Date().toISOString(), snapshot, runs: [], artifacts: [] };
58
58
  await writeRecord("checkpoints", entry.id, entry, storage);
59
59
  return checkpointSummary(entry);
@@ -115,13 +115,13 @@ function watchInputs(snapshot) {
115
115
  }
116
116
 
117
117
  export async function runCheckpoint(threadInput, id, argv, { timeoutMs = 0, execBackend, remoteShell, supervisor, connection, ...storage } = {}) {
118
- if (!argv.length || !argv[0] || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw new Error("A command and timeout-ms between 0 and 86400000 are required.");
118
+ if (!argv.length || !argv[0] || !Number.isInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > 86400000) throw Object.assign(new Error("A command and timeout-ms between 0 and 86400000 are required."), { code: "OPTIONS_INVALID" });
119
119
  const threadId = normalizeTaskId(threadInput);
120
120
  return withStoreLock(`checkpoint-${id}`, async () => {
121
121
  const entry = await getCheckpoint(threadId, id, storage), watcher = targetHost() === "local" ? watchInputs(entry.snapshot) : await remoteWatchInputs(entry.snapshot);
122
122
  try {
123
123
  const before = await captureInputs(entry.snapshot.roots, entry.snapshot.excludes, { allowMissing: true });
124
- if (before.digest !== entry.snapshot.digest) throw new Error("Inputs changed since capture. Capture a new checkpoint before running.");
124
+ if (before.digest !== entry.snapshot.digest) throw Object.assign(new Error("Inputs changed since capture. Capture a new checkpoint before running."), { code: "STALE_CHECKPOINT" });
125
125
  const run = { id: randomUUID(), argv, status: "running", started_at: new Date().toISOString() };
126
126
  entry.runs.push(run); await writeRecord("checkpoints", id, entry, storage);
127
127
  let result;
@@ -145,12 +145,12 @@ export async function runCheckpoint(threadInput, id, argv, { timeoutMs = 0, exec
145
145
  }
146
146
 
147
147
  export async function attachArtifacts(threadInput, id, refs, storage = {}) {
148
- if (!refs.length) throw new Error("At least one --artifact file is required.");
148
+ if (!refs.length) throw Object.assign(new Error("At least one --artifact file is required."), { code: "OPTIONS_INVALID" });
149
149
  return withStoreLock(`checkpoint-${id}`, async () => {
150
150
  const verified = await verifyCheckpoint(threadInput, id, storage);
151
- if (!verified.valid) throw new Error("A successful run against unchanged inputs is required before attaching artifacts.");
151
+ if (!verified.valid) throw Object.assign(new Error("A successful run against unchanged inputs is required before attaching artifacts."), { code: "STALE_CHECKPOINT" });
152
152
  const artifacts = await collectEvidence(refs);
153
- if (artifacts.some(a => a.type !== "file")) throw new Error("Artifacts must be local files.");
153
+ if (artifacts.some(a => a.type !== "file")) throw Object.assign(new Error("Artifacts must be local files."), { code: "OPTIONS_INVALID" });
154
154
  const entry = await getCheckpoint(threadInput, id, storage);
155
155
  entry.artifacts.push(...artifacts.map(a => ({ ...a, run_id: entry.runs.at(-1).id, attached_at: new Date().toISOString() })));
156
156
  await writeRecord("checkpoints", id, entry, storage);