@tyhld/conductor 0.12.95 → 0.12.96

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.
Files changed (2) hide show
  1. package/dist/relay.js +50 -2
  2. package/package.json +1 -1
package/dist/relay.js CHANGED
@@ -532,6 +532,31 @@ export function markDelivered(state, id) {
532
532
  // 純粋関数(外部I/Oなし=単体テスト/ドライランしやすい)
533
533
  // ───────────────────────────────────────────────────────────────────────────
534
534
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
535
+ // ── 行ごとの記録(jsonl)の読み方 ───────────────────────────────────────────
536
+ //
537
+ // 【なぜ要るか(実測 2026-10-04・0.12.95 の公開の流れ)】
538
+ // 行ごとの記録は「書き手が1行を足している最中」に読めてしまう。1行が数千文字あると、
539
+ // 書き込みが途中まで見える瞬間があり、その切れた行を JSON として読むと必ず失敗する
540
+ // (SyntaxError: Unterminated string in JSON)。
541
+ // 【考え方(これ1つで通す)】★読むのは【最後の改行まで】。
542
+ // 改行で終わっていない末尾の行は「まだ書きかけ」として扱い、次に読むときに拾う。
543
+ // =落ちない・取りこぼさない。見張りの流し読み(view-stream の readNew)と同じ考え。
544
+ // 【どこに効かせるか】★読んだあとに【消す】所(Codex の受け皿)だけは、これが無いと
545
+ // 書きかけの1行を捨てたうえにファイルを消すので、その完了が永久に失われる。
546
+ // 一方、行ごとに try/catch で飛ばして次の回に読み直す所(受領確認・受付の確かめ)は
547
+ // それ自体で落ちないので変えない(最後の1行に改行が無い記録を読み落とさないため)。
548
+ /**
549
+ * 書き終わった行(改行で終わっている行)だけを古い順に返す純粋関数。
550
+ * 末尾の書きかけ(改行で終わっていない残り)は返さない(=次に読むときに拾う)。
551
+ */
552
+ export function completeJsonlLines(text) {
553
+ const end = text.lastIndexOf('\n');
554
+ return end < 0 ? [] : text.slice(0, end).split('\n');
555
+ }
556
+ /** 末尾に書きかけの行(改行で終わっていない残り)が在るか。純粋関数。 */
557
+ export function hasPartialJsonlTail(text) {
558
+ return text.length > 0 && !text.endsWith('\n');
559
+ }
535
560
  /** 末尾の空行を除いた「末尾 RELAY_TAIL_LINES 行」を文字列で返す。静止比較の単位。 */
536
561
  export function tailOf(text) {
537
562
  const lines = text.replace(/\r/g, '').split('\n');
@@ -1021,10 +1046,18 @@ export function codexNotifyDir(env = process.env) {
1021
1046
  const home = (env.HOME ?? '').trim();
1022
1047
  return home ? path.join(home, '.tyhld', 'codex-notify') : '';
1023
1048
  }
1049
+ /**
1050
+ * 書きかけの行が末尾に在るとき、書き終わるのを待つ長さ。これを過ぎても改行が来なければ
1051
+ * 書き手が落ちた残骸とみなし、今までどおり捨てて進む(=受け皿が詰まらない)。
1052
+ */
1053
+ const CODEX_NOTICE_PARTIAL_WAIT_MS = 5_000;
1024
1054
  /**
1025
1055
  * 受け皿から「まだ渡していない完了」を1件取り出し、ファイルを空にする(=二重に使わない)。
1026
- * 返り: 最新の1件 / null(無い・読めない)。
1056
+ * 返り: 最新の1件 / null(無い・読めない・まだ書きかけ)。
1027
1057
  * ★読めない・壊れている行は捨てる。relay を止めない(フックと同じ作法)。
1058
+ * ★ただし【書きかけの行】は捨てない。捨てて消すと、書き終わったその1行=完了の知らせが
1059
+ * 永久に失われる(受け口は1件を1行で書き、書き込みは途中まで見えることがある)。
1060
+ * 書きかけが在る間は何も取らずに次の回へ回す(ファイルも消さない)。
1028
1061
  */
1029
1062
  export function takeCodexNotice(site, env = process.env) {
1030
1063
  const dir = codexNotifyDir(env);
@@ -1038,7 +1071,18 @@ export function takeCodexNotice(site, env = process.env) {
1038
1071
  catch {
1039
1072
  return null; // まだ1件も来ていない
1040
1073
  }
1041
- const lines = raw.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
1074
+ if (hasPartialJsonlTail(raw)) {
1075
+ let writing = false;
1076
+ try {
1077
+ writing = Date.now() - statSync(file).mtimeMs < CODEX_NOTICE_PARTIAL_WAIT_MS;
1078
+ }
1079
+ catch {
1080
+ writing = false; // 時刻が読めなければ、今までどおり捨てて進む
1081
+ }
1082
+ if (writing)
1083
+ return null; // 書き終わってから、次の回で拾う
1084
+ }
1085
+ const lines = completeJsonlLines(raw).map((l) => l.trim()).filter((l) => l.length > 0);
1042
1086
  let latest = null;
1043
1087
  for (const line of lines) {
1044
1088
  try {
@@ -2606,6 +2650,8 @@ export function hasSubmittedRecord(cwd, commandId, sinceMs, env = process.env) {
2606
2650
  const text = readFileSync(file, 'utf8');
2607
2651
  if (!text.includes(needle))
2608
2652
  continue;
2653
+ // ★書きかけの行(改行が来ていない末尾)は JSON として読めないので下の catch で飛ばす=
2654
+ // relay は落ちず、次の回に書き終わってから拾う(★最後の1行に改行が無い記録も読み落とさない)。
2609
2655
  for (const line of text.split('\n')) {
2610
2656
  if (!line.includes(needle))
2611
2657
  continue;
@@ -2693,6 +2739,8 @@ export function hasJobStampSince(cwd, commandId, sinceMs, env = process.env) {
2693
2739
  const text = readFileSync(file, 'utf8');
2694
2740
  if (!text.includes(needle))
2695
2741
  continue;
2742
+ // ★書きかけの行(改行が来ていない末尾)は JSON として読めないので下の catch で飛ばす=
2743
+ // relay は落ちず、次の回に書き終わってから拾う(★最後の1行に改行が無い記録も読み落とさない)。
2696
2744
  for (const line of text.split('\n')) {
2697
2745
  if (!hasExactJobStamp(line, commandId))
2698
2746
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyhld/conductor",
3
- "version": "0.12.95",
3
+ "version": "0.12.96",
4
4
  "description": "采配くん管制の見守りアプリ(conductor-agent)。各PCで常駐し、中央(devlog-tracker)へ定期的に生存報告(heartbeat)を送る常駐CLI。",
5
5
  "type": "module",
6
6
  "bin": {