devez-vibe 1.9.53 → 1.9.54

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/bin/dvz.exe CHANGED
Binary file
@@ -3,7 +3,7 @@
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { execFile, spawn } from "node:child_process";
5
5
  import { createReadStream, existsSync, readFileSync, readdirSync } from "node:fs";
6
- import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
6
+ import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
7
7
  import { createRequire } from "node:module";
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { dirname, join, resolve as resolvePath, sep } from "node:path";
@@ -1522,13 +1522,18 @@ function runClaudeCommand(params, args, deadlineMs = CLAUDE_COMMAND_TIMEOUT) {
1522
1522
  const output = Buffer.concat(stdout).toString("utf8").trim();
1523
1523
  const detail = Buffer.concat(stderr).toString("utf8").trim();
1524
1524
  if (code === 0) resolve(output);
1525
- else reject(new Error(detail || output || `Claude 명령이 종료 코드 ${code}로 실패했습니다.`));
1525
+ else reject(Object.assign(new Error(detail || output || `Claude 명령이 종료 코드 ${code}로 실패했습니다.`), { exitCode: code }));
1526
1526
  });
1527
1527
  });
1528
1528
  }
1529
1529
 
1530
+ // Windows의 Claude Code는 가끔 출력을 다 낸 뒤에도 0xC0000005 같은 충돌 코드로
1531
+ // 끝난다. 읽기 명령이라 종료 코드로 실패하면 한 번 더 실행한다. 멈춤은 다시 기다리지 않는다.
1530
1532
  async function runClaudeJson(params, args) {
1531
- const output = await runClaudeCommand(params, args);
1533
+ const output = await runClaudeCommand(params, args).catch((error) => {
1534
+ if (error.exitCode === undefined) throw error;
1535
+ return runClaudeCommand(params, args);
1536
+ });
1532
1537
  try { return output ? JSON.parse(output) : null; }
1533
1538
  catch (error) {
1534
1539
  throw new Error(`Claude 플러그인 응답을 해석하지 못했습니다: ${error.message}`);
@@ -1890,6 +1895,13 @@ function questionFallbackMessage(questions) {
1890
1895
  ].join("\n");
1891
1896
  }
1892
1897
 
1898
+ // Windows는 충돌한 프로세스를 0xC0000000 이상의 종료 코드로 알린다(0xC0000005 접근
1899
+ // 위반, 0xC0000374 힙 손상). 설정·인증 오류의 일반 종료 코드와 달리 다시 띄우면 대개 뜬다.
1900
+ function isClaudeCliCrash(error) {
1901
+ const code = /process exited with code (\d+)/.exec(String(error?.message ?? error))?.[1];
1902
+ return Number(code) >= 0xC0000000;
1903
+ }
1904
+
1893
1905
  async function createSession(params, resumeId) {
1894
1906
  const id = resumeId || randomUUID();
1895
1907
  const queue = new AsyncQueue();
@@ -2569,12 +2581,16 @@ const SUBAGENT_PULSE_MS = 5000;
2569
2581
  // 자체 시험에서 "오래 조용한 행"을 만들 때 쓰는 나이.
2570
2582
  const IDLE_ROW_AGE_MS = 120_000;
2571
2583
 
2584
+ // 순정 Claude Code처럼 행에 Agent의 name과 description을 함께 보인다. description의
2585
+ // 언어는 매 턴 지침이 화면 언어에 맞춘다.
2572
2586
  function startSubagent(session, block) {
2573
2587
  const input = block.input || {};
2588
+ const description = firstLine(input.description || "", SUBAGENT_DESCRIPTION_LIMIT);
2574
2589
  const existing = findSubagent(session, block.id);
2575
2590
  if (existing) {
2576
2591
  existing.toolUseId = block.id;
2577
2592
  existing.name = firstLine(input.name || input.subagent_type || input.agentType || existing.name || "agent", 40);
2593
+ existing.description = description || existing.description;
2578
2594
  existing.backgroundTask = false;
2579
2595
  existing.lastSeenAt = Date.now();
2580
2596
  emitSubagents(session);
@@ -2586,8 +2602,7 @@ function startSubagent(session, block) {
2586
2602
  taskId: "",
2587
2603
  background: false,
2588
2604
  name: firstLine(input.name || input.subagent_type || input.agentType || "agent", 40),
2589
- // 행은 이름과 경과 시간만 보인다. 무엇을 시켰는지는 이름이 말하게 한다.
2590
- description: "",
2605
+ description,
2591
2606
  tool: "",
2592
2607
  startedAt: Date.now(),
2593
2608
  lastSeenAt: Date.now(),
@@ -2674,7 +2689,7 @@ function resumeBackgroundSubagent(session, toolUseId, pending, result) {
2674
2689
  taskId,
2675
2690
  background: true,
2676
2691
  name: known?.name || "agent",
2677
- description: "",
2692
+ description: known?.description || "",
2678
2693
  tool: "",
2679
2694
  startedAt: Date.now(),
2680
2695
  lastSeenAt: Date.now(),
@@ -4060,7 +4075,7 @@ async function transcriptPath(id) {
4060
4075
  * conversation. A resume can re-append earlier messages under their original
4061
4076
  * uuids, so each uuid's first appearance wins.
4062
4077
  */
4063
- function transcriptEntries(raw) {
4078
+ function transcriptEntries(raw, sidechain = false) {
4064
4079
  const messages = [];
4065
4080
  const seen = new Set();
4066
4081
  for (const line of raw.split("\n")) {
@@ -4070,7 +4085,7 @@ function transcriptEntries(raw) {
4070
4085
  } catch {
4071
4086
  continue;
4072
4087
  }
4073
- if (!entry || typeof entry !== "object" || entry.isSidechain) continue;
4088
+ if (!entry || typeof entry !== "object" || (!sidechain && entry.isSidechain)) continue;
4074
4089
  if (entry.type !== "user" && entry.type !== "assistant" && entry.type !== "system") continue;
4075
4090
  if (typeof entry.uuid === "string" && entry.uuid) {
4076
4091
  if (seen.has(entry.uuid)) continue;
@@ -4115,14 +4130,60 @@ function restoreArtifacts(id, messages) {
4115
4130
  });
4116
4131
  }
4117
4132
 
4118
- async function transcriptMessagesAt(path) {
4133
+ async function transcriptMessagesAt(path, sidechain = false) {
4119
4134
  let raw;
4120
4135
  try {
4121
4136
  raw = await readFile(path, "utf8");
4122
4137
  } catch {
4123
4138
  return [];
4124
4139
  }
4125
- return transcriptEntries(raw);
4140
+ return transcriptEntries(raw, sidechain);
4141
+ }
4142
+
4143
+ // 하위 에이전트 기록은 본 기록 옆 `<세션 id>/subagents/agent-<작업 id>.jsonl`에 쌓인다.
4144
+ // 행 id는 도구 호출 id라, 작업 id를 아직 모르거나 이미 끝난 행은 meta 파일의 toolUseId로 찾는다.
4145
+ async function subagentTranscriptPath(session, id) {
4146
+ const main = await transcriptPath(liveSessionId(session.id));
4147
+ if (!main) return null;
4148
+ const dir = join(main.slice(0, -".jsonl".length), "subagents");
4149
+ const taskId = findSubagent(session, id)?.taskId || (id.startsWith("task:") ? id.slice(5) : "");
4150
+ if (taskId) return join(dir, `agent-${taskId}.jsonl`);
4151
+ let names;
4152
+ try {
4153
+ names = await readdir(dir);
4154
+ } catch {
4155
+ return null;
4156
+ }
4157
+ for (const name of names) {
4158
+ if (!name.endsWith(".meta.json")) continue;
4159
+ try {
4160
+ const meta = JSON.parse(await readFile(join(dir, name), "utf8"));
4161
+ if (meta.toolUseId === id) return join(dir, name.replace(/\.meta\.json$/, ".jsonl"));
4162
+ } catch {}
4163
+ }
4164
+ return null;
4165
+ }
4166
+
4167
+ // 화면이 띄운 하위 에이전트 하나만 지켜본다. 기록 파일이 커질 때만 다시 읽어 turns를 보낸다.
4168
+ function watchSubagent(session, id) {
4169
+ clearInterval(session.subagentWatch?.timer);
4170
+ session.subagentWatch = null;
4171
+ if (!id) return;
4172
+ const watch = { id, size: -1 };
4173
+ session.subagentWatch = watch;
4174
+ watch.timer = setInterval(() => sendSubagentHistory(session, watch).catch(() => {}), 1000);
4175
+ watch.timer.unref?.();
4176
+ setImmediate(() => sendSubagentHistory(session, watch).catch(() => {}));
4177
+ }
4178
+
4179
+ async function sendSubagentHistory(session, watch) {
4180
+ if (lookupSession(session.id) !== session) return watchSubagent(session, null);
4181
+ const path = await subagentTranscriptPath(session, watch.id);
4182
+ const size = (path && (await stat(path).catch(() => null))?.size) ?? -1;
4183
+ if (session.subagentWatch !== watch || size === watch.size) return;
4184
+ watch.size = size;
4185
+ const turns = path ? historyTurns(await transcriptMessagesAt(path, true)) : [];
4186
+ notify("turn/subagent/history", { threadId: session.id, id: watch.id, turns });
4126
4187
  }
4127
4188
 
4128
4189
  /** Every message of `id`'s conversation: the transcript file when it exists,
@@ -4299,7 +4360,9 @@ async function dispatch(method, params = {}) {
4299
4360
  return {};
4300
4361
  }
4301
4362
  if (method === "marketplace/upgrade") {
4302
- const output = await runClaudeCommand(params, ["plugin", "marketplace", "update"], CLAUDE_INSTALL_TIMEOUT);
4363
+ const name = String(params.marketplaceName || "");
4364
+ const args = ["plugin", "marketplace", "update", ...(name ? [name] : [])];
4365
+ const output = await runClaudeCommand(params, args, CLAUDE_INSTALL_TIMEOUT);
4303
4366
  return { message: output, consideredRoots: ["Claude marketplaces"], errors: [] };
4304
4367
  }
4305
4368
  if (method === "skills/list") return claudeSkills(params);
@@ -4307,9 +4370,7 @@ async function dispatch(method, params = {}) {
4307
4370
  if (method === "account/read") {
4308
4371
  return await runClaudeJson(params, ["auth", "status", "--json"]) || {};
4309
4372
  }
4310
- if (method === "plugin/reload") {
4311
- return { message: "Claude 플러그인 설정을 다시 읽었습니다. 새 대화부터 적용됩니다." };
4312
- }
4373
+ if (method === "plugin/reload") return {};
4313
4374
  if (method === "permissions/status") return claudePermissionStatus(params);
4314
4375
  if (method === "permissions/update") return updateClaudePermission(params);
4315
4376
  if (method === "permissions/auto-mode") return setClaudeAutoModeDisabled(params);
@@ -4328,7 +4389,12 @@ async function dispatch(method, params = {}) {
4328
4389
  return { known: true, running: Boolean(session.turn), turnId: session.turn?.id ?? null };
4329
4390
  }
4330
4391
  if (method === "session/start") {
4331
- const { session, account, usage } = await createSession(params);
4392
+ // Claude Code가 뜨는 도중 가끔 충돌한다. 새 세션은 아직 쓴 것이 없고 다시 만들 때
4393
+ // 새 id를 받으므로 한 번 더 띄운다.
4394
+ const { session, account, usage } = await createSession(params).catch((error) => {
4395
+ if (!isClaudeCliCrash(error)) throw error;
4396
+ return createSession(params);
4397
+ });
4332
4398
  return {
4333
4399
  id: session.id,
4334
4400
  thread: { id: session.id, turns: [] },
@@ -4398,6 +4464,11 @@ async function dispatch(method, params = {}) {
4398
4464
  nextCursor: null,
4399
4465
  };
4400
4466
  }
4467
+ if (method === "session/watchSubagent") {
4468
+ const session = lookupSession(params.sessionId);
4469
+ if (session) watchSubagent(session, params.id ? String(params.id) : null);
4470
+ return {};
4471
+ }
4401
4472
  if (method === "session/history") {
4402
4473
  const id = liveSessionId(params.sessionId);
4403
4474
  const messages = await sessionMessages(id, params.cwd);
@@ -4429,15 +4500,6 @@ async function dispatch(method, params = {}) {
4429
4500
  }
4430
4501
  return {};
4431
4502
  }
4432
- // 하위 에이전트 행의 x. 행은 도구 호출 id로 알려도 stop_task는 작업 id를 받으므로,
4433
- // task_started가 아직 오지 않은 행은 멈출 수 없다. 행은 종료 통지가 거둔다.
4434
- if (method === "session/stopTask") {
4435
- const session = lookupSession(params.sessionId);
4436
- const running = session && findSubagent(session, String(params.id || ""));
4437
- if (!running?.taskId) throw new Error("아직 멈출 수 있는 작업 정보가 없습니다. 잠시 후 다시 시도하세요.");
4438
- await session.query.stopTask(running.taskId);
4439
- return {};
4440
- }
4441
4503
  if (method === "session/compact") {
4442
4504
  return startPrompt({ ...params, input: [{ type: "text", text: "/compact" }] });
4443
4505
  }
@@ -4758,6 +4820,17 @@ async function runCommandTimeoutSelfTest() {
4758
4820
  30_000,
4759
4821
  );
4760
4822
  if (quick !== "ok") throw new Error(`Claude command self-test returned ${quick}`);
4823
+ // 출력 뒤 실패로 끝난 읽기 명령은 한 번 더 실행해 그 결과를 쓴다.
4824
+ const marker = join(tmpdir(), `dvz-json-retry-${process.pid}`);
4825
+ const flaky = `const fs = require("fs"); const first = !fs.existsSync(${JSON.stringify(marker)});`
4826
+ + ` fs.writeFileSync(${JSON.stringify(marker)}, ""); process.stdout.write(first ? "[0]" : "[1]");`
4827
+ + " process.exitCode = first ? 1 : 0;";
4828
+ try {
4829
+ const retried = await runClaudeJson({ claudePath: process.execPath }, ["-e", flaky]);
4830
+ if (JSON.stringify(retried) !== "[1]") throw new Error(`Claude read retry self-test returned ${JSON.stringify(retried)}`);
4831
+ } finally {
4832
+ await rm(marker, { force: true });
4833
+ }
4761
4834
  }
4762
4835
 
4763
4836
  // 사유가 늘어나면 안내 없이 영어 원문만 남으므로, 설치된 SDK의 목록과 대조한다.
@@ -4841,6 +4914,33 @@ async function runSelfTest() {
4841
4914
  || JSON.stringify(restoredPrompts) !== JSON.stringify(["compact 이전 질문", "compact 이후 질문"])) {
4842
4915
  throw new Error(`Claude compacted transcript self-test failed: ${JSON.stringify({ restored: restored.length, restoredPrompts })}`);
4843
4916
  }
4917
+ // 하위 에이전트 기록 파일은 모든 줄이 sidechain이라, 그 파일을 읽을 때만 남긴다.
4918
+ const subagentPrompt = historyTurns(transcriptEntries(compactedTranscript, true))
4919
+ .flatMap((turn) => turn.items)
4920
+ .some((item) => item.type === "userMessage" && item.content?.[0]?.text === "subagent");
4921
+ if (!subagentPrompt) throw new Error("Claude subagent transcript self-test failed");
4922
+ // 끝나서 작업 id를 모르는 행도 meta 파일의 toolUseId로 그 기록을 찾는다.
4923
+ const previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
4924
+ const configDir = join(tmpdir(), `devez-subagent-self-test-${randomUUID()}`);
4925
+ const agentDir = join(configDir, "projects", "D--work", "subagent-session", "subagents");
4926
+ await mkdir(agentDir, { recursive: true });
4927
+ await writeFile(join(configDir, "projects", "D--work", "subagent-session.jsonl"), "");
4928
+ await writeFile(join(agentDir, "agent-a1.meta.json"), JSON.stringify({ toolUseId: "toolu_child" }));
4929
+ await writeFile(join(agentDir, "agent-a1.jsonl"), JSON.stringify({
4930
+ type: "user", uuid: "child-1", isSidechain: true, message: { role: "user", content: "child prompt" },
4931
+ }));
4932
+ process.env.CLAUDE_CONFIG_DIR = configDir;
4933
+ try {
4934
+ const childPath = await subagentTranscriptPath({ id: "subagent-session", subagents: new Map() }, "toolu_child");
4935
+ const childTurns = childPath ? historyTurns(await transcriptMessagesAt(childPath, true)) : [];
4936
+ if (!childTurns.some((turn) => turn.items.some((item) => item.content?.[0]?.text === "child prompt"))) {
4937
+ throw new Error(`Claude subagent transcript lookup self-test failed: ${childPath}`);
4938
+ }
4939
+ } finally {
4940
+ if (previousConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
4941
+ else process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
4942
+ await rm(configDir, { recursive: true, force: true });
4943
+ }
4844
4944
  // A resumed transcript lists its Artifact pages once each at their latest
4845
4945
  // publish, read through the file's toolUseResult spelling.
4846
4946
  const artifactTranscript = [
@@ -4885,6 +4985,12 @@ async function runSelfTest() {
4885
4985
  || !isNewerVersion("2.2.0", "2.1.999")) {
4886
4986
  throw new Error("Claude installed CLI preference self-test failed");
4887
4987
  }
4988
+ // 충돌 종료만 다시 띄운다. 일반 종료 코드와 다른 실패는 그대로 알린다.
4989
+ if (!isClaudeCliCrash(new Error("Claude Code process exited with code 3221226356"))
4990
+ || isClaudeCliCrash(new Error("Claude Code process exited with code 1"))
4991
+ || isClaudeCliCrash(new Error("Claude 세션을 찾을 수 없습니다: abc"))) {
4992
+ throw new Error("Claude CLI crash retry self-test failed");
4993
+ }
4888
4994
  const latestOptions = makeOptions({ cwd: process.cwd(), permissionMode: "plan" }, "00000000-0000-4000-8000-000000000000");
4889
4995
  if (latestOptions.permissionMode !== BOOTSTRAP_PERMISSION_MODE
4890
4996
  || latestOptions.perTaskStopAffordance !== true
@@ -5527,24 +5633,6 @@ async function runSelfTest() {
5527
5633
  await dispatch("session/interrupt", { sessionId: limitSession.id });
5528
5634
  resumeAfterUsageLimit(limitSession, cancelledWait, cancelledWait.resetsAt);
5529
5635
  if (limitSession.turn || retryInputs.length !== 1) throw new Error("Cancelled usage limit resumed");
5530
- // 행의 x는 행 id가 아니라 SDK 작업 id로 멈춘다. 작업 id를 아직 모르는 행은 거절한다.
5531
- const stoppedTasks = [];
5532
- const stopSession = {
5533
- id: "stop-task-self-test",
5534
- query: { async stopTask(taskId) { stoppedTasks.push(taskId); } },
5535
- subagents: new Map([
5536
- ["toolu_stop", { id: "toolu_stop", toolUseId: "toolu_stop", taskId: "agent-stop" }],
5537
- ["toolu_pending", { id: "toolu_pending", toolUseId: "toolu_pending", taskId: "" }],
5538
- ]),
5539
- };
5540
- sessions.set(stopSession.id, stopSession);
5541
- await dispatch("session/stopTask", { sessionId: stopSession.id, id: "toolu_stop" });
5542
- const pendingStop = await dispatch("session/stopTask", { sessionId: stopSession.id, id: "toolu_pending" })
5543
- .then(() => null, (error) => error);
5544
- sessions.delete(stopSession.id);
5545
- if (stoppedTasks.join() !== "agent-stop" || !pendingStop) {
5546
- throw new Error(`Claude per-task stop self-test failed: ${stoppedTasks}`);
5547
- }
5548
5636
  // Ultracode rides with each prompt's effort; a steer that becomes a turn keeps it.
5549
5637
  const flagCalls = [];
5550
5638
  const ultracodeSession = {
@@ -6245,6 +6333,18 @@ async function runSelfTest() {
6245
6333
  if (commandSession.subagents.size !== 0) {
6246
6334
  throw new Error(`Claude foreground row outlived its tool result: ${JSON.stringify([...commandSession.subagents])}`);
6247
6335
  }
6336
+ // 행은 순정처럼 name(없으면 종류)과 description을 함께 싣는다.
6337
+ for (const [input, name] of [
6338
+ [{ name: "auth_review", description: "인증 흐름 조사", subagent_type: "Explore" }, "auth_review"],
6339
+ [{ description: "인증 흐름 조사", subagent_type: "Explore" }, "Explore"],
6340
+ ]) {
6341
+ startSubagent(commandSession, { id: "toolu_named", input });
6342
+ const row = findSubagent(commandSession, "toolu_named");
6343
+ if (row?.name !== name || row.description !== "인증 흐름 조사") {
6344
+ throw new Error(`Claude subagent row name self-test failed: ${JSON.stringify(row)}`);
6345
+ }
6346
+ commandSession.subagents.delete("toolu_named");
6347
+ }
6248
6348
  // 클라우드로 띄운 원격 Agent도 접수 결과에서 줄이 남고, 종료 통지가 거둔다.
6249
6349
  commandSession.tools.set("toolu_remote", { name: "Agent", input: { name: "cloud_review" }, item: { id: "toolu_remote" } });
6250
6350
  startSubagent(commandSession, { id: "toolu_remote", input: { name: "cloud_review" } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devez-vibe",
3
- "version": "1.9.53",
3
+ "version": "1.9.54",
4
4
  "type": "module",
5
5
  "description": "Stable terminal UI for Codex and Claude Agent SDK",
6
6
  "keywords": [