@tomflow/chatloop-extension 0.3.46 → 0.3.48

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.
@@ -3,37 +3,11 @@
3
3
  importScripts("loop/core.js", "loop/resources.js");
4
4
  const RUN_KEY = "chat-loop.action.run.v1";
5
5
  const SESSION_HOST = "com.tomflow.chatloop.extension";
6
- const HOUR_MS = 60 * 60 * 1000; // Acceptance wall-clock hours are independent of the MVP timeout.
7
- const RUN_BUDGET_MS = 10 * 60 * 1000; // Ten minutes per Chat; never interrupt a running Turn.
6
+ const RUN_BUDGET_MS = 10 * 60 * 1000;
8
7
  const ALARM_NAME = "chat-loop.turn-watch.v1";
9
- // One-shot deadline on the same MV3 alarm facility; never sends a Chat message.
10
8
  const DEADLINE_ALARM_NAME = "chat-loop.handoff-deadline.v1";
11
- const COUNT_ALARM_NAME = "chat-loop.count-wait.v1";
12
- const COUNT_INTERVAL_MS = 60000;
13
9
  const CONTINUE_TEXT = "继续当前任务,完成本轮后只在末行写一个状态:[CHAT_LOOP_STATUS:CONTINUE]、[CHAT_LOOP_STATUS:DONE] 或 [CHAT_LOOP_STATUS:BLOCKED]。";
14
- // The model cannot authenticate its originating Chat through MCP alone.
15
- // This per-Turn capability is sent only to the newly confirmed loop-owned Chat.
16
- // It is never a browser-send authorization; it only permits staging one intent.
17
10
  const turnToolNote = token => "\n\n【Chat Loop 本轮工具凭证】只有本轮需要自定下一轮内容或提前交接时,才能调用 chat_loop_extension__continue / chat_loop_extension__handoff;参数 turnToken 填写 " + token + "。工具只登记意图,不等待本轮结束,不直接发送;请照常在本轮结束时输出 CONTINUE、DONE 或 BLOCKED 标记。不要在助手正文重复该凭证。";
18
- // Explicit acceptance setup only, dispatched by the same formal Continue page.
19
- // Fixed harmless prompt; no arbitrary text or external write interface.
20
- const ACTION_TEST_TEXT = "这是 Chat Loop 短轮回归专用测试,不调用工具、不改文件。每次仅回复一句短话,最后一行单独输出一个协议状态标记。\n第1条消息:OK1,然后末行 [CHAT_LOOP_STATUS:CONTINUE]。\n第2条消息:OK2,然后末行 [CHAT_LOOP_STATUS:CONTINUE]。\n第3条消息:OK3,然后末行 [CHAT_LOOP_STATUS:DONE]。\n不主动交接,也不追加解释。校验串:LOOP-V037-FAST-3C1H。";
21
- const ACTION_TIMED_TEST_TEXT = "这是 Chat Loop 的长回复与一小时边界隔离测试,不能使用工具,不访问其他聊天,不改文件。请认真完成以下技术文字任务:用至少十八个独立自然段,以不同角度解释长任务的状态管理、异步任务完成检测、最终回执、一次性发送、跨会话交接、故障隔离与日志证据。段落各包含具体推理或例子,持续生成直到全部十八段完成。即使你看到与“时间到期”有关的叙述,也不要主动停止、跳过段落或交接。在全部正文结束后,最后一行只能输出 [CHAT_LOOP_STATUS:CONTINUE];不能提前写出这个标记。当前 Chat 不再收到新的用户消息。\n\n如果你是在新 Chat 收到了包含【原始任务用户消息】【最近轮次证据】的定时交接,请忽略上述长文要求,只回复“定时交接接收完成”,并在最后一行输出 [CHAT_LOOP_STATUS:DONE]。\n\n本测试仅用于验证真正完成一个回复之后才触发交接,模型生成需要多久并没有固定值。校验串 LOOP-V037-FAST-3C1H / MODE-TIMED-1H。";
22
- const ACTION_WALL_TEST_TEXT = "这是 Chat Loop 的墙钟边界隔离测试,不调用工具、不改文件。原 Chat 每轮计算当前算式,第一行只写得数,末行单独写 [CHAT_LOOP_STATUS:CONTINUE]。第一题:1+1=?后续题目由 Continue 提供。新 Chat 若收到【原始任务用户消息】和【最近轮次证据】,只回复“已接班”,末行写 [CHAT_LOOP_STATUS:DONE]。交接仅由扩展按时钟决定。校验串 LOOP-V037-FAST-3C1H / MODE-WALL-10M / WALL-CLOCK-TEST。";
23
- // The arithmetic progression belongs ONLY to the controlled 10m acceptance
24
- // Chat. Each confirmed continuation asks a different, easily checked problem.
25
- // No prompt variation is a substitute for respecting website security checks.
26
- const wallArithmeticText = step => {
27
- const left = Math.ceil(step / 2);
28
- const right = step === 2 ? 3 : step;
29
- return `第${step}题:${left}+${right}=?只写得数,末行写 [CHAT_LOOP_STATUS:CONTINUE]。`;
30
- };
31
- const COUNT_SETUP_TEXT = "这是 Chat Loop 的计数隔离验收,不调用工具、不改文件。初始整数86,本轮加1后输出87。以后每收到一条Continue才加1。每轮只能输出两行:第一行是整数;第二行是状态,整数小于100输出 [CHAT_LOOP_STATUS:CONTINUE],等于100才输出 [CHAT_LOOP_STATUS:DONE]。新Chat从交接包的最后已确认数值继续加1,交接本身不得DONE。十分钟交接由扩展执行,60秒发送间隔由扩展控制。校验串 LOOP-V037-FAST-3C1H / MODE-WALL-10M / COUNT-86-100 / WALL-CLOCK-TEST。";
32
- const countText = n => `上一轮确认数字是${n}。本轮只加1,第一行只输出${n + 1};第二行数字未到100写 [CHAT_LOOP_STATUS:CONTINUE],达到100才写 [CHAT_LOOP_STATUS:DONE]。`;
33
- const readCount = reply => {
34
- const m = String(reply).trim().match(/(?:^|\n)\s*(\d{2,3})\s*\r?\n\s*\[CHAT_LOOP_STATUS:(?:CONTINUE|DONE)\]$/);
35
- return m ? Number(m[1]) : null;
36
- };
37
11
  let queue = Promise.resolve();
