@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 +1 -1
- package/extension/acceptance.js +2 -1
- package/extension/background.js +19 -5
- package/extension/loop/core.js +9 -4
- package/extension/manifest.json +1 -1
- package/package.json +1 -1
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
|
|
package/extension/acceptance.js
CHANGED
|
@@ -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)) {
|
package/extension/background.js
CHANGED
|
@@ -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
|
|
351
|
+
return recordFault("PAGE_ERROR_COMPLETION_QUEUED");
|
|
344
352
|
if (sameTurn && ["RUNNING", "UNKNOWN"].includes(proof.status)) {
|
|
345
353
|
recovered++;
|
|
346
|
-
if (recovered === 2) return
|
|
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: "",
|
|
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
|
-
|
|
393
|
+
const finalReason = reason === "PAGE_ERROR"
|
|
384
394
|
? previous.retryCount === 1 ? "PAGE_ERROR_AFTER_RETRY" : "PAGE_ERROR_NO_RETRY_BUTTON"
|
|
385
|
-
: "CONTEXT_HANDOFF_REPEATED"
|
|
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} : {}),
|
package/extension/loop/core.js
CHANGED
|
@@ -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
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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 (
|
|
23
|
+
if (standalone.length !== 1) return "UNKNOWN";
|
|
19
24
|
return match[1];
|
|
20
25
|
}
|
|
21
26
|
function fingerprint(text) {
|
package/extension/manifest.json
CHANGED