codexteer 0.15.0 → 0.15.1

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
@@ -1,6 +1,12 @@
1
1
  # Changelog
2
2
 
3
- ## 0.15.0 — 未公開
3
+ ## 0.15.1 — 未公開
4
+
5
+ - ページ履歴に対応するタスクへの`send`・`send --new-turn`は、本文を含まないターン状態を分割取得して送信前に確認します。長いタスクの全履歴が64 MiBを超え、観測はできても送信準備だけ失敗する問題を修正しました。`--based-on`等の安全確認は維持します。
6
+ - 64 MiBの受信上限を超えた場合は`PAYLOAD_TOO_LARGE`を返します。上限は維持し、送信前の`not_sent`と受付未確認の`unknown`を区別します。
7
+ - runtime protocolとcapability契約はv1を維持します。0.15.0で起動済みのDesktopを再起動せずに操作できます。起動済みの監督は保存した旧CLIを使うため、修正版を使うには監督を起動し直すか、プロンプトを生成し直してください。
8
+
9
+ ## 0.15.0 — 2026-09-12公開
4
10
 
5
11
  監督の一時停止、指摘の重複抑制と検証結果の追跡を追加する機能リリースです。
6
12
 
@@ -21,4 +27,4 @@
21
27
 
22
28
  ## 0.14.1 — 公開済み
23
29
 
24
- `codexteer`名で公開した版です。監督CLIの永続配置と`CODEX_HOME`固定、操作ごとの互換性判定、観測APIの診断を含みます。今回の監督制御・指摘管理・方針上書き・Codex CLI起動・観測専用の直接接続は0.15.0で追加します。
30
+ `codexteer`名で公開した版です。監督CLIの永続配置と`CODEX_HOME`固定、操作ごとの互換性判定、観測APIの診断を含みます。監督制御・指摘管理・方針上書き・Codex CLI起動・観測専用の直接接続は0.15.0で追加しました。
package/README.md CHANGED
@@ -10,7 +10,7 @@ macOS、Node.js 20以降とnpm、`/Applications/ChatGPT.app`にインストー
10
10
 
11
11
  リポジトリのcloneやグローバルインストールなしで実行できます。npmパッケージ名とCLI名は`codexteer`です。
12
12
 
13
- このブランチは次期版`0.15.0`です。npm公開済みの`0.14.1`は利用できますが、監督方針の上書き、停止・再開、指摘管理、Codex CLI起動、Desktopへの観測専用接続は`0.15.0`で追加します。公開前にこれらを使う場合は、このブランチを「ソースから導入する」の手順で導入してください。変更点は[CHANGELOG](CHANGELOG.md)を参照してください。
13
+ このブランチは修正版`0.15.1`を準備します。npm公開済みの`0.15.0`には監督方針の上書き、停止・再開、指摘管理、Codex CLI起動、Desktopへの観測専用接続が含まれます。長いタスクへの送信前確認を修正した`0.15.1`を公開前に使う場合は、このブランチを「ソースから導入する」の手順で導入してください。変更点は[CHANGELOG](CHANGELOG.md)を参照してください。
14
14
 
15
15
  ```bash
16
16
  npx -y codexteer --version
@@ -28,10 +28,10 @@ npx -y codexteer doctor --json
28
28
  <details>
29
29
  <summary>特定のバージョンで実行する場合(任意)</summary>
30
30
 
31
- 不具合の切り分けや、同じ版で監督を開始し直したい場合は、パッケージ名に`@バージョン`を付けられます。`0.15.0`の公開後にその版を選ぶ場合は、次のように指定します。
31
+ 不具合の切り分けや、同じ版で監督を開始し直したい場合は、パッケージ名に`@バージョン`を付けられます。`0.15.1`の公開後にその版を選ぶ場合は、次のように指定します。
32
32
 
33
33
  ```bash