38
12
  const serial = fn => {
39
13
  const work = queue.then(fn);
@@ -69,12 +43,10 @@ const pathOf = url => {
69
43
  };
70
44
  const readRun = async () => (await chrome.storage.local.get(RUN_KEY))[RUN_KEY] || null;
71
45
  const windowMsOf = run => Number.isSafeInteger(run?.windowMs) && run.windowMs > 0
72
- ? run.windowMs : run?.timedTest && run.wallTestMinutes === 20
73
- ? 20 * 60 * 1000 : RUN_BUDGET_MS;
46
+ ? run.windowMs : RUN_BUDGET_MS;
74
47
  const deadlineOf = run => Number.isSafeInteger(run?.startedAt) && run.startedAt > 0
75
48
  ? run.startedAt + windowMsOf(run) : null;
76
- const clockActive = run => run && ["RESERVED", "PENDING", "WAITING_REPLY", "WAITING_INTERVAL"].includes(run.phase)
77
- && !run.testSetup && !run.wallTestHours;
49
+ const clockActive = run => run && ["RESERVED", "PENDING", "WAITING_REPLY"].includes(run.phase);
78
50
  // Durable, bounded metadata for model intent receipts. The prompt text remains only
79
51
  // in the active run while its Action is pending; history never stores the prompt.
80
52
  const MODEL_RECEIPT_TTL_MS = 24 * 60 * 60 * 1000;
@@ -139,9 +111,6 @@ async function saveRun(run) {
139
111
  } else {
140
112
  await chrome.alarms?.clear?.(DEADLINE_ALARM_NAME);
141
113
  }
142
- if (next.phase === "WAITING_INTERVAL" && next.countMode && !next.handoffPending)
143
- await chrome.alarms?.create?.(COUNT_ALARM_NAME, {when: next.countReleaseAt});
144
- else await chrome.alarms?.clear?.(COUNT_ALARM_NAME);
145
114
  } catch { /* The existing periodic alarm is a conservative backup. */ }
146
115
  }
147
116
  // Browser Owner remains the only send authority. Native Host mirrors a minimal
@@ -492,8 +461,7 @@ async function onModelIntent(message,sender) {
492
461
  ||/[\x00-\x1f]/.test(intent.reason)))
493
462
  return {status:'DENIED',code:'INVALID_MODEL_INTENT'};
494
463
  const run=await readRun();
495
- if(!run||run.phase!=='WAITING_REPLY'||run.testSetup||run.countMode
496
- ||run.timedTest||run.wallTestHours||run.modelTurnToken!==intent.turnToken)
464
+ if(!run||run.phase!=='WAITING_REPLY'||run.modelTurnToken!==intent.turnToken)
497
465
  return {status:'DENIED',code:'MODEL_INTENT_OWNER_NOT_RUNNING'};
498
466
  if(run.modelIntent)
499
467
  return {status:'DENIED',code:'MODEL_INTENT_ALREADY_QUEUED'};
@@ -551,8 +519,6 @@ async function onTurnFinished(message, sender) {
551
519
  // A Chat is owned only after an extension Action has explicitly reserved
552
520
  // and confirmed its user Turn. A status marker on an arbitrary Chat never
553
521
  // starts or reactivates a loop, including after DONE/BLOCKED.
554
- if (previous?.testSetup && previous.tabId === tabId)
555
- return {status: "IGNORED", code: "TEST_SETUP_UNVERIFIED"};
556
522
  if (!previous || ["DONE", "BLOCKED"].includes(previous.phase))
557
523
  return {status: "IGNORED", code: "NO_ACTIVE_OWNER"};
558
524
  if (previous.tabId !== tabId || previous.page !== message.page)
@@ -574,27 +540,6 @@ async function onTurnFinished(message, sender) {
574
540
  const recovery = await recheckUnknownTurn(message, previous, tabId);
575
541
  if (recovery) {status = recovery.status; replyText = recovery.replyText;}
576
542
  }
577
- // Dedicated acceptance Chats only: retain the exact unrecognized reply to
578
- // diagnose real UI text wrappers. Never persist private user Chat text.
579
- let parseEvidence = null;
580
- if (status === "UNKNOWN") {
581
- const owned = (await chrome.storage.local.get("chat-loop.acceptance.owned.v1"))[
582
- "chat-loop.acceptance.owned.v1"];
583
- if (Array.isArray(owned) && owned.some(item => item.id === tabId || item.id === previous?.sourceTabId)) {
584
- parseEvidence = {replyText: message.replyText.slice(0, 2500),
585
- length: message.replyText.length, capturedAt: Date.now()};
586
- }
587
- }
588
- const countMode = previous?.countMode === true;
589
- let verifiedCount = null;
590
- if (countMode && (status === "CONTINUE" || status === "DONE")) {
591
- verifiedCount = readCount(replyText);
592
- if (verifiedCount !== previous.countValue + 1
593
- || status !== (verifiedCount === 100 ? "DONE" : "CONTINUE")) {
594
- await saveRun({...previous, phase: "BLOCKED", reason: "COUNT_STEP_UNVERIFIED"});
595
- return {status: "BLOCKED", code: "COUNT_STEP_UNVERIFIED"};
596
- }
597
- }
598
543
  const now = Date.now();
599
544
  // Keep the same clock throughout one Chat. A successful Handoff resets it.
600
545
  const initial = Number.isSafeInteger(message.startedAt) && message.startedAt > 0
@@ -615,35 +560,11 @@ async function onTurnFinished(message, sender) {
615
560
  if (previous?.phase === "WAITING_REPLY" && previous.tabId === tabId) {
616
561
  await saveRun({...previous, phase: status === "DONE" ? "DONE" : "BLOCKED",
617
562
  reason: status, userKey: message.userKey, assistantKey: message.assistantKey,
618
- ...(countMode && verifiedCount !== null ? {countValue: verifiedCount} : {}),
619
- ...(parseEvidence ? {parseEvidence} : {})});
563
+ });
620
564
  }
621
565
  return {status: "IGNORED", code: nextAction === "UNSUPPORTED_MODEL_HANDOFF"
622
566
  ? "MODEL_HANDOFF_DISABLED" : status};
623
567
  }
