tailii-host 0.13.0 → 0.15.0

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 (83) hide show
  1. package/dist/backend/tmux.d.ts +40 -0
  2. package/dist/backend/tmux.js +147 -7
  3. package/dist/backend/tmux.js.map +1 -1
  4. package/dist/chat/chatTailController.js +2 -1
  5. package/dist/chat/chatTailController.js.map +1 -1
  6. package/dist/chat/subagentTailer.js +20 -5
  7. package/dist/chat/subagentTailer.js.map +1 -1
  8. package/dist/chat/subagentTranscript.js +29 -0
  9. package/dist/chat/subagentTranscript.js.map +1 -1
  10. package/dist/chat/transcriptTailer.d.ts +13 -1
  11. package/dist/chat/transcriptTailer.js +113 -2
  12. package/dist/chat/transcriptTailer.js.map +1 -1
  13. package/dist/codex/codexAppServer.d.ts +9 -2
  14. package/dist/codex/codexAppServer.js +34 -5
  15. package/dist/codex/codexAppServer.js.map +1 -1
  16. package/dist/codex/codexAsyncQuestion.d.ts +53 -0
  17. package/dist/codex/codexAsyncQuestion.js +186 -0
  18. package/dist/codex/codexAsyncQuestion.js.map +1 -0
  19. package/dist/codex/codexMessagePhase.d.ts +16 -0
  20. package/dist/codex/codexMessagePhase.js +22 -0
  21. package/dist/codex/codexMessagePhase.js.map +1 -0
  22. package/dist/codex/codexNativeTurnController.d.ts +62 -0
  23. package/dist/codex/codexNativeTurnController.js +383 -8
  24. package/dist/codex/codexNativeTurnController.js.map +1 -1
  25. package/dist/codex/codexRolloutTailer.js +31 -5
  26. package/dist/codex/codexRolloutTailer.js.map +1 -1
  27. package/dist/codex/codexSessionStore.d.ts +16 -1
  28. package/dist/codex/codexSessionStore.js +90 -19
  29. package/dist/codex/codexSessionStore.js.map +1 -1
  30. package/dist/codex/codexSubagentTracker.js +16 -0
  31. package/dist/codex/codexSubagentTracker.js.map +1 -1
  32. package/dist/codex/codexSystemNotice.d.ts +7 -0
  33. package/dist/codex/codexSystemNotice.js +13 -0
  34. package/dist/codex/codexSystemNotice.js.map +1 -1
  35. package/dist/engine/engine.js +17 -3
  36. package/dist/engine/engine.js.map +1 -1
  37. package/dist/engine/handlers/conversation.js +32 -19
  38. package/dist/engine/handlers/conversation.js.map +1 -1
  39. package/dist/engine/handlers/mode.js +93 -3
  40. package/dist/engine/handlers/mode.js.map +1 -1
  41. package/dist/engine/handlers/session.js +3 -1
  42. package/dist/engine/handlers/session.js.map +1 -1
  43. package/dist/hub/hubDaemon.js +6 -2
  44. package/dist/hub/hubDaemon.js.map +1 -1
  45. package/dist/hub/hubProtocol.d.ts +7 -2
  46. package/dist/hub/hubProtocol.js +5 -2
  47. package/dist/hub/hubProtocol.js.map +1 -1
  48. package/dist/hub/panePreviewPump.d.ts +23 -0
  49. package/dist/hub/panePreviewPump.js +55 -1
  50. package/dist/hub/panePreviewPump.js.map +1 -1
  51. package/dist/hub/questionInjection.d.ts +7 -0
  52. package/dist/hub/questionInjection.js +18 -0
  53. package/dist/hub/questionInjection.js.map +1 -1
  54. package/dist/hub/sessionHub.d.ts +21 -1
  55. package/dist/hub/sessionHub.js +108 -4
  56. package/dist/hub/sessionHub.js.map +1 -1
  57. package/dist/protocol/decode.js +19 -1
  58. package/dist/protocol/decode.js.map +1 -1
  59. package/dist/protocol/encode.js +4 -0
  60. package/dist/protocol/encode.js.map +1 -1
  61. package/dist/protocol/messages.d.ts +50 -0
  62. package/dist/protocol/messages.js +17 -0
  63. package/dist/protocol/messages.js.map +1 -1
  64. package/dist/sessions/claudeSessionStore.d.ts +13 -1
  65. package/dist/sessions/claudeSessionStore.js +72 -7
  66. package/dist/sessions/claudeSessionStore.js.map +1 -1
  67. package/dist/sessions/listMemoFile.d.ts +24 -0
  68. package/dist/sessions/listMemoFile.js +133 -0
  69. package/dist/sessions/listMemoFile.js.map +1 -0
  70. package/dist/sessions/sessionSearch.js +5 -0
  71. package/dist/sessions/sessionSearch.js.map +1 -1
  72. package/dist/shared/clientBuild.d.ts +1 -1
  73. package/dist/shared/clientBuild.js +1 -1
  74. package/dist/shared/commandExpansion.d.ts +13 -0
  75. package/dist/shared/commandExpansion.js +31 -0
  76. package/dist/shared/commandExpansion.js.map +1 -0
  77. package/dist/shared/readFailures.d.ts +4 -0
  78. package/dist/shared/readFailures.js +20 -0
  79. package/dist/shared/readFailures.js.map +1 -0
  80. package/dist/shared/usageLimitWait.d.ts +28 -1
  81. package/dist/shared/usageLimitWait.js +197 -16
  82. package/dist/shared/usageLimitWait.js.map +1 -1
  83. package/package.json +3 -3