34
- npx -y codexteer@0.15.0 supervise <thread-id>
34
+ npx -y codexteer@0.15.1 supervise <thread-id>
35
35
  ```
36
36
 
37
37
  </details>
@@ -347,9 +347,9 @@ codexteer help supervise prompt
347
347
  "prompt": "対象IDと保存したCLIの実行コマンドを含む監督プロンプト本文…",
348
348
  "deployment": {
349
349
  "codex_home": "/Users/me/.codex",
350
- "directory": "/Users/me/.codex/codex-steer/runtimes/0.15.0-<sha256>",
351
- "wrapper_path": "/Users/me/.codex/codex-steer/runtimes/0.15.0-<sha256>/bin/codexteer-wrapper.mjs",
352
- "version": "0.15.0",
350
+ "directory": "/Users/me/.codex/codex-steer/runtimes/0.15.1-<sha256>",
351
+ "wrapper_path": "/Users/me/.codex/codex-steer/runtimes/0.15.1-<sha256>/bin/codexteer-wrapper.mjs",
352
+ "version": "0.15.1",
353
353
  "sha256": "<sha256>",
354
354
  "reused": false
355
355
  },
@@ -447,6 +447,8 @@ codexteer send <thread-id> -- '--new-turn の処理を確認してください'
447
447
 
448
448
  `--source`から`--checkpoint`までの指示メタデータは`app-server`方式で使います。`accepted`は入力の受付を表します。`unknown`の場合は`history check`で確認してから再送を判断してください。
449
449
 
450
+ ページ履歴対応のタスクでは、送信前の状態確認も本文を含まない分割取得を使います。受信上限の64 MiBを超えた場合は`PAYLOAD_TOO_LARGE`で停止します。旧形式の履歴やv1の`--based-on`確認では全履歴の取得が必要です。修正前の保存済みCLIを使う監督には更新が自動適用されないため、更新したCLIで監督を起動し直すか、プロンプトを生成し直してください。
451
+
450
452
  送信音は既定でオンです。同梱の短い3音「プルッ」(約0.30秒、`assets/send-pururu.wav`)を、今回の入力が受け付けられた直後に鳴らします。`--no-sound`・`--dry-run`・受付失敗・受付未確認(UI方式を含む)では鳴りません。CodexやmacOSの通知設定は変更せず、音量・ミュートはMacの出力設定に従います。再生に失敗しても送信成功は維持され、`--json`では`data.sound.played`と鳴らなかった`reason`、通常出力では再生失敗の警告で確認できます。
451
453
 
452
454
  #### 状態・発言・変更の読み取りと監視
@@ -1,11 +1,11 @@
1
1
  # 配布と永続実行
2
2
 
3
3
  パッケージ名は `codexteer`、CLI名は `codexteer`、ライセンスはMITです。
4
- 旧npmパッケージは`@vinhphatfsg/codex-steer`です。`codexteer@0.14.1`は公開済みで、このブランチは次期版`0.15.0`を準備します。GitHubのPR作成・マージはnpm公開を行う操作ではありません。公開時は`npm whoami`でnpm側のユーザーを確認してください。
4
+ 旧npmパッケージは`@vinhphatfsg/codex-steer`です。`codexteer@0.15.0`は公開済みで、このブランチは修正版`0.15.1`を準備します。GitHubのPR作成・マージはnpm公開を行う操作ではありません。公開時は`npm whoami`でnpm側のユーザーを確認してください。
5
5
  `package.json` は `private` を持たず、`publishConfig` でnpm公式レジストリへのpublic公開を指定しています。公開可能な設定であることと、実際に公開済みであることは別です。
6
6
 
7
7
  公開後の基本的な実行形式は次のとおりです。通常はバージョン指定なしで使えます。監督開始後は保存したCLIを使い続けるため、監督中の一貫性を保つために版を明示する必要はありません。起動側と操作側の製品バージョンも、必要な通信仕様が対応していれば異なっていても使えます。
8
- 以下の新しい監督機能をnpxで使うには0.15.0の公開が必要です。公開前はこのブランチをソースから導入してください。`-y` はnpmの取得確認を省略します。
8
+ 以下の監督機能は公開済みの0.15.0から利用できます。長いタスクへの送信前確認の修正は0.15.1に含みます。修正版の公開前はこのブランチをソースから導入してください。`-y` はnpmの取得確認を省略します。
9
9
 
10
10
  ```text
11
11
  npx -y codexteer desktop start
@@ -148,6 +148,10 @@ APIの検証中に`CAPABILITY_UNVERIFIED`または`RUNTIME_PROTOCOL_UNVERIFIED`
148
148
 
149
149
  送信ではDesktopへの再開要求と実際の送信直前にも必要な仕様とruntimeの同一性を確認します。`--based-on`等の安全確認も再開前と送信直前に検証し、必要なAPIが使えないままタスクを再開しません。切り替わっていれば`RUNTIME_CHANGED`で拒否します。新規ターン用ソケットも使用前に所有者・権限・種類を再検査します。不明な受付結果の自動再送はしません。`help`・`supervise prompt`・ローカル履歴一覧・`--dry-run`はruntimeの版に依存しません。