624
- // Historical 1h/2h endurance fixtures explicitly park the Chat to avoid
625
- // huge message counts; the real 10m acceptance MUST use the normal running
626
- // loop: each finished CONTINUE immediately dispatches Continue, without a hold.
627
- if (previous?.wallTestHours
628
- && status === "CONTINUE" && !timeExpired
629
- && !previous.wallReleasedAt && previous.phase === "WAITING_REPLY") {
630
- const releaseAt = startedAt + (previous.wallTestMinutes === 10
631
- ? RUN_BUDGET_MS : previous.wallTestHours * HOUR_MS);
632
- await saveRun({...previous, phase: "WALL_CLOCK_HOLD", reason: "ACCEPTANCE_WALL_WAIT",
633
- userKey: message.userKey, assistantKey: message.assistantKey,
634
- wallHoldAt: now, wallReleaseAt: releaseAt});
635
- return {status: "WAITING_DEADLINE", releaseAt};
636
- }
637
- // Count acceptance: completion is recorded now, but the next Continue is
638
- // not authorized until 60 real seconds after the previous confirmed send.
639
- if (countMode && nextAction === "CONTINUE" && !timeExpired
640
- && now < previous.countLastSendAt + COUNT_INTERVAL_MS) {
641
- const countReleaseAt = previous.countLastSendAt + COUNT_INTERVAL_MS;
642
- await saveRun({...previous, phase: "WAITING_INTERVAL", reason: "COUNT_INTERVAL",
643
- userKey: message.userKey, assistantKey: message.assistantKey,
644
- countValue: verifiedCount, countReleaseAt});
645
- return {status: "WAITING_INTERVAL", countReleaseAt};
646
- }
647
568
  // Recheck at the last new-authorization boundary, even if a periodic or
648
569
  // one-shot alarm was delayed while the Worker handled completion evidence.
649
570
  const newlyDue = Date.now() >= startedAt + windowMsOf(previous);
@@ -655,34 +576,11 @@ async function onTurnFinished(message, sender) {
655
576
  assistantKey: message.assistantKey, phase: "RESERVED", token,
656
577
  action: nextAction, startedAt, modelReceipts:receiptHistory(previous),
657
578
  ...(previous?.lastFaultProof ? {lastFaultProof:previous.lastFaultProof} : {}),
658
- ...(countMode ? {countMode:true, countValue:verifiedCount,
659
- countLastSendAt:previous.countLastSendAt,
660
- countHandoffs:previous.countHandoffs || 0} : {}),
661
579
  ...((previous?.handoffPending || newlyDue) ? {
662
580
  handoffPending: true,
663
581
  handoffPendingAt: previous?.handoffPendingAt || Date.now()} : {}),
664
582
  // A completed Turn is a successful recovery and clears the consecutive error-Handoff guard.
665
583
  errorHandoffCount: 0,
666
- // Carry only dedicated acceptance evidence across the finished Turn. The
667
- // evidence proves the deadline was crossed while Observer was RUNNING.
668
- ...(previous?.timedTest && previous?.clockAdvancedAt ? {
669
- timedTest: true, clockAdvancedAt: previous.clockAdvancedAt,
670
- clockAdvanceMinutes: previous.clockAdvanceMinutes,
671
- clockRunningProof: previous.clockRunningProof} : {}),
672
- // Keep the 10m continuous-flow test identity only for audit evidence;
673
- // unlike historical hour-long endurance it never delays the next Continue.
674
- ...(previous?.timedTest && previous?.wallTestMinutes === 10 ? {
675
- timedTest: true, wallTestMinutes: 10,
676
- testTurnIndex: (previous.testTurnIndex || 1) + 1} : {}),
677
- // Preserve historical wall-clock hold receipt across its subsequent Turn.
678
- ...(previous?.timedTest && previous?.wallReleasedAt ? {
679
- timedTest: true, wallTestHours: previous.wallTestHours,
680
- wallHoldAt: previous.wallHoldAt, wallReleaseAt: previous.wallReleaseAt,
681
- wallReleasedAt: previous.wallReleasedAt,
682
- ...([10, 20].includes(previous.wallTestMinutes) ? {
683
- wallTestMinutes: previous.wallTestMinutes,
684
- ...(previous.wallTestMinutes === 20 ? {wallAdjustedAt: previous.wallAdjustedAt,
685
- wallOriginalReleaseAt: previous.wallOriginalReleaseAt} : {})} : {})} : {}),
686
584
  // A timed Handoff uses an Action-local read-only snapshot. Do not keep
687
585
  // the entire conversation in storage or embed it in the Action URL.
688
586
  handoffMode: nextAction === "HANDOFF" ? "TIMED" : null,
@@ -710,42 +608,6 @@ async function onTurnFinished(message, sender) {
710
608
  return {status: "BLOCKED", code: error.message || "ACTION_PAGE_CREATE_FAILED"};
711
609
  }
712
610
  }
713
- // Release a verified completed count Turn only after the persisted 60s gate.
714
- // The independent 10m cutoff wins over an unissued Continue.
715
- async function releaseCountInterval() {
716
- const old = await readRun();
717
- if (old?.phase !== "WAITING_INTERVAL" || old.countMode !== true) return;
718
- const now = Date.now(), deadline = deadlineOf(old);
719
- if (now < old.countReleaseAt && now < deadline && !old.handoffPending) return;
720
- const tab = await chrome.tabs.get(old.tabId).catch(() => null);
721
- if (pathOf(tab?.url) !== old.page) {
722
- await saveRun({...old, phase:"BLOCKED", reason:"COUNT_TAB_CHANGED"});
723
- return;
724
- }
725
- const proof = await chrome.tabs.sendMessage(old.tabId,
726
- {type:"CHAT_LOOP_TURN_VERIFY_V1"}).catch(() => null);
727
- if (proof?.status !== "READY" || proof.page !== old.page
728
- || proof.userKey !== old.userKey || proof.assistantKey !== old.assistantKey
729
- || readCount(proof.replyText) !== old.countValue) {
730
- await saveRun({...old, phase:"BLOCKED", reason:"COUNT_TURN_CHANGED"});
731
- return;
732
- }
733
- const action = old.handoffPending || Date.now() >= deadline ? "HANDOFF" : "CONTINUE";
734
- const run = {...old, phase:"RESERVED", action, token:crypto.randomUUID(),
735
- handoffMode: action === "HANDOFF" ? "TIMED" : null};
736
- delete run.countReleaseAt;
737
- await saveRun(run);
738
- try {
739
- await createBoundedAction({url:chrome.runtime.getURL(action === "HANDOFF"
740
- ? "handoff.html" : "continue.html") + "?tabId=" + old.tabId
741
- + "&token=" + encodeURIComponent(run.token), active:false, windowId:tab.windowId});
742
- } catch {
743
- await saveRun({...run,phase:"BLOCKED",reason:"COUNT_ACTION_OPEN_FAILED"});
744
- }
745
- }
746
- // Opening a formal Action page manually is a supported test/debug entry.
747
- // No token in URL: the Worker verifies a single explicit target and reserves
748
- // this exact Action tab. It never sends or creates the new Chat itself.
749
611
  async function onManualPrepare(message, sender) {
750
612
  const requested = message.action;
751
613
  if (!["CONTINUE", "HANDOFF"].includes(requested)
@@ -805,190 +667,6 @@ async function onManualPrepare(message, sender) {
805
667
  ...(summary ? {handoffText: summary} : {})});
806
668
  return {status: "PREPARED", token, tabId: tab.id};
807
669
  }