@@ -55,7 +55,9 @@ export type HubPreviewPumpFactory = (write: (payload: ControlMessage) => void, o
55
55
  /** 毎周期評価するポーリング間隔(ms)。前面購読あり=高頻度 / 一覧 watch のみ=低頻度。 */
56
56
  pollIntervalMs?: () => number,
57
57
  /** 使用量制限の自動再開待ち(pane フッター)の遷移通知(usage-limit-wait)。 */
58
- onUsageLimitWait?: (state: UsageLimitWaitState | null) => void) => HubPreviewPump;
58
+ onUsageLimitWait?: (state: UsageLimitWaitState | null) => void,
59
+ /** 会話が待機中(処理中でない)か。入力欄に残った文字の通知(input-residue)の条件。 */
60
+ isIdle?: () => boolean) => HubPreviewPump;
59
61
  export type SessionHubOptions = Omit<ReaperTickOptions, "now"> & {
60
62
  /** Unix 秒。テストでは固定時計を注入する。 */
61
63
  now?: () => number;
@@ -75,6 +77,14 @@ export type SessionHubOptions = Omit<ReaperTickOptions, "now"> & {
75
77
  previewPumpFactory?: HubPreviewPumpFactory;
76
78
  replayLimit?: number;
77
79
  questionInjector?: (answers: QuestionAnswer[], session: string) => Promise<void>;
80
+ /**
81
+ * TUI に AskUserQuestion の設問ダイアログが出ているか(回答キーを打つ前の確認, question-stale)。
82
+ * false = 出ていない(キーを打たず `stale` で返す)。null = 判定できない(従来どおり打つ)。
83
+ * 省略時は確認しない。
84
+ */
85
+ questionDialogVisible?: (session: string) => Promise<boolean | null>;
86
+ /** `stale` の後、設問ダイアログを確かめ直す最初の待ち(ms, 既定 1500。以後 2 倍・4 倍で計 3 回。テスト注入用)。 */
87
+ staleQuestionRecheckMs?: number;
78
88
  /**
79
89
  * 使用量制限の自動再開待ちの遷移を利用者へ知らせる(APNs push, usage-limit-wait)。制限到達
80
90
  * (`api_error` の `rate_limit`)後は購読者が居なくても pane を低頻度で見張り、待機表示の出現・
@@ -292,6 +302,8 @@ export declare class SessionHub {
292
302
  private readonly clients;
293
303
  /** 一覧 Mission Control の watcher(処理中会話全体の pane_preview 配信先)。 */
294
304
  private readonly previewWatchers;
305
+ /** `stale` で捨てた設問の再確認タイマー(会話ごとに 1 本, question-stale)。 */
306
+ private readonly staleQuestionRechecks;
295
307
  readonly actors: Map<string, SessionActor>;
296
308
  private readonly now;
297
309
  private readonly nowMs;
@@ -456,6 +468,14 @@ export declare class SessionHub {
456
468
  */
457
469
  private applyProcessing;
458
470
  private bumpSafe;
471
+ /**
472
+ * `stale` で捨てた設問を、少し後に確かめ直す(question-stale)。設問の通知は PreToolUse hook が allow を
473
+ * 返す前に届くので、利用者の他の遅い hook があるとダイアログはまだ描かれていないことがある。その窓で
474
+ * 答えられた設問は、ダイアログが出てきたら戻して再提示する。間隔を広げて数回まで確かめ、別の設問が来た・
475
+ * CLI 側で片付いた(PostToolUse の dismiss)・会話が入れ替わった・hub を閉じたら止める。
476
+ */
477
+ private recheckStaleQuestion;
478
+ private cancelStaleQuestionRecheck;
459
479
  private setPendingQuestion;
460
480
  private persistPendingQuestions;
461
481
  private readPendingQuestions;
@@ -16,6 +16,7 @@ import { CodexAppServerManager } from "../codex/codexAppServer.js";
16
16
  import { CodexNativeTurnController, chatContentKey, } from "../codex/codexNativeTurnController.js";
17
17
  import { COLLABORATION_MODE_STREAM_ID as CODEX_COLLABORATION_MODE_STREAM_ID, CONTEXT_STREAM_ID as CODEX_CONTEXT_STREAM_ID, CONTEXT_WINDOW_STREAM_ID as CODEX_CONTEXT_WINDOW_STREAM_ID, MODEL_STREAM_ID as CODEX_MODEL_STREAM_ID, } from "../codex/codexRolloutTailer.js";
18
18
  import { claudeTranscriptPathFor } from "../sessions/transcriptPath.js";
19
+ import { isNonBlockingQuestionId } from "../codex/codexAsyncQuestion.js";
19
20
  /**
20
21
  * backfilling 中の購読者は publishConversationEvent が丸ごと飛ばす。旗が立ちっぱなしになると
21
22
  * 「接続は健全なのに会話だけ永久に更新されない」実障害になるため、履歴が一定時間 1 行も進まな
@@ -54,11 +55,20 @@ function isContinuationHookEvent(event) {
54
55
  function defaultTranscriptPathFor(meta) {
55
56
  return claudeTranscriptPathFor(meta);
56
57
  }
58
+ /** 設問の回答時に TUI へ設問ダイアログが出ていなかった(question-stale)。キーは 1 つも打っていない。 */
59
+ class StaleQuestionError extends Error {
60
+ constructor() {
61
+ super("AskUserQuestion dialog is not on screen");
62
+ this.name = "StaleQuestionError";
63
+ }
64
+ }
57
65
  export class SessionHub {
58
66
  options;
59
67
  clients = new Map();
60
68
  /** 一覧 Mission Control の watcher(処理中会話全体の pane_preview 配信先)。 */
61
69
  previewWatchers = new Set();
70
+ /** `stale` で捨てた設問の再確認タイマー(会話ごとに 1 本, question-stale)。 */
71
+ staleQuestionRechecks = new Map();
62
72
  actors = new Map();
63
73
  now;
64
74
  nowMs;
@@ -310,6 +320,10 @@ export class SessionHub {
310
320
  }
311
321
  handleRelayMessage(message) {
312
322
  if (message.type === "question_event") {
323
+ // 新しい設問が来た / CLI 側でその設問が片付いた(PostToolUse)なら、捨てた設問の再確認は要らない。
324
+ if (message.event === "prompt" || this.staleQuestionRechecks.get(message.session)?.id === message.id) {
325
+ this.cancelStaleQuestionRecheck(message.session);
326
+ }
313
327
  const actor = this.actor(message.session);
314
328
  this.setPendingQuestion(message.session, actor, message.event === "prompt"
315
329
  ? { id: message.id, questions: message.questions ?? [], answerRoute: "tui" } : null);
@@ -423,8 +437,8 @@ export class SessionHub {
423
437
  const restoreSnapshot = actor.pendingQuestion;
424
438
  this.setPendingQuestion(message.session, actor, null);
425
439
  this.broadcast({ type: "question_event", session: message.session, event: "dismiss", id: message.questionId });
426
- this.sendTo(client, { type: "question_answer_result", id: message.id, status: "accepted" });
427
440
  if (answerRoute === "codex_native") {
441
+ this.sendTo(client, { type: "question_answer_result", id: message.id, status: "accepted" });
428
442
  try {
429
443
  if (this.codexTurnController?.answerQuestion?.(message.questionId, message.answers) !== true) {
430
444
  this.options.log?.(`Codex native 設問回答失敗: pending 不在 (${message.questionId})`);
@@ -441,15 +455,39 @@ export class SessionHub {
441
455
  actor.questionAnswerInjections += 1;
442
456
  // 同期 throw でも `.finally` を必ず通す(通らないと回答注入中の件数が戻らず、chat が
443
457
  // 永久に保留される)。
458
+ // 回答キーは TUI に設問ダイアログが出ているときだけ打つ(question-stale)。出ていなければ
459
+ // 数字キーは入力欄へ落ち、Enter の無い単一選択では「1」が入ったまま会話が止まる
460
+ // (実機 2026-10-05: 設問カードに答えたら CLI の入力欄に 1 が残り、アプリは処理中のまま)。
461
+ // 判定できない(pane を読めない)ときは従来どおり打つ。確認しない構成では従来どおり即受理。
462
+ const dialogVisible = this.options.questionDialogVisible;
463
+ const accept = () => {
464
+ this.sendTo(client, { type: "question_answer_result", id: message.id, status: "accepted" });
465
+ };
466
+ if (dialogVisible === undefined)
467
+ accept();
444
468
  let injection;
445
469
  try {
446
- injection = this.options.questionInjector?.(message.answers, message.session) ?? Promise.resolve();
470
+ injection = (async () => {
471
+ if (dialogVisible !== undefined) {
472
+ if (await dialogVisible(message.session) === false)
473
+ throw new StaleQuestionError();
474
+ accept();
475
+ }
476
+ await this.options.questionInjector?.(message.answers, message.session);
477
+ })();
447
478
  }
448
479
  catch (error) {
449
480
  injection = Promise.reject(error);
450
481
  }
451
482
  void injection
452
483
  .catch((error) => {
484
+ if (error instanceof StaleQuestionError) {
485
+ // 設問はもう TUI に無い(回答済み・取り消し済み・表示されなかった)。復元も再提示もしない。
486
+ this.sendTo(client, { type: "question_answer_result", id: message.id, status: "stale" });
487
+ this.options.log?.(`audit question-stale session=${auditValue(message.session)} question=${auditValue(message.questionId)}`);
488
+ this.recheckStaleQuestion(message.session, actor, restoreSnapshot);
489
+ return;
490
+ }
453
491
  this.options.log?.(`設問回答注入失敗: ${String(error)}`);
454
492
  // 自己修復(question-answer-retry): pendingQuestion を失ったまま TUI ダイアログが
455
493
  // 残ると、アプリから再回答する手段が無く会話が詰む。失敗の度に必ず復元し、
@@ -794,6 +832,9 @@ export class SessionHub {
794
832
  return result;
795
833
  }
796
834
  close() {
835
+ for (const recheck of this.staleQuestionRechecks.values())
836
+ clearTimeout(recheck.timer);
837
+ this.staleQuestionRechecks.clear();
797
838
  for (const actor of this.actors.values()) {
798
839
  if (actor.tailRetryTimer !== null)
799
840
  clearTimeout(actor.tailRetryTimer);
@@ -1699,7 +1740,11 @@ export class SessionHub {
1699
1740
  if (actor.usageLimitWatch !== null)
1700
1741
  return 5000;
1701
1742
  return 1000;
1702
- }, (state) => this.handleUsageLimitWait(session, actor, state));
1743
+ }, (state) => this.handleUsageLimitWait(session, actor, state),
1744
+ // 入力欄に残った文字の通知(input-residue)は処理中でない間だけ。hub 自身が発話・設問の回答・
1745
+ // 今すぐ送信を打っている最中の入力欄も、残り文字とみなさない。
1746
+ () => actor.processingSince === null && !actor.chatDrainRunning && actor.questionAnswerInjections === 0 &&
1747
+ !actor.sendNowRunning);
1703
1748
  actor.previewPump = pump;
1704
1749
  pump.start(session, this.options.metadataStore.get(session)?.agent === "codex" ? "codex_terminal" : "claude_status",
1705
1750
  // 処理中の pump 起動(=一覧 watch 起因が典型)は初回フレームから送る。静止 pane
@@ -2530,7 +2575,9 @@ export class SessionHub {
2530
2575
  catch (error) {
2531
2576
  this.options.log?.(`heartbeat 書込失敗: ${String(error)}`);
2532
2577
  }
2533
- if (state === "done" && actor.pendingQuestion !== null) {
2578
+ // turn を止めない設問(Codex の非同期質問)は turn controller が turn の終わりに締め切る。処理中表示の
2579
+ // done(steer の不確定失敗などでも来る)で消すと、controller は提示済みのままで出し直さない。
2580
+ if (state === "done" && actor.pendingQuestion !== null && !isNonBlockingQuestionId(actor.pendingQuestion.id)) {
2534
2581
  const id = actor.pendingQuestion.id;
2535
2582
  this.setPendingQuestion(session, actor, null);
2536
2583
  this.broadcast({ type: "question_event", session, event: "dismiss", id });
@@ -2546,6 +2593,63 @@ export class SessionHub {
2546
2593
  this.options.log?.(`heartbeat 書込失敗: ${String(error)}`);
2547
2594
  }
2548
2595
  }
2596
+ /**
2597
+ * `stale` で捨てた設問を、少し後に確かめ直す(question-stale)。設問の通知は PreToolUse hook が allow を
2598
+ * 返す前に届くので、利用者の他の遅い hook があるとダイアログはまだ描かれていないことがある。その窓で
2599
+ * 答えられた設問は、ダイアログが出てきたら戻して再提示する。間隔を広げて数回まで確かめ、別の設問が来た・
2600
+ * CLI 側で片付いた(PostToolUse の dismiss)・会話が入れ替わった・hub を閉じたら止める。
2601
+ */
2602
+ recheckStaleQuestion(session, actor, snapshot) {
2603
+ const dialogVisible = this.options.questionDialogVisible;
2604
+ if (dialogVisible === undefined)
2605
+ return;
2606
+ this.cancelStaleQuestionRecheck(session);
2607
+ const delays = this.options.staleQuestionRecheckMs !== undefined
2608
+ ? [this.options.staleQuestionRecheckMs, this.options.staleQuestionRecheckMs * 2, this.options.staleQuestionRecheckMs * 4]
2609
+ : [1_500, 3_000, 6_000];
2610
+ const schedule = (attempt) => {
2611
+ const delay = delays[attempt];
2612
+ if (delay === undefined) {
2613
+ this.cancelStaleQuestionRecheck(session);
2614
+ return;
2615
+ }
2616
+ const timer = setTimeout(() => {
2617
+ void (async () => {
2618
+ const mine = () => this.staleQuestionRechecks.get(session)?.timer === timer;
2619
+ const alive = () => mine() && this.actors.get(session) === actor && actor.pendingQuestion === null;
2620
+ const settle = () => { if (mine())
2621
+ this.staleQuestionRechecks.delete(session); };
2622
+ if (!alive()) {
2623
+ settle();
2624
+ return;
2625
+ }
2626
+ const visible = await dialogVisible(session);
2627
+ if (!alive()) {
2628
+ settle();
2629
+ return;
2630
+ }
2631
+ if (visible !== true) {
2632
+ schedule(attempt + 1);
2633
+ return;
2634
+ }
2635
+ this.cancelStaleQuestionRecheck(session);
2636
+ this.setPendingQuestion(session, actor, snapshot);
2637
+ this.broadcast({ type: "question_event", session, event: "prompt", id: snapshot.id, questions: snapshot.questions });
2638
+ this.options.log?.(`audit question-stale-restored session=${auditValue(session)} question=${auditValue(snapshot.id)}`);
2639
+ })().catch(() => { });
2640
+ }, delay);
2641
+ timer.unref?.();
2642
+ this.staleQuestionRechecks.set(session, { id: snapshot.id, timer });
2643
+ };
2644
+ schedule(0);
2645
+ }
2646
+ cancelStaleQuestionRecheck(session) {
2647
+ const recheck = this.staleQuestionRechecks.get(session);
2648
+ if (recheck === undefined)
2649
+ return;
2650
+ clearTimeout(recheck.timer);
2651
+ this.staleQuestionRechecks.delete(session);
2652
+ }
2549
2653
  setPendingQuestion(session, actor, pending) {
2550
2654
  actor.pendingQuestion = pending;
2551
2655
  this.persistPendingQuestions();