@linzumi/cli 0.0.4-beta → 0.0.5-beta

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/README.md CHANGED
@@ -27,7 +27,7 @@ runner. It wraps the same local runner that was previously started with
27
27
  Install the exact beta version:
28
28
 
29
29
  ```bash
30
- npm install -g @linzumi/cli@0.0.4-beta
30
+ npm install -g @linzumi/cli@0.0.5-beta
31
31
  ```
32
32
 
33
33
  Or install the current beta tag:
@@ -47,7 +47,7 @@ linzumi --version
47
47
  Expected CLI output:
48
48
 
49
49
  ```bash
50
- linzumi 0.0.4-beta
50
+ linzumi 0.0.5-beta
51
51
  ```
52
52
 
53
53
  ## Prod Quick Start
@@ -55,7 +55,7 @@ linzumi 0.0.4-beta
55
55
  For the Linzumi workspace in prod, this is the first command to try:
56
56
 
57
57
  ```bash
58
- npm install -g @linzumi/cli@0.0.4-beta
58
+ npm install -g @linzumi/cli@0.0.5-beta
59
59
 
60
60
  linzumi connect \
61
61
  --kandan-url wss://serve.kandanai.com \
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linzumi/cli",
3
- "version": "0.0.4-beta",
3
+ "version": "0.0.5-beta",
4
4
  "description": "Linzumi local Codex runner CLI.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -66,7 +66,7 @@ async function main(args: readonly string[]): Promise<void> {
66
66
  process.stdout.write(connectGuideText());
67
67
  return;
68
68
  case "version":
69
- process.stdout.write("linzumi 0.0.4-beta\n");
69
+ process.stdout.write("linzumi 0.0.5-beta\n");
70
70
  return;
71
71
  case "auth":
72
72
  await runAuthCommand(parsed.args);
@@ -144,7 +144,7 @@ export async function parseRunnerArgs(args: readonly string[]): Promise<RunnerOp
144
144
  }
145
145
 
146
146
  if (values.get("version") === true) {
147
- process.stdout.write("linzumi 0.0.4-beta\n");
147
+ process.stdout.write("linzumi 0.0.5-beta\n");
148
148
  process.exit(0);
149
149
  }
150
150
 
package/src/phoenix.ts CHANGED
@@ -13,6 +13,11 @@
13
13
  Spec: plans/2026-04-24-local-codex-runner-deep-quality-spec.md
14
14
  Relationship: Treats the Kandan websocket as a reconnectable transport so
15
15
  server restarts do not terminate the durable local Codex session.
16
+
17
+ - Date: 2026-04-26
18
+ Spec: plans/2026_04_26_linzumi_cli_review_followups_note.md
19
+ Relationship: Treats non-ok Phoenix replies for runner pushes as transport
20
+ failures so failed Kandan writes cannot be mistaken for synchronized state.
16
21
  */
17
22
  import {
18
23
  type JsonObject,
@@ -23,6 +28,7 @@ import {
23
28
  } from "./protocol";
24
29
 
25
30
  type PendingPush = {
31
+ readonly event: string;
26
32
  readonly resolve: (payload: JsonValue) => void;
27
33
  readonly reject: (error: Error) => void;
28
34
  };
@@ -112,6 +118,8 @@ export async function connectPhoenixClient(
112
118
 
113
119
  if (name === "phx_error") {
114
120
  pendingPush.reject(new Error("phoenix push failed"));
121
+ } else if (isNonOkPushReply(payload) && pendingPush.event !== "phx_join") {
122
+ pendingPush.reject(new Error(`phoenix push failed: ${replyErrorMessage(payload)}`));
115
123
  } else {
116
124
  pendingPush.resolve(payload);
117
125
  }
@@ -143,7 +151,7 @@ export async function connectPhoenixClient(
143
151
  const frame: PhoenixFrame = [null, ref, topic, event, payload];
144
152
 
145
153
  return new Promise((resolve, reject) => {
146
- pending.set(ref, { resolve, reject });
154
+ pending.set(ref, { event, resolve, reject });
147
155
  websocket.send(JSON.stringify(frame));
148
156
  });
149
157
  };
@@ -304,6 +312,10 @@ function isJoinReply(value: JsonValue): value is {
304
312
  }
305
313
 
306
314
  function joinErrorMessage(value: JsonValue): string {
315
+ return replyErrorMessage(value);
316
+ }
317
+
318
+ function replyErrorMessage(value: JsonValue): string {
307
319
  if (!isJsonObject(value)) {
308
320
  return "invalid reply";
309
321
  }
@@ -321,6 +333,10 @@ function joinErrorMessage(value: JsonValue): string {
321
333
  return "unknown";
322
334
  }
323
335
 
336
+ function isNonOkPushReply(value: JsonValue): boolean {
337
+ return isJsonObject(value) && value.status !== "ok";
338
+ }
339
+
324
340
  function isKandanControl(value: JsonValue): value is KandanControl {
325
341
  return isJsonObject(value) && typeof value.type === "string";
326
342
  }
package/src/runner.ts CHANGED
@@ -30,6 +30,11 @@
30
30
  Relationship: Leaves channel-scoped approval controls to `channelSession.ts`
31
31
  so the process runner remains lifecycle-only while Kandan safely resolves
32
32
  Codex app-server approval requests from the thread UI.
33
+
34
+ - Date: 2026-04-26
35
+ Spec: plans/2026_04_26_linzumi_cli_review_followups_note.md
36
+ Relationship: Rejects stale Kandan controls for previous local runner
37
+ instances before they can mutate the current Codex app-server session.
33
38
  */
34
39
  import { spawn, type ChildProcess } from "node:child_process";
35
40
  import { randomUUID } from "node:crypto";
@@ -270,6 +275,16 @@ async function openLocalCodexRunner(
270
275
 
271
276
  kandan.onControl(control => {
272
277
  log("kandan.control", { control });
278
+ if (!controlTargetsInstance(control, instanceId)) {
279
+ log("kandan.control_ignored", {
280
+ reason: "instance_id_mismatch",
281
+ instanceId,
282
+ controlInstanceId: control.instanceId,
283
+ controlType: control.type,
284
+ });
285
+ return;
286
+ }
287
+
273
288
  void (channelSession?.handleControl(control) ?? Promise.resolve(undefined))
274
289
  .then(handled => {
275
290
  if (handled !== undefined) {
@@ -286,12 +301,21 @@ async function openLocalCodexRunner(
286
301
  instanceId,
287
302
  message: error instanceof Error ? error.message : String(error),
288
303
  });
304
+ })
305
+ .catch(error => {
306
+ log("kandan.control_response_push_failed", {
307
+ message: error instanceof Error ? error.message : String(error),
308
+ });
289
309
  });
290
310
  });
291
311
 
292
312
  return { instanceId, codexUrl, close };
293
313
  }
294
314
 
315
+ function controlTargetsInstance(control: KandanControl, instanceId: string): boolean {
316
+ return control.instanceId === undefined || control.instanceId === instanceId;
317
+ }
318
+
295
319
  async function closeCleanupStack(cleanup: CleanupStack): Promise<void> {
296
320
  if (cleanup.closePromise !== undefined) {
297
321
  return cleanup.closePromise;