150
150
 
151
+ 送信前の状態確認では、`historyMode: paginated`のタスクは`thread/read`の`includeTurns:false`と`thread/turns/list`の`itemsView:notLoaded`を使います。全ページからターンIDと状態を確認し、タスク情報を再確認してから送信します。ページ取得の失敗・非対応時は送信とDesktop経由の再開を拒否し、全履歴取得へ切り替えません。旧形式の履歴は従来の取得方式を使います。`--based-on`がv1の場合は従来どおり全履歴で鮮度を確認し、v2の場合は分割取得で確認します。
152
+
153
+ WebSocketの受信上限は1メッセージ64 MiBです。超過は`PAYLOAD_TOO_LARGE`として停止し、サーバーの本文や生のエラーは表示しません。送信APIを呼ぶ前なら`not_sent`、呼び出した後で受付を確認できなければ`unknown`を維持します。個別の履歴ページや旧形式の全履歴が上限を超える場合にも上限は解除しません。
154
+
151
155
  ## 公開前の検証
152
156
 
153
157
  ```bash
@@ -171,7 +175,7 @@ npxから模擬Claudeと模擬Codexを起動して保存したCLIへの参照を
171
175
 
172
176
  レビュー・検証を終えた版を、npm側の公開権限を持つアカウントで公開します。手動公開には2FAを有効にしたアカウントを使ってください。
173
177
 
174
- 公開済みのパッケージ名とバージョンの組み合わせは再公開できません。既存の`codexteer@0.14.1`は変更せず、今回の配布物は`codexteer@0.15.0`として準備します。公開は別途実行します。削除しても同じ名前・版番号の組み合わせを再利用できません。[npm publishの仕様](https://docs.npmjs.com/cli/v11/commands/npm-publish/)
178
+ 公開済みのパッケージ名とバージョンの組み合わせは再公開できません。既存の`codexteer@0.15.0`は変更せず、今回の配布物は`codexteer@0.15.1`として準備します。公開は別途実行します。削除しても同じ名前・版番号の組み合わせを再利用できません。[npm publishの仕様](https://docs.npmjs.com/cli/v11/commands/npm-publish/)
175
179
 
176
180
  ```bash
177
181
  npm login
@@ -180,11 +184,11 @@ npm run test:package
180
184
  npm publish --access public
181
185
  ```
182
186
 
183
- 公開後は、公開した版と起動コマンドの登録を確認します。次は0.15.0を公開した場合の例です。
187
+ 公開後は、公開した版と起動コマンドの登録を確認します。次は0.15.1を公開した場合の例です。
184
188
 
185
189
  ```bash