808
- // This is a test preparation permit, not a successful Continue regression.
809
- // Both readiness probes are read-only; PENDING must be durable before any edit.
810
- async function onTestPrepare(message, sender) {
811
- const tabId = message.tabId;
812
- if (!Number.isInteger(tabId) || !Number.isInteger(sender.tab?.id)
813
- || ![chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1",
814
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&discardDraft=1",
815
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&timed=1",
816
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&discardDraft=1&timed=1",
817
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&timed=1&wall=1",
818
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&discardDraft=1&timed=1&wall=1",
819
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&timed=1&wall=2",
820
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&discardDraft=1&timed=1&wall=2",
821
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&timed=1&wall=10m",
822
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&discardDraft=1&timed=1&wall=10m",
823
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&timed=1&wall=10m&count=86",
824
- chrome.runtime.getURL("continue.html") + "?tabId=" + tabId + "&setup=1&discardDraft=1&timed=1&wall=10m&count=86"].includes(sender.url))
825
- return {status: "DENIED", code: "INVALID_TEST_ACTION"};
826
- const previous = await readRun();
827
- if (previous && (previous.sessionId&&previous.phase!=="DONE"
828
- || !["DONE", "BLOCKED"].includes(previous.phase)))
829
- return {status: "DENIED", code: "LOOP_ALREADY_ACTIVE"};
830
- if (previous?.tabId === tabId)
831
- return {status: "DENIED", code: "TEST_TARGET_ALREADY_USED"};
832
- const tab = await chrome.tabs.get(tabId).catch(() => null);
833
- if (tab?.url !== "https://chatgpt.com/")
834
- return {status: "DENIED", code: "TEST_TARGET_NOT_BLANK"};
835
- const params = new URL(sender.url).searchParams;
836
- const discardDraft = params.get("discardDraft") === "1";
837
- const timedTest = params.get("timed") === "1";
838
- const wall = params.get("wall");
839
- const wallTestHours = wall && wall !== "10m" ? Number(wall) : null;
840
- const wallTestMinutes = wall === "10m" ? 10 : null;
841
- const countMode = params.get("count") === "86";
842
- if (params.has("count") && (!countMode || !timedTest || wallTestMinutes !== 10))
843
- return {status:"DENIED",code:"COUNT_TEST_INVALID"};
844
- if (wall !== null && (!timedTest || !(wallTestMinutes === 10 || [1, 2].includes(wallTestHours))))
845
- return {status: "DENIED", code: "INVALID_WALL_TEST"};
846
- if (timedTest) {
847
- const registry = (await chrome.storage.local.get("chat-loop.acceptance.owned.v1"))[
848
- "chat-loop.acceptance.owned.v1"];
849
- if (!Array.isArray(registry) || !registry.some(item => item.id === tabId))
850
- return {status: "DENIED", code: "TIMED_TEST_OWNERSHIP_REQUIRED"};
851
- }
852
- const probe = async () => {
853
- const [read] = await chrome.scripting.executeScript({target: {tabId}, func: allowDiscard => {
854
- const visible = node => Boolean(node?.getClientRects?.().length);
855
- const mains = [...document.querySelectorAll("main")].filter(visible);
856
- const input = document.querySelector('[contenteditable="true"][data-composer-markdown]')
857
- || document.querySelector('#prompt-textarea[contenteditable="true"]');
858
- const draft = (input?.innerText || input?.textContent || "").trim();
859
- const knownTestDraft = (draft.includes("LOOP-V037-FAST-3C1H") || (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
860
- && draft.includes("【交接参考:只读网页片段,可能截断】")
861
- && draft.includes("【原始任务用户消息】")
862
- && draft.includes("【最近轮次证据】")
863
- && draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
864
- && draft.length >= 450 && draft.length <= 8192)) && (
865
- (draft.startsWith("这是 Chat Loop 短轮回归专用测试") && draft.length <= 800)
866
- || (draft.startsWith("你正在接手上一 Chat 尚未完成的同一个任务")
867
- && draft.length <= 8192 && (draft.length <= 2000
868
- || (draft.includes("【原始任务用户消息】") && draft.includes("【最近轮次证据】"))))
869
- || (draft.startsWith("你正在接手上一 Chat 未完成的任务。旧 Chat 的网页历史仍可核实")
870
- && draft.includes("【原始任务用户消息】") && draft.includes("【最近轮次证据】")
871
- && draft.endsWith("是否跨 Chat 由扩展决定,不要主动要求 Handoff。")
872
- && draft.length >= 450 && draft.length <= 8192)
873
- || (draft.startsWith("这是 Chat Loop 的计数隔离验收")
874
- && draft.includes("COUNT-86-100") && draft.endsWith("WALL-CLOCK-TEST。"))
875
- || ((draft.startsWith("这是 Chat Loop 的长回复与一小时边界隔离测试")
876
- || draft.startsWith("这是 Chat Loop 的墙钟边界隔离测试"))
877
- && (draft.includes("MODE-TIMED-1H") || draft.includes("MODE-WALL-10M"))));
878
- return location.href === "https://chatgpt.com/" && mains.length === 1
879
- && !mains[0].querySelector('[data-chatgpt-search-message-ids],[data-message-author-role]')
880
- && visible(input) && (!draft || (allowDiscard && knownTestDraft))
881
- && ![...document.querySelectorAll('button[data-testid="stop-button"]')].some(visible);
882
- }, args: [discardDraft]});
883
- return read?.result === true;
884
- };
885
- if (!await probe()) return {status: "DENIED", code: "TEST_TARGET_NOT_EMPTY"};
886
- await new Promise(resolve => setTimeout(resolve, 2300));
887
- if (!await probe()) return {status: "DENIED", code: "TEST_TARGET_NOT_STABLE"};
888
- const token = crypto.randomUUID();
889
- await saveRun({tabId, page: "/", userKey: null, assistantKey: null,
890
- phase: "RESERVED", token, action: "CONTINUE", testSetup: true, discardDraft,
891
- ...(timedTest ? {timedTest: true} : {}),
892
- ...(wallTestHours ? {wallTestHours} : {}),
893
- ...(wallTestMinutes ? {wallTestMinutes} : {}),
894
- ...(countMode ? {countMode:true, countValue:86, countHandoffs:0} : {}),
895
- // The first Turn starts with a real clock, including acceptance tests.
896
- // A dedicated test Action may advance it later only while Observer RUNNING.
897
- startedAt: Date.now(),
898
- manualActionTabId: sender.tab.id});
899
- return {status: "PREPARED", token, tabId};
900
- }
901
- async function onAcceptanceShortenWallHold(message, sender) {
902
- // User-requested shortening of an existing *owned acceptance* wait.
903
- // The real elapsed time must already exceed 20 min; never backdate time.
904
- const tabId = message.tabId, now = Date.now();
905
- if (!Number.isSafeInteger(tabId) || tabId <= 0
906
- || message.minutes !== 20 || !Number.isSafeInteger(sender.tab?.id)
907
- || sender.url !== chrome.runtime.getURL("clock.html")
908
- + "?tabId=" + tabId + "&minutes=20")
909
- return {status: "DENIED", code: "SHORTEN_SOURCE_INVALID"};
910
- const run = await readRun();
911
- if (run?.phase !== "WALL_CLOCK_HOLD" || run.tabId !== tabId
912
- || run.timedTest !== true || run.wallTestHours !== 2
913
- || run.wallAdjustedAt || !run.userKey || !run.assistantKey
914
- || !Number.isSafeInteger(run.startedAt)
915
- || now < run.startedAt + 20 * 60 * 1000)
916
- return {status: "DENIED", code: "SHORTEN_NOT_ELIGIBLE"};
917
- const owned = (await chrome.storage.local.get("chat-loop.acceptance.owned.v1"))[
918
- "chat-loop.acceptance.owned.v1"];
919
- if (!Array.isArray(owned) || !owned.some(item => item.id === tabId))
920
- return {status: "DENIED", code: "SHORTEN_NOT_OWNED"};
921
- const tab = await chrome.tabs.get(tabId).catch(() => null);
922
- if (!tab || pathOf(tab.url) !== run.page)
923
- return {status: "DENIED", code: "SHORTEN_TARGET_CHANGED"};
924
- const verify = async () => {
925
- const proof = await chrome.tabs.sendMessage(tabId,
926
- {type: "CHAT_LOOP_TURN_VERIFY_V1"}).catch(() => null);
927
- return proof?.contract === "chat-loop.turn-observer.v1"
928
- && proof.status === "READY" && proof.page === run.page
929
- && proof.userKey === run.userKey
930
- && proof.assistantKey === run.assistantKey
931
- && globalThis.ChatLoopCoreV1.parseStatus(proof.replyText) === "CONTINUE";
932
- };
933
- if (!await verify()) return {status: "DENIED", code: "SHORTEN_NOT_STABLE"};
934
- await new Promise(resolve => setTimeout(resolve, 2300));
935
- if (!await verify()) return {status: "DENIED", code: "SHORTEN_IDENTITY_CHANGED"};
936
- const latest = await readRun();
937
- if (latest?.phase !== "WALL_CLOCK_HOLD" || latest.tabId !== run.tabId
938
- || latest.wallHoldAt !== run.wallHoldAt || latest.userKey !== run.userKey)
939
- return {status: "DENIED", code: "SHORTEN_OWNER_CHANGED"};
940
- const changed = {...latest,
941
- wallOriginalReleaseAt: latest.wallReleaseAt,
942
- wallReleaseAt: latest.startedAt + 20 * 60 * 1000,
943
- wallTestMinutes: 20, wallAdjustedAt: Date.now()};
944
- await saveRun(changed);
945
- return {status: "SHORTENED_AFTER_REAL_20MIN", tabId,
946
- startedAt: changed.startedAt, releaseAt: changed.wallReleaseAt,
947
- adjustedAt: changed.wallAdjustedAt};
948
- }
949
- async function onAcceptanceClockAdvance(message, sender) {
950
- if (message?.minutes === 20) return onAcceptanceShortenWallHold(message, sender);
951
- // Test-only clock mutation. All production time comparisons remain unchanged.
952
- // This must happen DURING the assistant Turn, never before the first Turn.
953
- const tabId = message.tabId, minutes = message.minutes;
954
- if (!Number.isSafeInteger(tabId) || tabId <= 0
955
- || !Number.isSafeInteger(minutes) || minutes < 60 || minutes > 180
956
- || sender.url !== chrome.runtime.getURL("clock.html")
957
- + "?tabId=" + tabId + "&minutes=" + minutes
958
- || !Number.isSafeInteger(sender.tab?.id))
959
- return {status: "DENIED", code: "CLOCK_ACTION_SOURCE_INVALID"};
960
- const run = await readRun();
961
- if (run?.phase !== "WAITING_REPLY" || run?.action !== "CONTINUE"
962
- || run?.tabId !== tabId || run?.timedTest !== true
963
- || run?.clockAdvancedAt || !run.newUserKey || !run.page
964
- || run.testSetup) return {status: "DENIED", code: "CLOCK_TEST_NOT_WAITING"};
965
- const owned = (await chrome.storage.local.get("chat-loop.acceptance.owned.v1"))[
966
- "chat-loop.acceptance.owned.v1"];
967
- if (!Array.isArray(owned) || !owned.some(item => item.id === tabId))
968
- return {status: "DENIED", code: "CLOCK_TEST_NOT_OWNED"};
969
- const live = await chrome.tabs.get(tabId).catch(() => null);
970
- if (!live || pathOf(live.url) !== run.page)
971
- return {status: "DENIED", code: "CLOCK_TARGET_CHANGED"};
972
- const sample = async () => chrome.tabs.sendMessage(tabId,
973
- {type: "CHAT_LOOP_TURN_OBSERVE_V1", poll: true}).catch(() => null);
974
- const isRunning = x => x?.contract === "chat-loop.turn-observer.v1"
975
- && x.status === "RUNNING" && x.page === run.page
976
- && x.userKey === run.newUserKey;
977
- const first = await sample();
978
- if (!isRunning(first)) return {status: "DENIED", code: "CLOCK_TURN_NOT_RUNNING"};
979
- await new Promise(resolve => setTimeout(resolve, 300));
980
- const second = await sample();
981
- if (!isRunning(second)) return {status: "DENIED", code: "CLOCK_TURN_NOT_STABLE_RUNNING"};
982
- const now = Date.now();
983
- // Advance by a test-chosen elapsed duration, not by a hard-coded 61 minutes.
984
- // No scheduler event or writing action is dispatched at this moment.
985
- await saveRun({...run, startedAt: now - minutes * 60000,
986
- clockAdvancedAt: now, clockAdvanceMinutes: minutes,
987
- clockRunningProof: {first: first.reason, second: second.reason,
988
- userKey: run.newUserKey, observedAt: second.observedAt}});
989
- return {status: "ADVANCED_WHILE_RUNNING", minutes, tabId,
990
- firstReason: first.reason, secondReason: second.reason};
991
- }
992
670
  const isAction = (sender, message, run) => {
993
671
  if (!Number.isInteger(sender.tab?.id) || typeof sender.url !== "string") return false;
994
672
  try {
@@ -1006,21 +684,17 @@ const isAction = (sender, message, run) => {
1006
684
  async function onTake(message, sender) {
1007
685
  const run = await readRun();
1008
686
  if (!isAction(sender, message, run)) return {status: "DENIED", code: "ACTION_SOURCE_INVALID"};
1009
- if (run?.countMode && run.action === "CONTINUE" && !run.testSetup
1010
- && Date.now() < run.countLastSendAt + COUNT_INTERVAL_MS)
1011
- return {status:"DENIED",code:"COUNT_INTERVAL_NOT_DUE"};
1012
687
  if (run?.phase !== "RESERVED" || run.token !== message.token
1013
688
  || run.tabId !== message.tabId) return {status: "DENIED", code: "ACTION_NOT_RESERVED"};
1014
689
  const tab = await chrome.tabs.get(run.tabId).catch(() => null);
1015
- if (!tab || (run.testSetup ? tab.url !== "https://chatgpt.com/" : pathOf(tab.url) !== run.page)) {
690
+ if (!tab || pathOf(tab.url) !== run.page) {
1016
691
  await saveRun({...run, phase: "BLOCKED", reason: "CHAT_TARGET_CHANGED"});
1017
692
  return {status: "DENIED", code: "CHAT_TARGET_CHANGED"};
1018
693
  }
1019
694
  // Bind a model-control capability to the next *user* Turn, not to a tab or to another Chat.
1020
695
  // Acceptance fixtures retain their existing prompts; native Retry keeps its original Turn token.
1021
- const modelTurnToken = run.retryMode === "NATIVE" ? run.modelTurnToken || null
1022
- : run.testSetup || run.countMode || run.timedTest || run.wallTestHours
1023
- ? null : crypto.randomUUID();
696
+ const modelTurnToken = run.retryMode === "NATIVE"
697
+ ? run.modelTurnToken || null : crypto.randomUUID();
1024
698
  const pending={...run, phase: "PENDING", modelTurnToken};
1025
699
  await saveRun(run.modelIntent ? updateModelReceipt(pending,run.modelIntent.id,{
1026
700
  status:run.action==="HANDOFF"&&run.modelIntent.action==="CONTINUE"
@@ -1029,21 +703,16 @@ async function onTake(message, sender) {
1029
703
  modelTurnToken,
1030
704
  modelTurnNote: modelTurnToken ? turnToolNote(modelTurnToken) : "",
1031
705
  handoffMode: run.handoffMode || null, retryMode: run.retryMode || null,
1032
- countMode: run.countMode === true,
1033
- countValue: run.countMode ? run.countValue : null,
1034
706
  target: {tabId: run.tabId, page: run.page,
1035
707
  assistantKey: run.assistantKey,
1036
708
  userKey: run.retryMode === "NATIVE" || run.handoffMode === "ERROR"
1037
709
  ? run.newUserKey : run.userKey,
1038
- testSetup: run.testSetup === true, discardDraft: run.discardDraft === true},
710
+ },
1039
711
  modelIntent: run.action==="HANDOFF" && run.modelIntent?.action==="HANDOFF"
1040
712
  ? {id:run.modelIntent.id,reason:run.modelIntent.reason||""} : null,
1041
713
  pendingPlan: run.action==="HANDOFF" && run.modelIntent?.action==="CONTINUE"
1042
714
  ? {id:run.modelIntent.id,nextPrompt:run.modelIntent.nextPrompt} : null,
1043
- text: run.testSetup ? (run.countMode ? COUNT_SETUP_TEXT
1044
- : run.wallTestHours || run.wallTestMinutes === 10 ? ACTION_WALL_TEST_TEXT
1045
- : run.timedTest ? ACTION_TIMED_TEST_TEXT : ACTION_TEST_TEXT)
1046
- : run.action === "HANDOFF" ? run.handoffText || null
715
+ text: run.action === "HANDOFF" ? run.handoffText || null
1047
716
  : run.modelIntent?.action==="CONTINUE" ? (
1048
717
  "【Chat Loop 续接来源】上一轮由模型登记的 Continue 请求 " + run.modelIntent.id
1049
718
  + " 已被扩展唯一 Action 领取。\n"
@@ -1051,9 +720,7 @@ async function onTake(message, sender) {
1051
720
  + run.modelIntent.nextPrompt + "\n\n"
1052
721
  + "执行前核对当前项目状态、既有用户授权和已完成副作用,不重做不明操作。"
1053
722
  + "本轮完成后,末行只输出一个状态标记:[CHAT_LOOP_STATUS:CONTINUE]、[CHAT_LOOP_STATUS:DONE] 或 [CHAT_LOOP_STATUS:BLOCKED]。")
1054
- : run.countMode ? countText(run.countValue)
1055
- : run.timedTest && run.wallTestMinutes === 10
1056
- ? wallArithmeticText(run.testTurnIndex || 2) : CONTINUE_TEXT};
723
+ : CONTINUE_TEXT};
1057
724
  if (modelTurnToken && typeof permit.text === "string")
1058
725
  permit.text += permit.modelTurnNote;
1059
726
  return permit;
@@ -1111,31 +778,16 @@ async function onResult(message, sender) {
1111
778
  // its first Retry/Error Handoff using the OLD Chat's assistant ID.
1112
779
  userKey: null, assistantKey: null, startedAt: Date.now(),
1113
780
  errorHandoffCount: run.handoffMode === "ERROR" ? (run.errorHandoffCount || 0) + 1 : 0,
1114
- ...(run.countMode ? {countMode:true, countValue:run.countValue,
1115
- countHandoffs:(run.countHandoffs || 0) + 1,
1116
- countLastSendAt:Date.now()} : {})};
781
+ };
1117
782
  delete next.handoffText;
1118
783
  delete next.errorReason;
1119
784
  delete next.handoffPending;
1120
785
  delete next.handoffPendingAt;
1121
786
  delete next.deadlineAt;
1122
787
  }
1123
- } else if (confirmed && run.testSetup) {
1124
- const tab = await chrome.tabs.get(run.tabId).catch(() => null);
1125
- confirmed = Boolean(tab && pathOf(tab.url) && pathOf(tab.url) === message.newPage);
1126
- if (confirmed) {
1127
- next = {...run, page: message.newPage, phase: "WAITING_REPLY", reason: "",
1128
- newUserKey: message.newUserKey,
1129
- ...(run.wallTestMinutes === 10 ? {testTurnIndex: 1} : {}),
1130
- ...(run.countMode ? {countLastSendAt:Date.now()} : {}),
1131
- ...(run.wallTestHours ? {startedAt: Date.now()} : {})};
1132
- delete next.testSetup;
1133
- delete next.discardDraft;
1134
- }
1135
788
  } else if (confirmed) {
1136
789
  next = {...run, phase: "WAITING_REPLY", reason: "",
1137
- newUserKey: message.newUserKey,
1138
- ...(run.countMode ? {countLastSendAt:Date.now()} : {})};
790
+ newUserKey: message.newUserKey};
1139
791
  }
1140
792
  // A model prompt is one-shot. Keep only a short receipt, not the prompt
1141
793
  // body, after the Action has reached a confirmed or blocked terminal state.
@@ -1179,54 +831,6 @@ async function reconnectOpenChats() {
1179
831
  } catch (error) {console.warn("Chat Loop observer reconnect unavailable:", error?.message);}
1180
832
  }
1181
833
  }
1182
- async function releaseWallClockHold(run) {
1183
- const liveRun = await readRun();
1184
- if (liveRun?.phase !== "WALL_CLOCK_HOLD" || liveRun.tabId !== run?.tabId
1185
- || liveRun.wallHoldAt !== run?.wallHoldAt
1186
- || liveRun.userKey !== run?.userKey) return {status: "IGNORED", code: "HOLD_ALREADY_CHANGED"};
1187
- if (run?.phase !== "WALL_CLOCK_HOLD" || !([1, 2].includes(run.wallTestHours)
1188
- || run.wallTestMinutes === 10)
1189
- || !run.timedTest || !Number.isSafeInteger(run.wallReleaseAt)
1190
- || Date.now() < run.wallReleaseAt) return {status: "NOT_DUE"};
1191
- const owned = (await chrome.storage.local.get("chat-loop.acceptance.owned.v1"))[
1192
- "chat-loop.acceptance.owned.v1"];
1193
- if (!Array.isArray(owned) || !owned.some(t => t.id === run.tabId))
1194
- return {status: "DENIED", code: "NOT_TEST_OWNED"};
1195
- const tab = await chrome.tabs.get(run.tabId).catch(() => null);
1196
- if (!tab || pathOf(tab.url) !== run.page) {
1197
- await saveRun({...run, phase: "BLOCKED", reason: "WALL_CHAT_CHANGED"});
1198
- return {status: "BLOCKED", code: "WALL_CHAT_CHANGED"};
1199
- }
1200
- const verify = async () => {
1201
- const p = await chrome.tabs.sendMessage(run.tabId,
1202
- {type: "CHAT_LOOP_TURN_VERIFY_V1"}).catch(() => null);
1203
- return p?.contract === "chat-loop.turn-observer.v1" && p.status === "READY"
1204
- && p.page === run.page && p.userKey === run.userKey
1205
- && p.assistantKey === run.assistantKey
1206
- && globalThis.ChatLoopCoreV1.parseStatus(p.replyText) === "CONTINUE";
1207
- };
1208
- if (!await verify()) return {status: "UNVERIFIED", code: "WALL_TURN_NOT_STABLE"};
1209
- await new Promise(resolve => setTimeout(resolve, 2300));
1210
- if (!await verify()) return {status: "UNVERIFIED", code: "WALL_TURN_CHANGED"};
1211
- const latest = await readRun();
1212
- if (latest?.phase !== "WALL_CLOCK_HOLD" || latest.tabId !== run.tabId
1213
- || latest.userKey !== run.userKey || latest.wallHoldAt !== run.wallHoldAt)
1214
- return {status: "IGNORED", code: "HOLD_CHANGED_DURING_VERIFICATION"};
1215
- const token = crypto.randomUUID();
1216
- const next = {...run, phase: "RESERVED", action: "CONTINUE", token,
1217
- wallReleasedAt: Date.now(), reason: ""};
1218
- await saveRun(next); // reserve once, then Action page exclusively sends
1219
- try {
1220
- const action = await createBoundedAction({
1221
- url: chrome.runtime.getURL("continue.html") + "?tabId=" + run.tabId
1222
- + "&token=" + encodeURIComponent(token), active: false, windowId: tab.windowId
1223
- });
1224
- return {status: "QUEUED", actionTabId: action.id};
1225
- } catch {
1226
- await saveRun({...next, phase: "BLOCKED", reason: "WALL_ACTION_CREATE_FAILED"});
1227
- return {status: "BLOCKED", code: "WALL_ACTION_CREATE_FAILED"};
1228
- }
1229
- }
1230
834
  // MV3 background service workers can suspend, and hidden-page setInterval
1231
835
  // may be throttled. An alarm asks the Observer to sample, never acts directly.
1232
836
  async function ensureAlarm() {
@@ -1243,14 +847,6 @@ async function watchActiveTurn() {
1243
847
  await serial(async()=>{await expireStaleModelIntent();await prepareHandoffIfDue();});
1244
848
  const run = await readRun();
1245
849
  // While an Action is pending, never dispatch competing operations.
1246
- if (run?.phase === "WALL_CLOCK_HOLD") {
1247
- void serial(() => releaseWallClockHold(run));
1248
- return;
1249
- }
1250
- if (run?.phase === "WAITING_INTERVAL") {
1251
- void serial(releaseCountInterval);
1252
- return;
1253
- }
1254
850
  if (run && ["RESERVED", "PENDING"].includes(run.phase)) return;
1255
851
  const tabs = run?.phase === "WAITING_REPLY"
1256
852
  ? [await chrome.tabs.get(run.tabId).catch(() => null)]
@@ -1289,8 +885,7 @@ async function watchActiveTurn() {
1289
885
  chrome.alarms?.onAlarm?.addListener(alarm => {
1290
886
  if (alarm?.name === ALARM_NAME) void watchActiveTurn();
1291
887
  if (alarm?.name === DEADLINE_ALARM_NAME)
1292
- void serial(async () => {await prepareHandoffIfDue();await releaseCountInterval();});
1293
- if (alarm?.name === COUNT_ALARM_NAME) void serial(releaseCountInterval);
888
+ void serial(prepareHandoffIfDue);
1294
889
  });
1295
890
  chrome.runtime.onInstalled?.addListener(() => {
1296
891
  void ensureAlarm();
@@ -1356,34 +951,29 @@ async function onExtensionCliTarget(message,sender) {
1356
951
  return {status:"DENIED",code:"OWNER_CHANGED"};
1357
952
  return {status:"OK",tabId:run.tabId,page:run.page};
1358
953
  }
954
+ // One explicit router for authenticated, serialized state changes.
955
+ // The registered handler still verifies source, token, Owner and Turn identity.
956
+ const SERIAL_ROUTES=new Map([
957
+ ['CHAT_LOOP_TURN_FINISHED_V1',onTurnFinished],
958
+ ['CHAT_LOOP_TURN_BLOCKED_V1',onTurnBlocked],
959
+ ['CHAT_LOOP_ACTION_TAKE_V1',onTake],
960
+ ['CHAT_LOOP_ACTION_RESULT_V1',onResult],
961
+ ['CHAT_LOOP_ACTION_MANUAL_PREPARE_V1',onManualPrepare],
962
+ ['CHAT_LOOP_MODEL_INTENT_V1',onModelIntent],
963
+ ['CHAT_LOOP_MODEL_INTENT_STATUS_V1',onModelIntentStatus],
964
+ ['CHAT_LOOP_SESSION_START_PREPARE_V1',sessionStartPrepare],
965
+ ['CHAT_LOOP_SESSION_START_TAKE_V1',sessionStartTake],
966
+ ['CHAT_LOOP_SESSION_START_RESULT_V1',sessionStartResult],
967
+ ['CHAT_LOOP_SESSION_STATUS_V1',sessionStatus],
968
+ ['CHAT_LOOP_RESOURCE_CHAT_V1',resourceChatGate]
969
+
970
+ ]);
1359
971
  chrome.runtime.onMessage.addListener((message, sender, reply) => {
1360
- const type = message?.type;
1361
- if (["CHAT_LOOP_TURN_FINISHED_V1", "CHAT_LOOP_TURN_BLOCKED_V1",
1362
- "CHAT_LOOP_ACTION_TAKE_V1",
1363
- "CHAT_LOOP_ACTION_RESULT_V1", "CHAT_LOOP_ACTION_MANUAL_PREPARE_V1",
1364
- "CHAT_LOOP_MODEL_INTENT_V1", "CHAT_LOOP_MODEL_INTENT_STATUS_V1",
1365
- "CHAT_LOOP_ACTION_TEST_PREPARE_V1",
1366
- "CHAT_LOOP_SESSION_START_PREPARE_V1", "CHAT_LOOP_SESSION_START_TAKE_V1",
1367
- "CHAT_LOOP_SESSION_START_RESULT_V1", "CHAT_LOOP_SESSION_STATUS_V1",
1368
- "CHAT_LOOP_RESOURCE_CHAT_V1",
1369
- "CHAT_LOOP_ACCEPTANCE_CLOCK_ADVANCE_V1"].includes(type)) {
1370
- const work = serial(() => type === "CHAT_LOOP_RESOURCE_CHAT_V1"
1371
- ? resourceChatGate(message,sender)
1372
- : type === "CHAT_LOOP_SESSION_START_PREPARE_V1"
1373
- ? sessionStartPrepare(message,sender) : type === "CHAT_LOOP_SESSION_START_TAKE_V1"
1374
- ? sessionStartTake(message,sender) : type === "CHAT_LOOP_SESSION_START_RESULT_V1"
1375
- ? sessionStartResult(message,sender) : type === "CHAT_LOOP_SESSION_STATUS_V1"
1376
- ? sessionStatus(message,sender) : type === "CHAT_LOOP_MODEL_INTENT_V1"
1377
- ? onModelIntent(message, sender) : type === "CHAT_LOOP_MODEL_INTENT_STATUS_V1"
1378
- ? onModelIntentStatus(message,sender) : type === "CHAT_LOOP_TURN_FINISHED_V1"
1379
- ? onTurnFinished(message, sender) : type === "CHAT_LOOP_TURN_BLOCKED_V1"
1380
- ? onTurnBlocked(message, sender) : type === "CHAT_LOOP_ACTION_TAKE_V1"
1381
- ? onTake(message, sender) : type === "CHAT_LOOP_ACTION_MANUAL_PREPARE_V1"
1382
- ? onManualPrepare(message, sender) : type === "CHAT_LOOP_ACTION_TEST_PREPARE_V1"
1383
- ? onTestPrepare(message, sender)
1384
- : type === "CHAT_LOOP_ACCEPTANCE_CLOCK_ADVANCE_V1"
1385
- ? onAcceptanceClockAdvance(message, sender) : onResult(message, sender));
1386
- void work.then(reply, error => reply({status: "BLOCKED", code: error.message || "SCHEDULER_ERROR"}));
972
+ const type=message?.type;
973
+ const handle=SERIAL_ROUTES.get(type);
974
+ if(handle){
975
+ void serial(()=>handle(message,sender)).then(reply,
976
+ error=>reply({status:'BLOCKED',code:error.message||'SCHEDULER_ERROR'}));
1387
977
  return true;
1388
978
  }
1389
979
  if (type === 'CHAT_LOOP_POPUP_CLEAN_OPEN_V1') {