tailii-host 0.10.1 → 0.11.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.
- package/dist/approval/hook.d.ts +2 -0
- package/dist/approval/hook.js +38 -3
- package/dist/approval/hook.js.map +1 -1
- package/dist/backend/herdr.d.ts +31 -23
- package/dist/backend/herdr.js +181 -50
- package/dist/backend/herdr.js.map +1 -1
- package/dist/backend/sessionBackend.d.ts +8 -1
- package/dist/backend/sessionBackend.js +3 -0
- package/dist/backend/sessionBackend.js.map +1 -1
- package/dist/backend/tmux.d.ts +387 -2
- package/dist/backend/tmux.js +936 -55
- package/dist/backend/tmux.js.map +1 -1
- package/dist/chat/subagentTranscript.js +11 -3
- package/dist/chat/subagentTranscript.js.map +1 -1
- package/dist/chat/transcriptTailer.d.ts +3 -1
- package/dist/chat/transcriptTailer.js +65 -4
- package/dist/chat/transcriptTailer.js.map +1 -1
- package/dist/commands/launch.js +4 -2
- package/dist/commands/launch.js.map +1 -1
- package/dist/engine/engine.js +1 -0
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/handlers/conversation.js +29 -2
- package/dist/engine/handlers/conversation.js.map +1 -1
- package/dist/hub/hubDaemon.js +18 -5
- package/dist/hub/hubDaemon.js.map +1 -1
- package/dist/hub/hubProtocol.d.ts +16 -0
- package/dist/hub/hubProtocol.js +26 -2
- package/dist/hub/hubProtocol.js.map +1 -1
- package/dist/hub/reaper.d.ts +31 -0
- package/dist/hub/reaper.js +122 -4
- package/dist/hub/reaper.js.map +1 -1
- package/dist/hub/sessionHub.d.ts +76 -0
- package/dist/hub/sessionHub.js +177 -12
- package/dist/hub/sessionHub.js.map +1 -1
- package/dist/protocol/decode.js +11 -0
- package/dist/protocol/decode.js.map +1 -1
- package/dist/protocol/messages.d.ts +30 -0
- package/dist/sessions/claudeLiveStatus.d.ts +62 -0
- package/dist/sessions/claudeLiveStatus.js +202 -0
- package/dist/sessions/claudeLiveStatus.js.map +1 -0
- package/dist/sessions/claudeSessionStore.js +13 -0
- package/dist/sessions/claudeSessionStore.js.map +1 -1
- package/dist/sessions/cronSchedule.d.ts +20 -0
- package/dist/sessions/cronSchedule.js +172 -0
- package/dist/sessions/cronSchedule.js.map +1 -0
- package/dist/sessions/heartbeat.d.ts +44 -3
- package/dist/sessions/heartbeat.js +44 -4
- package/dist/sessions/heartbeat.js.map +1 -1
- package/dist/sessions/sessionSearch.js +9 -0
- package/dist/sessions/sessionSearch.js.map +1 -1
- package/dist/shared/clientBuild.d.ts +1 -1
- package/dist/shared/clientBuild.js +1 -1
- package/dist/shared/crossSession.d.ts +10 -3
- package/dist/shared/crossSession.js +105 -1
- package/dist/shared/crossSession.js.map +1 -1
- package/dist/shared/localCommandModelCopy.d.ts +2 -0
- package/dist/shared/localCommandModelCopy.js +43 -0
- package/dist/shared/localCommandModelCopy.js.map +1 -0
- package/dist/shared/narration.d.ts +7 -0
- package/dist/shared/narration.js +119 -0
- package/dist/shared/narration.js.map +1 -0
- package/dist/shared/pastedContent.d.ts +7 -0
- package/dist/shared/pastedContent.js +113 -0
- package/dist/shared/pastedContent.js.map +1 -0
- package/dist/shared/systemNotice.js +4 -1
- package/dist/shared/systemNotice.js.map +1 -1
- package/package.json +3 -3
package/dist/hub/sessionHub.js
CHANGED
|
@@ -9,7 +9,7 @@ import { decodeHubClientLine, encodeHubMessage, } from "./hubProtocol.js";
|
|
|
9
9
|
import { HISTORY_DONE_STREAM_ID, findTrailingTurnEndMarkerMs, findTrailingUserPromptText, } from "../chat/transcriptTailer.js";
|
|
10
10
|
import { sameUsageLimitWait } from "../shared/usageLimitWait.js";
|
|
11
11
|
import { PROTOCOL_V1, PROTOCOL_V2 } from "../protocol.js";
|
|
12
|
-
import { ChatInjectionRejectedError, LoginCodeError } from "../backend/tmux.js";
|
|
12
|
+
import { ChatInjectionRejectedError, LoginCodeError, SEND_NOW_BLOCKED_BY_DIALOG, SEND_NOW_EXPIRED_IN_QUEUE, } from "../backend/tmux.js";
|
|
13
13
|
import { CodexAppServerManager } from "../codex/codexAppServer.js";
|
|
14
14
|
import { CodexNativeTurnController, chatContentKey, } from "../codex/codexNativeTurnController.js";
|
|
15
15
|
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";
|
|
@@ -23,6 +23,12 @@ import { claudeTranscriptPathFor } from "../sessions/transcriptPath.js";
|
|
|
23
23
|
const BACKFILL_STALL_TIMEOUT_MS = 15_000;
|
|
24
24
|
const DELIVERED_RECEIPT_TTL_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
25
25
|
const MAX_DELIVERED_RECEIPTS_PER_SESSION = 10_000;
|
|
26
|
+
/**
|
|
27
|
+
* 「今すぐ送信」が順番(先行する chat の注入)を待てる上限(ms)。engine は hub の応答を 30 秒で
|
|
28
|
+
* 諦める。それより後に chord を送ると、アプリに失敗と出した後で実行中のターンが打ち切られる。
|
|
29
|
+
* chord の後の確認(約 6 秒)と撮り直し(約 1.5 秒)を引いた残りより短くする。
|
|
30
|
+
*/
|
|
31
|
+
export const SEND_NOW_QUEUE_WAIT_LIMIT_MS = 20_000;
|
|
26
32
|
/** 中断マーカーで done にした後、継続 hook(Pre/PostToolUse)の遅着 active を残響として無視する窓(ms)。 */
|
|
27
33
|
const LATE_HOOK_AFTER_INTERRUPT_MS = 3_000;
|
|
28
34
|
/**
|
|
@@ -376,6 +382,14 @@ export class SessionHub {
|
|
|
376
382
|
else if (actor.pendingQuestion?.id !== message.questionId) {
|
|
377
383
|
this.sendTo(client, { type: "question_answer_result", id: message.id, status: "already_resolved" });
|
|
378
384
|
}
|
|
385
|
+
else if (actor.cancellingQuestionId === message.questionId) {
|
|
386
|
+
// chat の注入がこの設問を Esc で閉じている最中(chat-cancel-choice)。いま回答キーを流すと、
|
|
387
|
+
// 閉じた後の入力欄へ落ちる。「回答済み」とは返さない: 閉じるのを諦めた場合、設問は未回答の
|
|
388
|
+
// まま残るのに、アプリは回答を捨ててしまう(設問シートも再表示ピルも無く、答える手段が消える)。
|
|
389
|
+
// 再試行できる失敗で返す(`unknown` → engine が `question_answer_failed` → アプリが 1s / 3s で
|
|
390
|
+
// 自動再送)。再送が届く頃には決着している: 閉じていれば `already_resolved`、諦めていれば受理。
|
|
391
|
+
this.sendTo(client, { type: "question_answer_result", id: message.id, status: "unknown" });
|
|
392
|
+
}
|
|
379
393
|
else {
|
|
380
394
|
// first-wins の判定と clear は await を挟まず同期的に行い、勝者だけが注入する。
|
|
381
395
|
const answerRoute = actor.pendingQuestion.answerRoute;
|
|
@@ -397,7 +411,18 @@ export class SessionHub {
|
|
|
397
411
|
}
|
|
398
412
|
else {
|
|
399
413
|
this.injectionsInFlight += 1;
|
|
400
|
-
|
|
414
|
+
// 回答キーの注入が終わるまで chat を注入しない(`chatDrainHeldByQuestion`)。
|
|
415
|
+
actor.questionAnswerInjections += 1;
|
|
416
|
+
// 同期 throw でも `.finally` を必ず通す(通らないと回答注入中の件数が戻らず、chat が
|
|
417
|
+
// 永久に保留される)。
|
|
418
|
+
let injection;
|
|
419
|
+
try {
|
|
420
|
+
injection = this.options.questionInjector?.(message.answers, message.session) ?? Promise.resolve();
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
injection = Promise.reject(error);
|
|
424
|
+
}
|
|
425
|
+
void injection
|
|
401
426
|
.catch((error) => {
|
|
402
427
|
this.options.log?.(`設問回答注入失敗: ${String(error)}`);
|
|
403
428
|
// 自己修復(question-answer-retry): pendingQuestion を失ったまま TUI ダイアログが
|
|
@@ -425,6 +450,7 @@ export class SessionHub {
|
|
|
425
450
|
})
|
|
426
451
|
.finally(() => {
|
|
427
452
|
this.injectionsInFlight -= 1;
|
|
453
|
+
actor.questionAnswerInjections -= 1;
|
|
428
454
|
void this.drainChatQueue(message.session, actor);
|
|
429
455
|
});
|
|
430
456
|
}
|
|
@@ -553,6 +579,13 @@ export class SessionHub {
|
|
|
553
579
|
}
|
|
554
580
|
if (message.type === "chat_send") {
|
|
555
581
|
const actor = this.actor(message.session);
|
|
582
|
+
// 利用者が新しい発話で改めて「この設問に答えずに送る」を送った。閉じられなかった設問を
|
|
583
|
+
// もう一度だけ試す(失敗させた発話そのものの再送では試さない)。
|
|
584
|
+
if (message.cancelQuestionId !== undefined &&
|
|
585
|
+
actor.choiceCancelFailed?.questionId === message.cancelQuestionId &&
|
|
586
|
+
!actor.choiceCancelFailed.clientMessageIds.has(message.clientMessageId)) {
|
|
587
|
+
actor.choiceCancelFailed = null;
|
|
588
|
+
}
|
|
556
589
|
if (hasDeliveredReceipt(actor.deletedChatMessageIds, message.clientMessageId)) {
|
|
557
590
|
this.sendTo(client, {
|
|
558
591
|
type: "chat_send_result", id: message.id, status: "failed",
|
|
@@ -639,6 +672,12 @@ export class SessionHub {
|
|
|
639
672
|
void this.drainChatQueue(message.session, actor);
|
|
640
673
|
return;
|
|
641
674
|
}
|
|
675
|
+
if (message.type === "chat_send_now") {
|
|
676
|
+
const actor = this.actor(message.session);
|
|
677
|
+
actor.sendNowWaiters.push({ client, id: message.id, receivedAtMs: this.nowMs() });
|
|
678
|
+
void this.runSendNow(message.session, actor);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
642
681
|
if (message.type === "pending_message_delete") {
|
|
643
682
|
// 対象不在も成功だが、client 保存前 crash で同じ Outbox が復元されても実行しないよう
|
|
644
683
|
// 削除 tombstone は durable に残す。actor 不在でもここだけは軽量 actor を作る。
|
|
@@ -783,6 +822,9 @@ export class SessionHub {
|
|
|
783
822
|
}
|
|
784
823
|
entry.waiters.length = 0;
|
|
785
824
|
}
|
|
825
|
+
for (const waiter of actor.sendNowWaiters.splice(0)) {
|
|
826
|
+
this.sendTo(waiter.client, { type: "chat_send_now_result", id: waiter.id, status: "failed", error: reason });
|
|
827
|
+
}
|
|
786
828
|
// drain が injector / App Server の await 中でも、復帰後に同じ actor 参照から次の旧入力へ
|
|
787
829
|
// 進めないよう durable queue の実体も空にする。現在実行中の1件は復帰時のidentity検査で捨てる。
|
|
788
830
|
actor.chatQueue.length = 0;
|
|
@@ -1316,13 +1358,12 @@ export class SessionHub {
|
|
|
1316
1358
|
ownState.backfilling = false;
|
|
1317
1359
|
if (reason !== "history-done") {
|
|
1318
1360
|
this.options.log?.(`audit backfill-force-finish session=${session} reason=${reason}`);
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1361
|
+
// Claude も全履歴を begin/done で囲む。不完全な履歴を採用せず、iOS のバッファを解除する。
|
|
1362
|
+
this.sendTo(client, {
|
|
1363
|
+
type: "conversation_event", session, serverSeq: 0,
|
|
1364
|
+
payload: { type: "chat_output", v: PROTOCOL_V1,
|
|
1365
|
+
streamId: "pc:history-cancelled", role: "system", text: "", eof: true },
|
|
1366
|
+
});
|
|
1326
1367
|
}
|
|
1327
1368
|
if (actor.codexLive !== null) {
|
|
1328
1369
|
// codex は rollout(履歴) と app-server(live) が同じ発話を二重に表すため、履歴側が
|
|
@@ -1519,6 +1560,8 @@ export class SessionHub {
|
|
|
1519
1560
|
pendingCodexTurns: new Map(), startingCodexMessageIds: new Set(),
|
|
1520
1561
|
runtimeClaim: null, codexLive: null,
|
|
1521
1562
|
chatQueue: [], chatOrder: [], chatDrainRunning: false, chatDrainBlocked: false,
|
|
1563
|
+
questionAnswerInjections: 0, cancellingQuestionId: null, choiceCancelFailed: null,
|
|
1564
|
+
sendNowRunning: false, sendNowWaiters: [],
|
|
1522
1565
|
codexQueue: [], codexOrder: [], codexDrainRunning: false, codexDrainBlocked: false };
|
|
1523
1566
|
this.actors.set(session, actor);
|
|
1524
1567
|
}
|
|
@@ -1660,12 +1703,97 @@ export class SessionHub {
|
|
|
1660
1703
|
actor.codexDrainBlocked = false;
|
|
1661
1704
|
void this.drainCodexQueue(session, actor);
|
|
1662
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* 未回答の設問が chat の注入を止めているか。
|
|
1708
|
+
*
|
|
1709
|
+
* 設問ダイアログの表示中に本文を打つと選択肢へ落ちるので、原則は回答(または TUI 側での解消)まで
|
|
1710
|
+
* durable queue に保留する。回答キーの注入中も同じ(受理の時点で `pendingQuestion` は clear
|
|
1711
|
+
* 済みだが、pane にはまだ設問が出ている)。
|
|
1712
|
+
*
|
|
1713
|
+
* 例外は利用者が「この設問に答えずに送る」と決めた送信が queue にあるとき(chat-cancel-choice):
|
|
1714
|
+
* 設問を Esc で閉じてから注入するので保留しない。**`cancelQuestionId` がいま未回答の設問の id と
|
|
1715
|
+
* 一致するときだけ**解く。利用者が見ていない別の設問(queue に残っていた送信が、後から出た
|
|
1716
|
+
* 設問に出会う等)は閉じない。queue のどこかに 1 件あれば先頭から順に流す(FIFO を崩して
|
|
1717
|
+
* 後続だけを先に送らない。先に保留されていた発話も同じ利用者の発話で、設問を閉じれば届けられる)。
|
|
1718
|
+
* Codex の設問(App Server の request)は pane のダイアログではなく Esc で閉じられないため対象外。
|
|
1719
|
+
*/
|
|
1720
|
+
chatDrainHeldByQuestion(actor) {
|
|
1721
|
+
if (actor.questionAnswerInjections > 0)
|
|
1722
|
+
return true;
|
|
1723
|
+
const pending = actor.pendingQuestion;
|
|
1724
|
+
if (pending === null)
|
|
1725
|
+
return false;
|
|
1726
|
+
if (pending.answerRoute !== "tui")
|
|
1727
|
+
return true;
|
|
1728
|
+
// 一度閉じられなかった設問は、利用者が新しい発話で改めて送るまで保留に戻す。
|
|
1729
|
+
if (actor.choiceCancelFailed?.questionId === pending.id)
|
|
1730
|
+
return true;
|
|
1731
|
+
return !actor.chatQueue.some((entry) => entry.message.cancelQuestionId === pending.id);
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* 「今すぐ送信」を pane へ送る(chat-send-now)。chat の注入と同じ pane を触るので直列化する:
|
|
1735
|
+
* 注入の最中なら終わるのを待ち(`drainChatQueue` の終わりで呼び直される)、送っている間は
|
|
1736
|
+
* 注入を始めさせない。設問の表示中・回答キーの注入中は 1 キーも送らずに断る(chord は
|
|
1737
|
+
* ダイアログでは効かず、キーだけが選択肢へ落ちる)。
|
|
1738
|
+
*/
|
|
1739
|
+
async runSendNow(session, actor) {
|
|
1740
|
+
if (actor.sendNowRunning || actor.chatDrainRunning || actor.sendNowWaiters.length === 0)
|
|
1741
|
+
return;
|
|
1742
|
+
// 待ちすぎた要求には chord を送らない(1 キーも送らずに失敗で返す)。
|
|
1743
|
+
const expired = actor.sendNowWaiters.filter((waiter) => this.nowMs() - waiter.receivedAtMs > SEND_NOW_QUEUE_WAIT_LIMIT_MS);
|
|
1744
|
+
const waiters = actor.sendNowWaiters.filter((waiter) => !expired.includes(waiter));
|
|
1745
|
+
actor.sendNowWaiters.length = 0;
|
|
1746
|
+
const reply = (outcome, targets = waiters) => {
|
|
1747
|
+
this.options.log?.(`audit chat-send-now session=${auditValue(session)} status=${outcome.status}`);
|
|
1748
|
+
for (const waiter of targets) {
|
|
1749
|
+
this.sendTo(waiter.client, {
|
|
1750
|
+
type: "chat_send_now_result", id: waiter.id, status: outcome.status,
|
|
1751
|
+
...("reason" in outcome ? { error: outcome.reason } : {}),
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
if (expired.length > 0)
|
|
1756
|
+
reply({ status: "failed", reason: SEND_NOW_EXPIRED_IN_QUEUE }, expired);
|
|
1757
|
+
if (waiters.length === 0) {
|
|
1758
|
+
void this.drainChatQueue(session, actor);
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1761
|
+
// 設問を Esc で閉じている最中(`cancellingQuestionId`)は chat の注入の中なので、上の
|
|
1762
|
+
// `chatDrainRunning` で待っている。ここへ来るのは注入が終わった後だけ。
|
|
1763
|
+
if (actor.questionAnswerInjections > 0 || actor.pendingQuestion?.answerRoute === "tui") {
|
|
1764
|
+
reply({ status: "blocked", reason: SEND_NOW_BLOCKED_BY_DIALOG });
|
|
1765
|
+
// 注入の終わりから呼ばれた場合、残りの queue の再開はこちらの役目(門番は drain 側が持つ)。
|
|
1766
|
+
void this.drainChatQueue(session, actor);
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
actor.sendNowRunning = true;
|
|
1770
|
+
this.injectionsInFlight += 1;
|
|
1771
|
+
let outcome;
|
|
1772
|
+
try {
|
|
1773
|
+
outcome = await (this.options.sendNowInjector?.(session) ??
|
|
1774
|
+
Promise.resolve({ status: "failed", reason: "send-now is unavailable" }));
|
|
1775
|
+
}
|
|
1776
|
+
catch (error) {
|
|
1777
|
+
outcome = { status: "failed", reason: error instanceof Error ? error.message : String(error) };
|
|
1778
|
+
}
|
|
1779
|
+
finally {
|
|
1780
|
+
this.injectionsInFlight -= 1;
|
|
1781
|
+
actor.sendNowRunning = false;
|
|
1782
|
+
}
|
|
1783
|
+
reply(outcome);
|
|
1784
|
+
// 明示 kill / reaper kill が await 中に actor を廃棄していたら、古い queue を動かさない。
|
|
1785
|
+
if (this.actors.get(session) !== actor)
|
|
1786
|
+
return;
|
|
1787
|
+
void this.drainChatQueue(session, actor);
|
|
1788
|
+
void this.runSendNow(session, actor);
|
|
1789
|
+
}
|
|
1663
1790
|
async drainChatQueue(session, actor) {
|
|
1664
|
-
if (actor.chatDrainRunning || actor.chatDrainBlocked || actor.
|
|
1791
|
+
if (actor.chatDrainRunning || actor.chatDrainBlocked || actor.sendNowRunning ||
|
|
1792
|
+
this.chatDrainHeldByQuestion(actor))
|
|
1665
1793
|
return;
|
|
1666
1794
|
actor.chatDrainRunning = true;
|
|
1667
1795
|
try {
|
|
1668
|
-
while (actor
|
|
1796
|
+
while (!this.chatDrainHeldByQuestion(actor)) {
|
|
1669
1797
|
const entry = actor.chatQueue[0];
|
|
1670
1798
|
if (entry === undefined)
|
|
1671
1799
|
break;
|
|
@@ -1676,6 +1804,12 @@ export class SessionHub {
|
|
|
1676
1804
|
break;
|
|
1677
1805
|
actor.chatQueue.shift();
|
|
1678
1806
|
this.injectionsInFlight += 1;
|
|
1807
|
+
// この注入が閉じることになる設問(ここまで来て未回答の設問が残っているのは、それを指す
|
|
1808
|
+
// `cancelQuestionId` 付きの送信が queue にあって保留が解けたときだけ)。注入の前に控える:
|
|
1809
|
+
// await の間に別の設問へ替わっていたら触らない。
|
|
1810
|
+
const cancelledQuestion = actor.pendingQuestion;
|
|
1811
|
+
if (cancelledQuestion !== null)
|
|
1812
|
+
actor.cancellingQuestionId = cancelledQuestion.id;
|
|
1679
1813
|
try {
|
|
1680
1814
|
actor.injectingChatMessageIds.add(message.clientMessageId);
|
|
1681
1815
|
if (!this.persistChatReceipts()) {
|
|
@@ -1690,13 +1824,27 @@ export class SessionHub {
|
|
|
1690
1824
|
}
|
|
1691
1825
|
continue;
|
|
1692
1826
|
}
|
|
1827
|
+
const resolveCancelledQuestion = () => {
|
|
1828
|
+
if (cancelledQuestion === null || this.actors.get(session) !== actor ||
|
|
1829
|
+
actor.pendingQuestion?.id !== cancelledQuestion.id)
|
|
1830
|
+
return;
|
|
1831
|
+
this.setPendingQuestion(session, actor, null);
|
|
1832
|
+
this.broadcast({ type: "question_event", session, event: "dismiss", id: cancelledQuestion.id });
|
|
1833
|
+
this.options.log?.(`audit chat-cancel-choice session=${auditValue(session)} question=${auditValue(cancelledQuestion.id)}`);
|
|
1834
|
+
};
|
|
1693
1835
|
await (this.options.chatInjector?.(message.text, session, {
|
|
1694
1836
|
recordedPromptText: () => this.trailingUserPromptText(session),
|
|
1837
|
+
cancelChoice: cancelledQuestion !== null,
|
|
1838
|
+
// 閉じた時点で片付ける(続く本文の注入が失敗しても、TUI に設問はもう無い)。
|
|
1839
|
+
onChoiceCancelled: resolveCancelledQuestion,
|
|
1695
1840
|
}) ?? Promise.resolve());
|
|
1696
1841
|
// 明示 kill / reaper kill が注入 await 中に actor を廃棄した場合、同名の新 actorへ
|
|
1697
1842
|
// 古い receipt を復活させない。waiter は retireSession が既に失敗で解放している。
|
|
1698
1843
|
if (this.actors.get(session) !== actor)
|
|
1699
1844
|
continue;
|
|
1845
|
+
// 本文が入力欄へ届いた = 設問はもう pane を塞いでいない(閉じたのを見届けた場合に加え、
|
|
1846
|
+
// TUI 側で先に解消されていて dismiss だけが届いていなかった場合もここで片付く)。
|
|
1847
|
+
resolveCancelledQuestion();
|
|
1700
1848
|
actor.injectingChatMessageIds.delete(message.clientMessageId);
|
|
1701
1849
|
actor.pendingChatMessages.delete(message.clientMessageId);
|
|
1702
1850
|
removeOrderedID(actor.chatOrder, message.clientMessageId);
|
|
@@ -1725,6 +1873,15 @@ export class SessionHub {
|
|
|
1725
1873
|
// **非配達が確定**している。uncertain は「二重送信が怖くて消せない」分類なので、
|
|
1726
1874
|
// ここへ積むと削除不能・後続ブロックのゾンビになる(下のコメント参照)。
|
|
1727
1875
|
const rejectedBeforeSend = error instanceof LoginCodeError || error instanceof ChatInjectionRejectedError;
|
|
1876
|
+
// 設問を閉じられなかった(設問はまだ未回答で残っている)。後続の保留分で Esc を
|
|
1877
|
+
// 撃ち直さない。
|
|
1878
|
+
if (cancelledQuestion !== null && actor.pendingQuestion?.id === cancelledQuestion.id) {
|
|
1879
|
+
const failed = actor.choiceCancelFailed?.questionId === cancelledQuestion.id
|
|
1880
|
+
? actor.choiceCancelFailed
|
|
1881
|
+
: { questionId: cancelledQuestion.id, clientMessageIds: new Set() };
|
|
1882
|
+
failed.clientMessageIds.add(message.clientMessageId);
|
|
1883
|
+
actor.choiceCancelFailed = failed;
|
|
1884
|
+
}
|
|
1728
1885
|
if (rejectedBeforeSend) {
|
|
1729
1886
|
removeOrderedID(actor.chatOrder, message.clientMessageId);
|
|
1730
1887
|
}
|
|
@@ -1744,12 +1901,20 @@ export class SessionHub {
|
|
|
1744
1901
|
}
|
|
1745
1902
|
finally {
|
|
1746
1903
|
this.injectionsInFlight -= 1;
|
|
1904
|
+
// 閉じ終えた(設問は片付いた)か、諦めた(設問は残っている = また回答を受け付ける)。
|
|
1905
|
+
if (cancelledQuestion !== null && actor.cancellingQuestionId === cancelledQuestion.id) {
|
|
1906
|
+
actor.cancellingQuestionId = null;
|
|
1907
|
+
}
|
|
1747
1908
|
}
|
|
1748
1909
|
}
|
|
1749
1910
|
}
|
|
1750
1911
|
finally {
|
|
1751
1912
|
actor.chatDrainRunning = false;
|
|
1752
|
-
if (
|
|
1913
|
+
if (actor.sendNowWaiters.length > 0) {
|
|
1914
|
+
// 注入が終わるのを待っていた「今すぐ送信」を先に流す(残りの queue はその後で再開する)。
|
|
1915
|
+
void this.runSendNow(session, actor);
|
|
1916
|
+
}
|
|
1917
|
+
else if (!actor.chatDrainBlocked && !this.chatDrainHeldByQuestion(actor) && actor.chatQueue.length > 0 &&
|
|
1753
1918
|
actor.chatOrder[0] === actor.chatQueue[0]?.message.clientMessageId) {
|
|
1754
1919
|
void this.drainChatQueue(session, actor);
|
|
1755
1920
|
}
|