186
- npm view codexteer@0.15.0 version bin --json
187
- npx -y codexteer@0.15.0 --version
190
+ npm view codexteer@0.15.1 version bin --json
191
+ npx -y codexteer@0.15.1 --version
188
192
  ```
189
193
 
190
194
  `EPRIVATE`はリポジトリの`package.json`に公開禁止設定が残っていることを示します。`--access public`はその禁止を解除しません。
@@ -194,7 +198,7 @@ npx -y codexteer@0.15.0 --version
194
198
 
195
199
  ## 監督状態・指摘と接続方式
196
200
 
197
- ここからの監督制御、指摘管理、Codex CLI起動、接続方式の選択は0.15.0で追加します。保存済みCLIの機能自体は0.14.1から利用できます。製品の機能追加に伴うminor更新であり、runtime protocolと既存capabilityのv1契約は維持します。
201
+ ここからの監督制御、指摘管理、Codex CLI起動、接続方式の選択は0.15.0で追加しました。保存済みCLIの機能自体は0.14.1から利用できます。0.15.1は送信前確認の修正を含むpatch更新です。runtime protocolと既存capabilityのv1契約は維持し、0.15.0で起動したDesktopとの通信に再起動は不要です。
198
202
 
199
203
  保存したCLIへのコマンドは、canonical CODEX_HOME、Node guard、`--supervisor <session-id>`、`--connection shared|desktop`を一緒に保持します。各プロンプト生成は新しいIDを発行しますが、生成だけでは登録しません。直接起動はエージェント起動前に登録し、終了時に停止します。コピーした本文は最初のregisterで開始します。
200
204
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexteer",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Send steering messages to running Codex Desktop threads from a terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,7 @@ import { RpcClient, RpcFailure } from "./rpc.mjs";
6
6
  import { normalizeThreadId } from "./thread-id.mjs";
7
7
  import { ensureDesktopSubscription } from "./subscription.mjs";
8
8
  import { assertRuntimeOperation, compatibleClient } from "./compatibility.mjs";
9
+ import { turnMetadata } from "./paged-read.mjs";
9
10
 
10
11
  export class SendFailure extends Error {
11
12
  constructor(message, { code = "NOT_SENT", uncertain = false, threadId, rpcCode, capability, operation } = {}) {
@@ -31,9 +32,28 @@ function verifyThread(thread, threadId) {
31
32
  if (!Array.isArray(thread.turns)) throw new Error("App Server did not return the task's turn state.");
32
33
  }
33
34
 
34
- export async function sendOnClient(client, threadId, message, { newTurn = false, subscribe, clientMessageId = randomUUID(), beforeSend } = {}) {
35
- const { thread } = await client.request("thread/read", { threadId, includeTurns: true });
35
+ export async function readSendState(client, threadId) {
36
+ const { thread } = await client.request("thread/read", { threadId, includeTurns: false });
36
37
  verifyThread(thread, threadId);
38
+ if (thread.historyMode !== "paginated") {
39
+ const full = (await client.request("thread/read", { threadId, includeTurns: true })).thread;
40
+ verifyThread(full, threadId);
41
+ return full;
42
+ }
43
+ // Steering needs turn IDs/statuses, not the potentially huge item bodies.
44
+ // Mark these summaries incomplete so legacy --based-on checks still fetch
45
+ // their required evidence instead of mistaking them for full history.
46
+ const turns = (await turnMetadata(client, threadId)).map(turn => ({ ...turn, itemsView: "notLoaded" }));
47
+ const current = (await client.request("thread/read", { threadId, includeTurns: false })).thread;
48
+ verifyThread(current, threadId);
49
+ if (current.historyMode !== "paginated" || current.status?.type !== thread.status?.type) {
50
+ throw Object.assign(new Error("The task changed while checking turn state. Read it again before sending."), { code: "STALE_OBSERVATION" });
51
+ }
52
+ return { ...current, turns };
53
+ }
54
+
55
+ export async function sendOnClient(client, threadId, message, { newTurn = false, subscribe, clientMessageId = randomUUID(), beforeSend } = {}) {
56
+ const thread = await readSendState(client, threadId);
37
57
  const input = [{ type: "text", text: message }];
38
58
  if (!newTurn) {
39
59
  const turn = activeTurn(thread);
@@ -47,7 +67,7 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
47
67
  if (accepted?.turnId !== turn.id) throw new RpcFailure("App Server returned an unexpected turn ID. Do not retry automatically.", { code: "PROTOCOL_ERROR", uncertain: true });
48
68
  return { turn_id: accepted.turnId, client_message_id: clientUserMessageId };
49
69
  }
50
- if (!["idle", "notLoaded"].includes(thread.status?.type)) throw new Error("--new-turn requires an idle task. Nothing was sent.");
70
+ 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.");
51
71
  // Resume through Desktop's long-lived connection so approval ownership
52
72
  // survives the short-lived sender. Never resume only on the sender connection.
53
73
  if (!subscribe) throw new Error("Desktop subscription support is required for --new-turn.");
@@ -55,10 +75,9 @@ export async function sendOnClient(client, threadId, message, { newTurn = false,
55
75
  // resuming, too. Validate again after resume to catch intervening user input.
56
76
  await beforeSend?.(thread, client);
57
77
  await subscribe(threadId);
58
- const resumed = await client.request("thread/read", { threadId, includeTurns: true });
59
- verifyThread(resumed.thread, threadId);
60
- if (resumed.thread.status?.type !== "idle") throw new Error("Task became active while resuming. Nothing was sent.");
61
- await beforeSend?.(resumed.thread, client);
78
+ const resumed = await readSendState(client, threadId);
79
+ if (resumed.status?.type !== "idle" || resumed.turns.some(turn => turn.status === "inProgress")) throw new Error("Task became active while resuming. Nothing was sent.");
80
+ await beforeSend?.(resumed, client);
62
81
  const clientUserMessageId = clientMessageId;
63
82
  const accepted = await client.request("turn/start", { threadId, input, clientUserMessageId }, { mutation: true });
64
83
  if (typeof accepted?.turn?.id !== "string") throw new RpcFailure("App Server did not confirm a turn ID. Do not retry automatically.", { code: "PROTOCOL_ERROR", uncertain: true });
@@ -14,9 +14,9 @@ export async function turnMetadata(client, threadId) {
14
14
  const turns = [], seen = new Set(), ids = new Set(); let cursor;
15
15
  do {
16
16
  const page = await client.request("thread/turns/list", { threadId, itemsView: "notLoaded", sortDirection: "asc", limit: 100, ...(cursor ? { cursor } : {}) });
17
- if (!Array.isArray(page.data) || (page.nextCursor && (seen.has(page.nextCursor) || !page.data.length))) throw invalidCursor();
17
+ if (!Array.isArray(page?.data) || (page.nextCursor != null && (typeof page.nextCursor !== "string" || !page.nextCursor || seen.has(page.nextCursor) || !page.data.length))) throw invalidCursor();
18
18
  for (const t of page.data) {
19
- if (!t?.id || !t.status || ids.has(t.id)) throw invalidCursor();
19
+ if (typeof t?.id !== "string" || !t.id || typeof t.status !== "string" || !t.status || ids.has(t.id)) throw invalidCursor();
20
20
  turns.push({ id: t.id, status: t.status }); ids.add(t.id);
21
21
  }
22
22
  cursor = page.nextCursor; seen.add(cursor);
package/src/rpc.mjs CHANGED
@@ -14,12 +14,19 @@ export class RpcFailure extends Error {
14
14
  }
15
15
 
16
16
  function connectionCode(error) {
17
+ if (error?.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH") return "PAYLOAD_TOO_LARGE";
17
18
  if (["EACCES", "EPERM"].includes(error?.code)) return "PERMISSION_DENIED";
18
19
  if (error?.code === "ETIMEDOUT") return "TIMEOUT";
19
20
  if (["ENOENT", "ECONNREFUSED", "ECONNRESET", "ECONNABORTED", "EPIPE"].includes(error?.code)) return "CONNECTION_FAILED";
20
21
  return "PROTOCOL_ERROR";
21
22
  }
22
23
 
24
+ function connectionMessage(error, fallback) {
25
+ return error?.code === "WS_ERR_UNSUPPORTED_MESSAGE_LENGTH"
26
+ ? "App Server response exceeded the 64 MiB receive limit. Use paginated history access; the receive limit remains enforced."
27
+ : fallback;
28
+ }
29
+
23
30
  export function connectSocket(socketPath, { timeoutMs = 8000, signal } = {}) {
24
31
  return new Promise((resolve, reject) => {
25
32
  if (signal?.aborted) { reject(new RpcFailure("App Server connection cancelled.", { code: "CANCELLED" })); return; }
@@ -41,7 +48,7 @@ export function connectSocket(socketPath, { timeoutMs = 8000, signal } = {}) {
41
48
  const timer = setTimeout(() => finish(new RpcFailure("App Server connection timed out.", { code: "TIMEOUT" })), timeoutMs);
42
49
  // Keep an error listener even after connection so a peer reset never throws globally.
43
50
  socket.on("error", () => {});
44
- socket.once("error", error => finish(new RpcFailure("Could not connect to the shared App Server.", { code: connectionCode(error) })));
51
+ socket.once("error", error => finish(new RpcFailure(connectionMessage(error, "Could not connect to the shared App Server."), { code: connectionCode(error) })));
45
52
  socket.once("close", () => finish(new RpcFailure("App Server connection closed before initialization.")));
46
53
  socket.once("open", () => finish());
47
54
  signal?.addEventListener("abort", cancel, { once: true });
@@ -91,7 +98,7 @@ export class RpcClient extends EventEmitter {
91
98
  }
92
99
  });
93
100
  socket.on("close", () => this.rejectPending("App Server connection closed."));
94
- socket.on("error", error => this.abort("App Server connection failed.", connectionCode(error)));
101
+ socket.on("error", error => this.abort(connectionMessage(error, "App Server connection failed."), connectionCode(error)));
95
102
  }
96
103
 
97
104
  static async connect(socketPath, options = {}) {