viber-channel 0.8.12 → 0.8.13

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.
@@ -291,6 +291,49 @@ export interface ExecDeps {
291
291
  writeSpec?: SpecFileWriter;
292
292
  /** Injectable local-log writer (tests). Returns the log path. */
293
293
  writeLog?: (commandId: string, recap: string) => string;
294
+ /** Injectable daemon-stderr writer (tests). Defaults to process.stderr. */
295
+ writeDaemonLine?: (line: string) => void;
296
+ }
297
+
298
+ /** Where the log lives, as shown to whoever reads the WEB (#506): RELATIVE, so
299
+ * it names the file without carrying the machine's absolute path — home
300
+ * directory and user name included — to the server. The absolute path stays on
301
+ * the daemon's own stderr, which never leaves the machine. */
302
+ export function runnerLogRef(commandId: string): string {
303
+ return `.viber/runner-logs/${commandId}.log`;
304
+ }
305
+
306
+ /** What `writeRunnerLog` returns when it could NOT write (unwritable dir, full
307
+ * disk). Named, because the remote message must not point at a file that was
308
+ * never created. */
309
+ export const LOG_UNAVAILABLE = "(local log unavailable)";
310
+
311
+ /** The log reference to put in the REMOTE message, or undefined when no log
312
+ * exists (#506 review, both reviewers): pointing someone at
313
+ * `.viber/runner-logs/<id>.log` when the write failed would be this issue in
314
+ * miniature — a component asserting more than it knows, on the very sentence
315
+ * this step fixes. Callers fall back to the no-path wording. */
316
+ export function remoteLogRef(commandId: string, logPath: string): string | undefined {
317
+ return logPath === LOG_UNAVAILABLE ? undefined : runnerLogRef(commandId);
318
+ }
319
+
320
+ /** The one line of a recap worth putting on the daemon's stderr: its first
321
+ * `error:` line, bounded. Already redacted upstream by `redactSecrets`. Falls
322
+ * back to the first non-empty line when the CLI printed no `error:` line, so a
323
+ * failure is never reported as silence.
324
+ *
325
+ * The result must be ONE line: it is written with a trailing newline onto a
326
+ * shared stream, so any control character surviving inside it could forge a
327
+ * second log line. Hence splitting on a LONE `\r` too (Codex review — a bare CR
328
+ * is what a progress-bar writer emits), and stripping the remaining C0/C1
329
+ * controls rather than trusting the split alone. */
330
+ export function firstErrorLine(recap: string, max = 300): string {
331
+ const lines = recap
332
+ .split(/\r\n|\r|\n/)
333
+ .map((l) => l.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").trim())
334
+ .filter(Boolean);
335
+ const line = lines.find((l) => l.startsWith("error:")) ?? lines[0] ?? "(no output)";
336
+ return line.slice(0, max);
294
337
  }
295
338
 
296
339
  /** Write the full launch recap to a 0600 LOCAL log (never sent to the server —
@@ -303,7 +346,7 @@ export function writeRunnerLog(commandId: string, recap: string, cwd: string = p
303
346
  mkdirSync(dir, { recursive: true });
304
347
  writeFileSync(path, recap, { mode: 0o600 });
305
348
  } catch {
306
- return "(local log unavailable)";
349
+ return LOG_UNAVAILABLE;
307
350
  }
308
351
  return path;
309
352
  }
@@ -447,6 +490,21 @@ export async function executeClaim(
447
490
  // marker (older vibe-master, output lost to maxBuffer) we degrade to exactly
448
491
  // the previous message rather than inventing a cause.
449
492
  const logPath = deps.writeLog ? deps.writeLog(cmd.id, recap) : writeRunnerLog(cmd.id, recap);
493
+ // #506, the issue's second defect: the remote message sent people to "the
494
+ // machine's log", and the log they actually open — the daemon's — only ever
495
+ // said "reconcile: handled 1 command(s)". A failure now states itself where
496
+ // it is read FIRST. This stream is local, so the ABSOLUTE path belongs here;
497
+ // what travels to the server is the relative ref below.
498
+ if (!ok) {
499
+ const writeDaemonLine =
500
+ deps.writeDaemonLine ?? ((line: string) => process.stderr.write(`${line}\n`));
501
+ writeDaemonLine(
502
+ `[runner] spawn FAILED command=${cmd.id} reason=${reason ?? "unknown"} log=${logPath} ${firstErrorLine(recap)}`,
503
+ );
504
+ }
505
+ // Resolved ONCE: three call sites in a ternary could drift apart under a
506
+ // later edit, and they must agree on whether a log exists (Opus review).
507
+ const logRef = remoteLogRef(cmd.id, logPath);
450
508
  const finalStatus = await report(
451
509
  auth,
452
510
  cmd.id,
@@ -456,8 +514,15 @@ export async function executeClaim(
456
514
  fencing_token: cmd.fencing_token,
457
515
  status: "failed",
458
516
  error: reason
459
- ? messageForReason(reason, detail, rosterNames(cmd.prefix, cmd.template_spec.roles))
460
- : "team spawn failed — see local runner log",
517
+ ? messageForReason(
518
+ reason,
519
+ detail,
520
+ rosterNames(cmd.prefix, cmd.template_spec.roles),
521
+ logRef,
522
+ )
523
+ : logRef
524
+ ? `team spawn failed — see ${logRef} on that machine`
525
+ : "team spawn failed, and the local log could not be written on that machine",
461
526
  ...(reason ? { reason } : {}),
462
527
  },
463
528
  fetchImpl,
@@ -22,6 +22,7 @@ export const SPAWN_REASONS = [
22
22
  "already_running",
23
23
  "partial_team",
24
24
  "template_invalid",
25
+ "roster_invalid",
25
26
  "codex_unavailable",
26
27
  "launch_failed",
27
28
  // Emitted by the runner itself
@@ -176,6 +177,7 @@ export function messageForReason(
176
177
  reason: SpawnReason,
177
178
  detail?: string,
178
179
  roster: readonly string[],
180
+ logRef?: string,
179
181
  ): string {
180
182
  const safe = keepRosterNames(detail, roster);
181
183
  const names = safe ? ` (${safe})` : "";
@@ -197,7 +199,21 @@ export function messageForReason(
197
199
  // passes --spec-file. So this means the server's own snapshot could not be
198
200
  // read back — a defect. Telling the user to go read a log they cannot open,
199
201
  // for a problem they did not cause, would repeat this issue in miniature.
200
- return "the roster sent for this spawn could not be read back on the runner machine — this is likely a defect, not something you did wrong; the full error is in the machine's log";
202
+ // #506, second defect of the issue: this sentence sent the reader to "the
203
+ // machine's log" WITHOUT naming it — and the log they did open, the
204
+ // daemon's, said nothing about the failure. `logRef` is the RELATIVE path
205
+ // (`.viber/runner-logs/<command_id>.log`): it locates the file without
206
+ // shipping an absolute Windows path — hence the machine's user name — to
207
+ // the server and onto someone's screen.
208
+ return `the roster sent for this spawn could not be read back on the runner machine — this is likely a defect, not something you did wrong; the full error is in ${logRef ?? "the machine's log"} on that machine`;
209
+ case "roster_invalid":
210
+ // #506: the OTHER half of what template_invalid used to swallow. The
211
+ // snapshot was read fine — it is the ROSTER that cannot be launched as
212
+ // written, and that IS the template author's to fix. Two catches, two
213
+ // reasons: this one says "you can fix this", template_invalid says "this
214
+ // is a defect on our side". Saying the second for the first sends someone
215
+ // to read a log for a mistake they could correct in two clicks.
216
+ return `this team template cannot be launched as written${names} — fix the template, then spawn again`;
201
217
  case "codex_unavailable":
202
218
  return "codex is not launchable on this machine, so no agent was started — install codex or set VIBER_CODEX_BIN";
203
219
  case "launch_failed":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viber-channel",
3
- "version": "0.8.12",
3
+ "version": "0.8.13",
4
4
  "description": "Voice + text MCP channel between a Claude Code session and the Viber UI (https://viber.dgypx.dev). Push transcripts to Claude; send_message tool delivers text back to the UI.",
5
5
  "type": "module",
6
6
  "bin": {