@tomflow/chatloop-extension 0.3.35 → 0.3.37

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/README.md CHANGED
@@ -30,7 +30,7 @@ Run: chatloop-extension extension <command> --help
30
30
  {"projectId":"new-distinct-test","prompt":"新的独立任务","confirmed":true,"replaceBlockedSessionId":"11111111-1111-4111-a111-111111111111"}
31
31
  ```
32
32
 
33
- `PAGE_ERROR` 且无原生 Retry 时,Worker 仅观察最多四次;只有原用户消息 ID 未变且连续两次证明错误消失才继续观察;若回查时已经识别到同一 Turn 的可靠完成事件或已发出完成事件,则让已排队的完成回执自然结项,避免 4 秒回查窗口错误覆盖 DONE。其余情况依旧 BLOCKED。**不能把当前网页错误消失当成原消息未发送的证据**。
33
+ `PAGE_ERROR` 且无原生 Retry 时,Worker 仅观察最多四次;只有原用户消息 ID 未变且连续两次证明错误消失才继续观察;若回查时已经识别到同一 Turn 的可靠完成事件或已发出完成事件,则让已排队的完成回执自然结项,避免 4 秒回查窗口错误覆盖 DONE。其余情况依旧 BLOCKED。正式 `popup.html?audit=1` Audit 包含最近一条真实已核验故障的 `run.lastFaultProof`(不含正文);若仅有页面告警而无此字段,不能宣称 Worker 实际进入了恢复分支。**不能把当前网页错误消失当成原消息未发送的证据**。
34
34
 
35
35
  Clean 只清空输入框,不发送,不更改 Owner;必须指定精确标签页 ID 且验证目标。Chrome 已安装扩展不随 npm 包自动安装。本地源码与已发布 npm 版本可能不同,真实结果请看 [CURRENT](../../context/CURRENT.md)。
36
36
 
@@ -55,7 +55,8 @@
55
55
  "clockAdvancedAt", "clockAdvanceMinutes", "clockRunningProof",
56
56
  "wallTestHours", "wallHoldAt", "wallReleaseAt", "wallReleasedAt",
57
57
  "wallTestMinutes", "wallAdjustedAt", "wallOriginalReleaseAt",
58
- "countMode", "countValue", "countHandoffs", "countLastSendAt", "countReleaseAt"]
58
+ "countMode", "countValue", "countHandoffs", "countLastSendAt", "countReleaseAt",
59
+ "lastFaultProof"]
59
60
  .filter(k => run[k] !== undefined).map(k => [k, run[k]])) : null
60
61
  };
61
62
  if (run?.parseEvidence && owned.some(item => item.id === run.tabId || item.id === run.sourceTabId)) {
@@ -319,6 +319,14 @@ async function onTurnBlocked(message, sender) {
319
319
  const second = await verify();
320
320
  if (!second || first.retryAvailable !== second.retryAvailable)
321
321
  return {status: "IGNORED", code: "ERROR_NOT_STABLE"};
322
+ // A bounded non-content receipt lets the existing Audit prove which
323
+ // decision the ONLY Worker actually took. Never log raw Chat text.
324
+ const faultProof = code => ({code, reason, userKey: message.userKey, at: Date.now()});
325
+ const recordFault = async code => {
326
+ await saveRun({...previous, lastFaultProof: faultProof(code)});
327
+ return {status: "IGNORED", code};
328
+ };
329
+ await saveRun({...previous, lastFaultProof: faultProof("ERROR_CONFIRMED")});
322
330
  // A network interruption may show a transient page error without a native
323
331
  // Retry button. Only *observe* for a bounded grace period: never resend an
324
332
  // uncertain user Turn. Resume normal observation only if the EXACT submitted
@@ -340,10 +348,10 @@ async function onTurnBlocked(message, sender) {
340
348
  // that the SAME user Turn recovered; do not invent/send another message.
341
349
  if (sameTurn && (proof.status === "TURN_FINISHED"
342
350
  || proof.status === "IDLE" && proof.reason === "TURN_ALREADY_EMITTED"))
343
- return {status: "IGNORED", code: "PAGE_ERROR_COMPLETION_QUEUED"};
351
+ return recordFault("PAGE_ERROR_COMPLETION_QUEUED");
344
352
  if (sameTurn && ["RUNNING", "UNKNOWN"].includes(proof.status)) {
345
353
  recovered++;
346
- if (recovered === 2) return {status: "IGNORED", code: "PAGE_ERROR_CLEARED_SAME_TURN"};
354
+ if (recovered === 2) return recordFault("PAGE_ERROR_CLEARED_SAME_TURN");
347
355
  } else recovered = 0;
348
356
  }
349
357
  }
@@ -351,6 +359,7 @@ async function onTurnBlocked(message, sender) {
351
359
  // or evade site access controls. Keep the original Chat and user Turn intact.
352
360
  if (reason === "SECURITY_CHALLENGE") {
353
361
  await saveRun({...previous, phase:"BLOCKED", reason,
362
+ lastFaultProof:faultProof("SECURITY_CHALLENGE"),
354
363
  errorUserKey:message.userKey, errorAt:Date.now()});
355
364
  return {status:"BLOCKED", code:reason};
356
365
  }
@@ -362,7 +371,8 @@ async function onTurnBlocked(message, sender) {
362
371
  const retry = reason === "PAGE_ERROR";
363
372
  const token = crypto.randomUUID();
364
373
  let next = {...previous, phase: "RESERVED", action: retry ? "CONTINUE" : "HANDOFF",
365
- token, reason: "", ...(retry ? {retryMode: "NATIVE", retryCount: 1}
374
+ token, reason: "", lastFaultProof:faultProof(retry ? "NATIVE_RETRY_ONCE" : "CONTEXT_HANDOFF"),
375
+ ...(retry ? {retryMode: "NATIVE", retryCount: 1}
366
376
  : {handoffMode: "ERROR", errorReason: reason})};
367
377
  if (previous.modelIntent) {
368
378
  next=updateModelReceipt(next,previous.modelIntent.id,{
@@ -380,9 +390,12 @@ async function onTurnBlocked(message, sender) {
380
390
  return {status: "BLOCKED", code: "ERROR_ACTION_CREATE_FAILED"};
381
391
  }
382
392
  }
383
- await saveRun({...previous, phase: "BLOCKED", reason: reason === "PAGE_ERROR"
393
+ const finalReason = reason === "PAGE_ERROR"
384
394
  ? previous.retryCount === 1 ? "PAGE_ERROR_AFTER_RETRY" : "PAGE_ERROR_NO_RETRY_BUTTON"
385
- : "CONTEXT_HANDOFF_REPEATED", errorUserKey: message.userKey, errorAt: Date.now()});
395
+ : "CONTEXT_HANDOFF_REPEATED";
396
+ await saveRun({...previous, phase:"BLOCKED", reason:finalReason,
397
+ lastFaultProof:faultProof(finalReason),
398
+ errorUserKey:message.userKey, errorAt:Date.now()});
386
399
  return {status: "BLOCKED", code: reason === "PAGE_ERROR" ? "PAGE_ERROR" : "CONTEXT_HANDOFF_REPEATED"};
387
400
  }
388
401
  // A model-driven request can only be queued against a currently RUNNING,
@@ -571,6 +584,7 @@ async function onTurnFinished(message, sender) {
571
584
  tabId, page: message.page, userKey: message.userKey,
572
585
  assistantKey: message.assistantKey, phase: "RESERVED", token,
573
586
  action: nextAction, startedAt, modelReceipts:receiptHistory(previous),
587
+ ...(previous?.lastFaultProof ? {lastFaultProof:previous.lastFaultProof} : {}),
574
588
  ...(countMode ? {countMode:true, countValue:verifiedCount,
575
589
  countLastSendAt:previous.countLastSendAt,
576
590
  countHandoffs:previous.countHandoffs || 0} : {}),
@@ -8,14 +8,19 @@
8
8
  const match = MARKER.exec(lines.at(-1)?.trim() || "");
9
9
  if (!match) return "UNKNOWN";
10
10
  const markers = text.match(ALL_MARKERS) || [];
11
- // A portable handoff packet may legitimately quote DONE/CONTINUE rules for
12
- // the next Chat. Only its final HANDOFF token must be unique; all other
13
- // statuses still require a single marker in the entire reply.
11
+ // Status is a protocol line, not any occurrence of its token in prose.
12
+ // A reply may describe a previous Turn as, e.g., "it ended with
13
+ // [CHAT_LOOP_STATUS:CONTINUE]" and still legitimately finish with DONE.
14
+ // Require exactly one standalone status line, at the end. Never accept
15
+ // two standalone statuses (even if they have the same value).
16
+ const standalone = lines.filter(line => MARKER.test(line.trim()));
17
+ // Historical HANDOFF packets may embed prior standalone statuses. Keep
18
+ // its compatibility rule while requiring only one HANDOFF token total.
14
19
  if (match[1] === "HANDOFF") {
15
20
  return markers.filter(v => v === "[CHAT_LOOP_STATUS:HANDOFF]").length === 1
16
21
  ? "HANDOFF" : "UNKNOWN";
17
22
  }
18
- if (markers.length !== 1) return "UNKNOWN";
23
+ if (standalone.length !== 1) return "UNKNOWN";
19
24
  return match[1];
20
25
  }
21
26
  function fingerprint(text) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Chat Loop Extension",
4
- "version": "0.3.35",
4
+ "version": "0.3.37",
5
5
  "description": "Chat Loop browser companion: Eye actions, Turn observer, Continue and Handoff Actions",
6
6
  "permissions": [
7
7
  "activeTab",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomflow/chatloop-extension",
3
- "version": "0.3.35",
3
+ "version": "0.3.37",
4
4
  "type": "module",
5
5
  "description": "Chat Loop Extension package: nine formal Action pages and its own native CLI bridge",
6
6
  "bin": {