iterate-plugin 3.5.2 → 3.5.4
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 +3 -3
- package/README.zh-CN.md +3 -3
- package/dist/session-hooks.js +48 -4
- package/dist/skill-prompt.js +63 -29
- package/dist/tools/checkpoint.js +39 -5
- package/dist/tools/config.js +11 -2
- package/dist/tools/context.js +5 -1
- package/dist/tools/defense-events.js +48 -9
- package/dist/tools/defense-store.js +63 -5
- package/dist/tools/experience-bank.js +14 -11
- package/dist/tools/experience-store.js +79 -14
- package/dist/tools/fix.js +30 -10
- package/dist/tools/history.js +5 -3
- package/dist/tools/prune.js +197 -29
- package/dist/tools/quality-gate.js +3 -0
- package/dist/tools/quality-store.js +8 -2
- package/dist/tools/review.js +17 -3
- package/dist/tools/transcript.js +72 -15
- package/dist/tools/triage.js +35 -2
- package/dist/transcript.js +30 -7
- package/lib/client.js +89 -21
- package/lib/parse.js +27 -2
- package/package.json +9 -7
- package/src/client/index.ts +53 -8
- package/src/session-hooks.ts +50 -4
- package/src/skill-prompt.ts +63 -29
- package/src/tools/checkpoint.ts +45 -6
- package/src/tools/config.ts +11 -2
- package/src/tools/context.ts +5 -1
- package/src/tools/defense-events.ts +50 -9
- package/src/tools/defense-store.ts +64 -8
- package/src/tools/experience-bank.ts +14 -11
- package/src/tools/experience-store.ts +88 -23
- package/src/tools/fix.ts +32 -12
- package/src/tools/history.ts +5 -3
- package/src/tools/prune.ts +204 -28
- package/src/tools/quality-gate.ts +4 -0
- package/src/tools/quality-store.ts +7 -2
- package/src/tools/review.ts +16 -3
- package/src/tools/transcript.ts +71 -14
- package/src/tools/triage.ts +36 -2
- package/src/transcript.ts +32 -7
- package/src/types.ts +2 -0
package/dist/transcript.js
CHANGED
|
@@ -35,6 +35,10 @@ const MAX_TIMELINE = 500;
|
|
|
35
35
|
/** Max applied-fix records kept (newest wins; bounded so a long run cannot
|
|
36
36
|
* grow the manifest payload without limit). */
|
|
37
37
|
const MAX_FIXES = 200;
|
|
38
|
+
/** Max round number accepted by the builder. A model-authored/manifest-backed
|
|
39
|
+
* round value is attacker-influenced JSON: `roundStart(1e9)` / `snapshotConvergence`
|
|
40
|
+
* would otherwise preallocate arrays of that size and OOM the host. */
|
|
41
|
+
const MAX_ROUNDS = 1000;
|
|
38
42
|
/** Thresholds applied when reducing a string list under a cap. */
|
|
39
43
|
function clampStringList(source, cap) {
|
|
40
44
|
const out = [];
|
|
@@ -103,6 +107,7 @@ export class ReviewTranscriptBuilder {
|
|
|
103
107
|
round = 0;
|
|
104
108
|
maxRounds = 0;
|
|
105
109
|
active = true;
|
|
110
|
+
stoppedReason = null;
|
|
106
111
|
rounds = [];
|
|
107
112
|
convergence = [];
|
|
108
113
|
globalFindings = [];
|
|
@@ -154,17 +159,27 @@ export class ReviewTranscriptBuilder {
|
|
|
154
159
|
this.phases.push(n);
|
|
155
160
|
this.touch();
|
|
156
161
|
}
|
|
157
|
-
/**
|
|
158
|
-
|
|
162
|
+
/**
|
|
163
|
+
* End the run (stops the "active" pulsing in the UI). `reason` records WHY
|
|
164
|
+
* it ended, so a run stopped by max-rounds or validation is never mistaken
|
|
165
|
+
* for a live run NOR for a clean convergence.
|
|
166
|
+
*/
|
|
167
|
+
finish(reason) {
|
|
159
168
|
this.active = false;
|
|
169
|
+
if (typeof reason === 'string' && reason.trim())
|
|
170
|
+
this.stoppedReason = reason.trim();
|
|
160
171
|
this.touch();
|
|
161
172
|
}
|
|
162
173
|
/** Open a review round, capturing the current round index. */
|
|
163
174
|
roundStart(round, maxRounds) {
|
|
164
175
|
const r = typeof round === 'number' && Number.isFinite(round) ? Math.floor(round) : 1;
|
|
165
|
-
|
|
176
|
+
// Clamp to MAX_ROUNDS: `this.rounds.length < this.round` below preallocates
|
|
177
|
+
// an array of size `round` — an unbounded model-controlled value (e.g. 1e9)
|
|
178
|
+
// would OOM the host. Values above the cap are folded into the cap so the
|
|
179
|
+
// real rounds are never silently dropped.
|
|
180
|
+
this.round = Math.min(r > 0 ? r : 1, MAX_ROUNDS);
|
|
166
181
|
if (typeof maxRounds === 'number' && Number.isFinite(maxRounds) && maxRounds >= 0) {
|
|
167
|
-
this.maxRounds = Math.floor(maxRounds);
|
|
182
|
+
this.maxRounds = Math.min(Math.floor(maxRounds), MAX_ROUNDS);
|
|
168
183
|
}
|
|
169
184
|
while (this.rounds.length < this.round) {
|
|
170
185
|
this.rounds.push({ round: this.rounds.length + 1, threads: [] });
|
|
@@ -248,7 +263,10 @@ export class ReviewTranscriptBuilder {
|
|
|
248
263
|
// ─── Convergence (F2) ───────────────────────────────────────────────────
|
|
249
264
|
/** Record a round's new-finding count for the convergence series. */
|
|
250
265
|
snapshotConvergence(round, newCount) {
|
|
251
|
-
const
|
|
266
|
+
const rawR = typeof round === 'number' && Number.isFinite(round) ? Math.floor(round) : 1;
|
|
267
|
+
// Clamp (mirrors roundStart): the loop below preallocates `r` slots, so an
|
|
268
|
+
// unbounded model-controlled round number would OOM the host.
|
|
269
|
+
const r = Math.min(rawR > 0 ? rawR : 1, MAX_ROUNDS);
|
|
252
270
|
const n = typeof newCount === 'number' && Number.isFinite(newCount) ? newCount : 0;
|
|
253
271
|
while (this.convergence.length < r)
|
|
254
272
|
this.convergence.push(-1);
|
|
@@ -267,7 +285,9 @@ export class ReviewTranscriptBuilder {
|
|
|
267
285
|
this.fixes.push({
|
|
268
286
|
id,
|
|
269
287
|
timestamp: typeof record.timestamp === 'string' ? record.timestamp : isoNow(),
|
|
270
|
-
round: typeof record.round === 'number'
|
|
288
|
+
round: typeof record.round === 'number' && Number.isFinite(record.round)
|
|
289
|
+
? Math.floor(record.round)
|
|
290
|
+
: this.round,
|
|
271
291
|
file,
|
|
272
292
|
summary: typeof record.summary === 'string' ? record.summary : '',
|
|
273
293
|
linesAdded: typeof record.linesAdded === 'number' ? Math.floor(record.linesAdded) : 0,
|
|
@@ -313,7 +333,9 @@ export class ReviewTranscriptBuilder {
|
|
|
313
333
|
const type = typeof entry.type === 'string' ? entry.type : 'decision';
|
|
314
334
|
this.timeline.push({
|
|
315
335
|
timestamp: typeof entry.timestamp === 'string' ? entry.timestamp : isoNow(),
|
|
316
|
-
round: typeof entry.round === 'number'
|
|
336
|
+
round: typeof entry.round === 'number' && Number.isFinite(entry.round)
|
|
337
|
+
? Math.floor(entry.round)
|
|
338
|
+
: this.round,
|
|
317
339
|
type,
|
|
318
340
|
data: entry.data && typeof entry.data === 'object'
|
|
319
341
|
? entry.data
|
|
@@ -370,6 +392,7 @@ export class ReviewTranscriptBuilder {
|
|
|
370
392
|
checkpoint: this.checkpoint,
|
|
371
393
|
timeline: this.timeline,
|
|
372
394
|
nudge: this.nudge,
|
|
395
|
+
stoppedReason: this.stoppedReason,
|
|
373
396
|
approval: {
|
|
374
397
|
active: this.approval !== 'allow',
|
|
375
398
|
policy: this.approval,
|
package/lib/client.js
CHANGED
|
@@ -941,18 +941,25 @@ function computeConvergenceProgress(report) {
|
|
|
941
941
|
report.convergence ?? {}
|
|
942
942
|
);
|
|
943
943
|
const totalRounds = typeof convergence.totalRounds === "number" ? convergence.totalRounds : 1;
|
|
944
|
-
const currentRounds = (
|
|
945
|
-
/** @type {Array<unknown>} */
|
|
946
|
-
(report.rounds ?? []).length
|
|
947
|
-
);
|
|
944
|
+
const currentRounds = currentRoundNumber(report);
|
|
948
945
|
if (!(totalRounds > 0)) return 0;
|
|
949
946
|
return Math.min(100, Math.round(currentRounds / totalRounds * 100));
|
|
950
947
|
}
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
/** @type {Array<unknown
|
|
954
|
-
|
|
948
|
+
function currentRoundNumber(report) {
|
|
949
|
+
const rounds = (
|
|
950
|
+
/** @type {Array<Record<string, unknown>>} */
|
|
951
|
+
report.rounds ?? []
|
|
955
952
|
);
|
|
953
|
+
let max = 0;
|
|
954
|
+
for (const r of rounds) {
|
|
955
|
+
if (!r || typeof r !== "object") continue;
|
|
956
|
+
const n = Number(r.round);
|
|
957
|
+
if (Number.isFinite(n) && n > max) max = n;
|
|
958
|
+
}
|
|
959
|
+
return rounds.length > 0 && max === 0 ? rounds.length : max;
|
|
960
|
+
}
|
|
961
|
+
function getCurrentRound(report) {
|
|
962
|
+
return currentRoundNumber(report);
|
|
956
963
|
}
|
|
957
964
|
function getTotalRounds(report) {
|
|
958
965
|
const convergence = (
|
|
@@ -1283,7 +1290,10 @@ function buildRuntimeStatusGuide() {
|
|
|
1283
1290
|
...RUNTIME_ARTIFACTS.map((a) => `- ${a.key}\uFF08${a.label}\uFF09\uFF1A${a.hint}`),
|
|
1284
1291
|
"",
|
|
1285
1292
|
"\u67E5\u770B\u72B6\u6001\uFF1A\u8BA9\u6A21\u578B\u8C03\u7528 iterate_status\uFF08\u6C47\u603B\uFF09\u6216 iterate_history\uFF08\u660E\u7EC6\uFF09\u3002",
|
|
1286
|
-
"\u6E05\u7406\u72B6\u6001\uFF1A\u8BA9\u6A21\u578B\u8C03\u7528 iterate_prune\uFF08\u9ED8\u8BA4 dry-run\uFF0C\u53EA\u62A5\u544A\u4E0D\u5220\u9664\uFF0C\u663E\u5F0F dryRun:false \u624D\u771F\u6B63\u6E05\u7406\uFF09\u3002"
|
|
1293
|
+
"\u6E05\u7406\u72B6\u6001\uFF1A\u8BA9\u6A21\u578B\u8C03\u7528 iterate_prune\uFF08\u9ED8\u8BA4 dry-run\uFF0C\u53EA\u62A5\u544A\u4E0D\u5220\u9664\uFF0C\u663E\u5F0F dryRun:false \u624D\u771F\u6B63\u6E05\u7406\uFF09\u3002",
|
|
1294
|
+
'\u91CD\u7F6E\u72B6\u6001\uFF1A\u4E2D\u65AD\u540E\u91CD\u65B0\u5F00\u59CB\u524D\uFF0C\u53EF\u5206\u522B\u8BA9\u6A21\u578B\u8C03\u7528 iterate_checkpoint(operation:"clear")\u3001',
|
|
1295
|
+
' iterate_quality_gate(operation:"clear")\u3001iterate_defense_events(operation:"clear")',
|
|
1296
|
+
" \u6E05\u9664\u9648\u65E7\u65AD\u70B9\u3001\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66\u4E0E\u9632\u5FA1\u4E8B\u4EF6\u6D41\u3002"
|
|
1287
1297
|
];
|
|
1288
1298
|
return lines.join("\n");
|
|
1289
1299
|
}
|
|
@@ -1710,6 +1720,25 @@ function severityColor(severity) {
|
|
|
1710
1720
|
function severityLabel(severity) {
|
|
1711
1721
|
return SEVERITY_LABEL[coerceSeverity(severity)];
|
|
1712
1722
|
}
|
|
1723
|
+
function stoppedReasonLabel(reason) {
|
|
1724
|
+
switch (reason) {
|
|
1725
|
+
case "converged":
|
|
1726
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u5DF2\u6536\u655B\uFF08\u65E0\u65B0\u53D1\u73B0\uFF09";
|
|
1727
|
+
case "max_rounds_reached":
|
|
1728
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u8FBE\u5230\u8F6E\u6570\u4E0A\u9650";
|
|
1729
|
+
case "aborted_by_validation":
|
|
1730
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u5931\u8D25\u540E\u56DE\u6EDA\u505C\u6B62";
|
|
1731
|
+
case "aborted_by_config":
|
|
1732
|
+
return "\u5DF2\u7ED3\u675F \xB7 \u9A8C\u8BC1\u547D\u4EE4\u4E0D\u5728\u767D\u540D\u5355\uFF08\u914D\u7F6E\u9700\u4FEE\u590D\uFF09";
|
|
1733
|
+
default:
|
|
1734
|
+
return reason ? `\u5DF2\u7ED3\u675F \xB7 ${reason}` : "\u5DF2\u7ED3\u675F";
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function runStatusText(live, manifest) {
|
|
1738
|
+
if (live) return "\u8FD0\u884C\u4E2D";
|
|
1739
|
+
const reason = manifest && typeof manifest.stoppedReason === "string" ? manifest.stoppedReason : "";
|
|
1740
|
+
return reason ? stoppedReasonLabel(reason) : "\u5DF2\u7ED3\u675F";
|
|
1741
|
+
}
|
|
1713
1742
|
function readSlots(ctx) {
|
|
1714
1743
|
if (ctx && ctx.slots && typeof ctx.slots.inject === "function" && typeof ctx.slots.register === "function") {
|
|
1715
1744
|
return ctx.slots;
|
|
@@ -1855,7 +1884,7 @@ function ConvergenceDashboard(props) {
|
|
|
1855
1884
|
key: "phase",
|
|
1856
1885
|
"data-live": transcript.active === true ? "" : void 0,
|
|
1857
1886
|
title: transcript.active === true ? `\u5F53\u524D\u5904\u4E8E\u300C${phase}\u300D\u9636\u6BB5\uFF08\u8FD0\u884C\u4E2D\uFF09` : `\u6700\u8FD1\u4E00\u6B21\u6267\u884C\u505C\u7559\u5728\u300C${phase}\u300D\u9636\u6BB5\uFF08\u5DF2\u7ED3\u675F\uFF09`
|
|
1858
|
-
}, `${phase} \xB7 ${transcript.active === true
|
|
1887
|
+
}, `${phase} \xB7 ${runStatusText(transcript.active === true, transcript)}`) : null;
|
|
1859
1888
|
const taskModeTranscript = latestTranscript(session);
|
|
1860
1889
|
const taskMode = taskModeTranscript?.taskMode;
|
|
1861
1890
|
const taskModeChip = taskMode ? React.createElement("span", {
|
|
@@ -3095,6 +3124,11 @@ ${JSON.stringify({
|
|
|
3095
3124
|
mode: String(cp.mode || "normal"),
|
|
3096
3125
|
maxRounds: typeof cp.maxRounds === "number" ? cp.maxRounds : null
|
|
3097
3126
|
}, null, 2)}
|
|
3127
|
+
\`\`\``;
|
|
3128
|
+
const clearText = `\u8BF7\u8C03\u7528 \`iterate_checkpoint\` \u6E05\u9664\u5F53\u524D\u65AD\u70B9\uFF1A
|
|
3129
|
+
|
|
3130
|
+
\`\`\`json
|
|
3131
|
+
${JSON.stringify({ operation: "clear" }, null, 2)}
|
|
3098
3132
|
\`\`\``;
|
|
3099
3133
|
const item = (label, value) => React.createElement("span", { className: "iterate-obs-chip" }, `${label} ${String(value ?? "?")}`);
|
|
3100
3134
|
return React.createElement(
|
|
@@ -3118,13 +3152,24 @@ ${JSON.stringify({
|
|
|
3118
3152
|
item("fixed", cp.fixedCount),
|
|
3119
3153
|
item("resume", cp.resumeCount)
|
|
3120
3154
|
),
|
|
3121
|
-
React.createElement(
|
|
3122
|
-
|
|
3123
|
-
"
|
|
3124
|
-
"
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3155
|
+
React.createElement(
|
|
3156
|
+
"div",
|
|
3157
|
+
{ className: "iterate-obs-bar", style: { marginBottom: 4 } },
|
|
3158
|
+
React.createElement("button", {
|
|
3159
|
+
className: "iterate-btn",
|
|
3160
|
+
"data-primary": "",
|
|
3161
|
+
"data-copied": copiedKey === "cp-resume" ? "" : void 0,
|
|
3162
|
+
onClick: () => copyInstruction("cp-resume", resumeText),
|
|
3163
|
+
title: "\u590D\u5236 iterate_checkpoint resume \u6307\u4EE4\u6587\u672C\uFF08\u52A0\u8F7D\u65AD\u70B9\u5E76\u8BA1\u6570\u4E00\u6B21\u6062\u590D\uFF09"
|
|
3164
|
+
}, copiedKey === "cp-resume" ? "\u5DF2\u590D\u5236" : "\u590D\u5236\u6062\u590D\u6307\u4EE4"),
|
|
3165
|
+
React.createElement("button", {
|
|
3166
|
+
className: "iterate-btn",
|
|
3167
|
+
"data-danger": "",
|
|
3168
|
+
"data-copied": copiedKey === "cp-clear" ? "" : void 0,
|
|
3169
|
+
onClick: () => copyInstruction("cp-clear", clearText),
|
|
3170
|
+
title: "\u590D\u5236 iterate_checkpoint clear \u6307\u4EE4\u6587\u672C\uFF08\u6E05\u9664\u5F53\u524D\u65AD\u70B9\uFF0C\u4F9B\u91CD\u65B0\u5F00\u59CB\uFF09"
|
|
3171
|
+
}, copiedKey === "cp-clear" ? "\u5DF2\u590D\u5236" : "\u6E05\u9664\u65AD\u70B9")
|
|
3172
|
+
)
|
|
3128
3173
|
)
|
|
3129
3174
|
);
|
|
3130
3175
|
};
|
|
@@ -3152,7 +3197,7 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
|
|
|
3152
3197
|
"div",
|
|
3153
3198
|
{ className: "iterate-obs-block-head" },
|
|
3154
3199
|
React.createElement("span", {}, "\u8FD0\u884C\u63A7\u5236\u53F0"),
|
|
3155
|
-
React.createElement("span", { className: "iterate-obs-badge", "data-live": live ? "" : void 0 }, live
|
|
3200
|
+
React.createElement("span", { className: "iterate-obs-badge", "data-live": live ? "" : void 0 }, runStatusText(live, manifest)),
|
|
3156
3201
|
React.createElement("span", { className: "iterate-obs-head-meta" }, `\u6279\u51C6\u7B56\u7565\uFF1A${policyLabel}`)
|
|
3157
3202
|
),
|
|
3158
3203
|
React.createElement(
|
|
@@ -3293,6 +3338,11 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
|
|
|
3293
3338
|
};
|
|
3294
3339
|
const renderQualityGate = () => {
|
|
3295
3340
|
const gateInstruction = "\u8BF7\u8C03\u7528 `iterate_quality_gate` \u67E5\u8BE2\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u72B6\u6001";
|
|
3341
|
+
const gateClearInstruction = `\u8BF7\u8C03\u7528 \`iterate_quality_gate\` \u6E05\u9664\u5F53\u524D\u8D28\u91CF\u95E8\u7981\u8BC1\u4E66\uFF1A
|
|
3342
|
+
|
|
3343
|
+
\`\`\`json
|
|
3344
|
+
${JSON.stringify({ operation: "clear" }, null, 2)}
|
|
3345
|
+
\`\`\``;
|
|
3296
3346
|
const gate = qualityGate;
|
|
3297
3347
|
const dims = gate && gate.dimensions ? gate.dimensions : [];
|
|
3298
3348
|
const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0);
|
|
@@ -3309,7 +3359,14 @@ ${JSON.stringify({ operation: "nudge", text: null }, null, 2)}
|
|
|
3309
3359
|
"data-copied": copiedKey === "qgate" ? "" : void 0,
|
|
3310
3360
|
onClick: () => copyInstruction("qgate", gateInstruction),
|
|
3311
3361
|
title: "\u590D\u5236 iterate_quality_gate \u67E5\u8BE2\u6307\u4EE4"
|
|
3312
|
-
}, copiedKey === "qgate" ? "\u5DF2\u590D\u5236" : "\u67E5\u8BE2\u95E8\u7981")
|
|
3362
|
+
}, copiedKey === "qgate" ? "\u5DF2\u590D\u5236" : "\u67E5\u8BE2\u95E8\u7981"),
|
|
3363
|
+
React.createElement("button", {
|
|
3364
|
+
className: "iterate-btn",
|
|
3365
|
+
"data-danger": "",
|
|
3366
|
+
"data-copied": copiedKey === "qgate-clear" ? "" : void 0,
|
|
3367
|
+
onClick: () => copyInstruction("qgate-clear", gateClearInstruction),
|
|
3368
|
+
title: "\u590D\u5236 iterate_quality_gate clear \u6307\u4EE4\u6587\u672C\uFF08\u91CD\u7F6E\u9648\u65E7\u7684 FAIL \u8BC1\u4E66\uFF09"
|
|
3369
|
+
}, copiedKey === "qgate-clear" ? "\u5DF2\u590D\u5236" : "\u6E05\u9664\u95E8\u7981")
|
|
3313
3370
|
);
|
|
3314
3371
|
if (!hasGate) {
|
|
3315
3372
|
return React.createElement(
|
|
@@ -3524,7 +3581,18 @@ ${JSON.stringify({ operation: "list", type: defenseFilter || void 0 }, null, 2)}
|
|
|
3524
3581
|
"data-copied": copiedKey === "defense-counts" ? "" : void 0,
|
|
3525
3582
|
onClick: () => copyInstruction("defense-counts", "\u8BF7\u8C03\u7528 `iterate_defense_events` \u67E5\u8BE2\u4E8B\u4EF6\u7EDF\u8BA1"),
|
|
3526
3583
|
title: "\u590D\u5236\u7EDF\u8BA1\u6307\u4EE4"
|
|
3527
|
-
}, copiedKey === "defense-counts" ? "\u5DF2\u590D\u5236" : "\u7EDF\u8BA1")
|
|
3584
|
+
}, copiedKey === "defense-counts" ? "\u5DF2\u590D\u5236" : "\u7EDF\u8BA1"),
|
|
3585
|
+
React.createElement("button", {
|
|
3586
|
+
className: "iterate-btn",
|
|
3587
|
+
"data-danger": "",
|
|
3588
|
+
"data-copied": copiedKey === "defense-clear" ? "" : void 0,
|
|
3589
|
+
onClick: () => copyInstruction("defense-clear", `\u8BF7\u8C03\u7528 \`iterate_defense_events\` \u6E05\u9664\u9632\u5FA1\u4E8B\u4EF6\u6D41\uFF1A
|
|
3590
|
+
|
|
3591
|
+
\`\`\`json
|
|
3592
|
+
${JSON.stringify({ operation: "clear" }, null, 2)}
|
|
3593
|
+
\`\`\``),
|
|
3594
|
+
title: "\u590D\u5236 iterate_defense_events clear \u6307\u4EE4\u6587\u672C\uFF08\u91CD\u7F6E\u9648\u65E7\u4E8B\u4EF6\u6D41\uFF09"
|
|
3595
|
+
}, copiedKey === "defense-clear" ? "\u5DF2\u590D\u5236" : "\u6E05\u9664\u4E8B\u4EF6")
|
|
3528
3596
|
);
|
|
3529
3597
|
if (allEvents.length === 0 && totalCount === 0) {
|
|
3530
3598
|
return React.createElement(
|
|
@@ -3725,7 +3793,7 @@ ${JSON.stringify({ operation: "list", type: defenseFilter || void 0 }, null, 2)}
|
|
|
3725
3793
|
"div",
|
|
3726
3794
|
{ className: "iterate-obs-head", "data-closed": open ? void 0 : "", onClick: () => setOpen((v) => !v) },
|
|
3727
3795
|
React.createElement("span", { className: "iterate-obs-title" }, "iterate \u89C2\u6D4B\u53F0"),
|
|
3728
|
-
React.createElement("span", { className: "iterate-obs-badge", "data-live": live ? "" : void 0 }, live
|
|
3796
|
+
React.createElement("span", { className: "iterate-obs-badge", "data-live": live ? "" : void 0 }, runStatusText(live, manifest)),
|
|
3729
3797
|
React.createElement("span", { className: "iterate-obs-head-meta" }, headMeta || "runtime"),
|
|
3730
3798
|
React.createElement("button", {
|
|
3731
3799
|
className: "iterate-btn",
|
package/lib/parse.js
CHANGED
|
@@ -1217,12 +1217,34 @@ export function computeConvergenceProgress(report) {
|
|
|
1217
1217
|
const totalRounds = typeof convergence.totalRounds === 'number'
|
|
1218
1218
|
? convergence.totalRounds
|
|
1219
1219
|
: 1
|
|
1220
|
-
const currentRounds =
|
|
1220
|
+
const currentRounds = currentRoundNumber(report)
|
|
1221
1221
|
// Guard against an empty report (totalRounds <= 0) producing NaN.
|
|
1222
1222
|
if (!(totalRounds > 0)) return 0
|
|
1223
1223
|
return Math.min(100, Math.round((currentRounds / totalRounds) * 100))
|
|
1224
1224
|
}
|
|
1225
1225
|
|
|
1226
|
+
/**
|
|
1227
|
+
* The actual current round (1-indexed) carried by a report. Uses the highest
|
|
1228
|
+
* per-round `round` field: a normal-mode aggregate ships ONLY the live round
|
|
1229
|
+
* (`rounds: [{round: r, ...}]`), so `rounds.length` would be stuck at 1 while
|
|
1230
|
+
* the run is on round 3. Dry-run aggregates ship the cumulative list, where
|
|
1231
|
+
* the round numbers and the array length agree anyway.
|
|
1232
|
+
*
|
|
1233
|
+
* @param {Record<string, unknown>} report
|
|
1234
|
+
* @returns {number}
|
|
1235
|
+
*/
|
|
1236
|
+
function currentRoundNumber(report) {
|
|
1237
|
+
const rounds = /** @type {Array<Record<string, unknown>>} */ (report.rounds ?? [])
|
|
1238
|
+
let max = 0
|
|
1239
|
+
for (const r of rounds) {
|
|
1240
|
+
if (!r || typeof r !== 'object') continue
|
|
1241
|
+
const n = Number(r.round)
|
|
1242
|
+
if (Number.isFinite(n) && n > max) max = n
|
|
1243
|
+
}
|
|
1244
|
+
// Fallback: no usable round numbers → array length (covers all-0 rounds).
|
|
1245
|
+
return rounds.length > 0 && max === 0 ? rounds.length : max
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1226
1248
|
/**
|
|
1227
1249
|
* Get the current round number (1-indexed) from a report.
|
|
1228
1250
|
*
|
|
@@ -1230,7 +1252,7 @@ export function computeConvergenceProgress(report) {
|
|
|
1230
1252
|
* @returns {number}
|
|
1231
1253
|
*/
|
|
1232
1254
|
export function getCurrentRound(report) {
|
|
1233
|
-
return (
|
|
1255
|
+
return currentRoundNumber(report)
|
|
1234
1256
|
}
|
|
1235
1257
|
|
|
1236
1258
|
/**
|
|
@@ -1816,6 +1838,9 @@ export function buildRuntimeStatusGuide() {
|
|
|
1816
1838
|
'',
|
|
1817
1839
|
'查看状态:让模型调用 iterate_status(汇总)或 iterate_history(明细)。',
|
|
1818
1840
|
'清理状态:让模型调用 iterate_prune(默认 dry-run,只报告不删除,显式 dryRun:false 才真正清理)。',
|
|
1841
|
+
'重置状态:中断后重新开始前,可分别让模型调用 iterate_checkpoint(operation:"clear")、',
|
|
1842
|
+
' iterate_quality_gate(operation:"clear")、iterate_defense_events(operation:"clear")',
|
|
1843
|
+
' 清除陈旧断点、质量门禁证书与防御事件流。',
|
|
1819
1844
|
]
|
|
1820
1845
|
return lines.join('\n')
|
|
1821
1846
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iterate-plugin",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.4",
|
|
4
4
|
"description": "dsh plugin that turns the iterate skill into an autonomous closed-loop harness with quality command center and experience bank (v3.3). Features: plan -> parallel review xN -> atomic fixes -> validate -> loop -> auto-stop, plus dry-run pure-review mode, quality gate compute/persist, writable experience bank, defense events stream (record + bilingual labels), and native command buttons — with a live quality command center (F8/F9/F10 render real session data), §8 assign-fix instruction, and task_mode indicator wired end to end.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"0.1.5-alpha.1": "compatible",
|
|
38
38
|
"0.1.5-alpha.2": "compatible",
|
|
39
39
|
"0.1.5-rc.1": "compatible",
|
|
40
|
-
"0.1.5-rc.2": "compatible"
|
|
40
|
+
"0.1.5-rc.2": "compatible",
|
|
41
|
+
"0.1.6-alpha.1": "compatible"
|
|
41
42
|
}
|
|
42
43
|
},
|
|
43
44
|
"client": {
|
|
@@ -75,13 +76,14 @@
|
|
|
75
76
|
},
|
|
76
77
|
"dependencies": {
|
|
77
78
|
"@deepseek-ai/cordis": "4.0.2",
|
|
78
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
79
|
-
"@deepseek-ai/dsh-util-values": "0.1.
|
|
80
|
-
"js-yaml": "4.3.
|
|
79
|
+
"@deepseek-ai/dsh-tools": "0.1.6-alpha.1",
|
|
80
|
+
"@deepseek-ai/dsh-util-values": "0.1.6-alpha.1",
|
|
81
|
+
"js-yaml": "4.3.2"
|
|
81
82
|
},
|
|
82
83
|
"devDependencies": {
|
|
83
|
-
"@deepseek-ai/dsh-
|
|
84
|
-
"@deepseek-ai/dsh-
|
|
84
|
+
"@deepseek-ai/dsh-agent": "0.1.6-alpha.1",
|
|
85
|
+
"@deepseek-ai/dsh-jobs": "^0.1.6-alpha.1",
|
|
86
|
+
"@deepseek-ai/dsh-session": "0.1.6-alpha.1",
|
|
85
87
|
"@types/js-yaml": "4.0.9",
|
|
86
88
|
"@types/node": "22.15.0",
|
|
87
89
|
"@types/react": "19.2.2",
|
package/src/client/index.ts
CHANGED
|
@@ -244,6 +244,8 @@ interface ObsManifest {
|
|
|
244
244
|
timeline?: ObsEntry[]
|
|
245
245
|
nudge?: ObsNudge | null
|
|
246
246
|
approval?: ObsApproval
|
|
247
|
+
/** Why the run ended: converged / max_rounds_reached / aborted_by_validation. */
|
|
248
|
+
stoppedReason?: string | null
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
// ─── Quality command center data types (v3.1+; mirrors src/types.ts) ────────
|
|
@@ -735,6 +737,24 @@ function severityLabel(severity: string | undefined): string {
|
|
|
735
737
|
return SEVERITY_LABEL[coerceSeverity(severity)]
|
|
736
738
|
}
|
|
737
739
|
|
|
740
|
+
/** Human label for WHY a finished run stopped (transcript manifest). */
|
|
741
|
+
function stoppedReasonLabel(reason: string | null | undefined): string {
|
|
742
|
+
switch (reason) {
|
|
743
|
+
case 'converged': return '已结束 · 已收敛(无新发现)'
|
|
744
|
+
case 'max_rounds_reached': return '已结束 · 达到轮数上限'
|
|
745
|
+
case 'aborted_by_validation': return '已结束 · 验证失败后回滚停止'
|
|
746
|
+
case 'aborted_by_config': return '已结束 · 验证命令不在白名单(配置需修复)'
|
|
747
|
+
default: return reason ? `已结束 · ${reason}` : '已结束'
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** Badge text for the run's live/finished state, annotated with the stop reason. */
|
|
752
|
+
function runStatusText(live: boolean, manifest: ObsManifest | null | undefined): string {
|
|
753
|
+
if (live) return '运行中'
|
|
754
|
+
const reason = manifest && typeof manifest.stoppedReason === 'string' ? manifest.stoppedReason : ''
|
|
755
|
+
return reason ? stoppedReasonLabel(reason) : '已结束'
|
|
756
|
+
}
|
|
757
|
+
|
|
738
758
|
/** Resolve the slots service: prefer the typed `ctx.slots`, fall back to `ctx.get`. */
|
|
739
759
|
function readSlots(ctx: ClientContext): SlotsService | undefined {
|
|
740
760
|
if (ctx && ctx.slots && typeof ctx.slots.inject === 'function' && typeof ctx.slots.register === 'function') {
|
|
@@ -940,7 +960,7 @@ function ConvergenceDashboard(props: SlotProps) {
|
|
|
940
960
|
title: transcript.active === true
|
|
941
961
|
? `当前处于「${phase}」阶段(运行中)`
|
|
942
962
|
: `最近一次执行停留在「${phase}」阶段(已结束)`,
|
|
943
|
-
}, `${phase} · ${transcript.active === true
|
|
963
|
+
}, `${phase} · ${runStatusText(transcript.active === true, transcript)}`)
|
|
944
964
|
: null
|
|
945
965
|
|
|
946
966
|
// v3.0: task_mode indicator (code/iterate)
|
|
@@ -2172,6 +2192,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2172
2192
|
mode: String(cp.mode || 'normal'),
|
|
2173
2193
|
maxRounds: typeof cp.maxRounds === 'number' ? cp.maxRounds : null,
|
|
2174
2194
|
}, null, 2)}\n\`\`\``
|
|
2195
|
+
// Clearing a stale checkpoint after an interrupted run — iterate_checkpoint
|
|
2196
|
+
// exposes `clear`, but before this button it was only reachable by typing
|
|
2197
|
+
// the command manually (an interrupted run otherwise shows a permanent
|
|
2198
|
+
// "可恢复" chip with no companion reset action).
|
|
2199
|
+
const clearText = `请调用 \`iterate_checkpoint\` 清除当前断点:\n\n\`\`\`json\n${JSON.stringify({ operation: 'clear' }, null, 2)}\n\`\`\``
|
|
2175
2200
|
const item = (label: string, value: unknown) =>
|
|
2176
2201
|
React.createElement('span', { className: 'iterate-obs-chip' }, `${label} ${String(value ?? '?')}`)
|
|
2177
2202
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
@@ -2187,11 +2212,18 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2187
2212
|
item('fixed', cp.fixedCount),
|
|
2188
2213
|
item('resume', cp.resumeCount),
|
|
2189
2214
|
),
|
|
2190
|
-
React.createElement('
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2215
|
+
React.createElement('div', { className: 'iterate-obs-bar', style: { marginBottom: 4 } },
|
|
2216
|
+
React.createElement('button', {
|
|
2217
|
+
className: 'iterate-btn', 'data-primary': '', 'data-copied': copiedKey === 'cp-resume' ? '' : undefined,
|
|
2218
|
+
onClick: () => copyInstruction('cp-resume', resumeText),
|
|
2219
|
+
title: '复制 iterate_checkpoint resume 指令文本(加载断点并计数一次恢复)',
|
|
2220
|
+
}, copiedKey === 'cp-resume' ? '已复制' : '复制恢复指令'),
|
|
2221
|
+
React.createElement('button', {
|
|
2222
|
+
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'cp-clear' ? '' : undefined,
|
|
2223
|
+
onClick: () => copyInstruction('cp-clear', clearText),
|
|
2224
|
+
title: '复制 iterate_checkpoint clear 指令文本(清除当前断点,供重新开始)',
|
|
2225
|
+
}, copiedKey === 'cp-clear' ? '已复制' : '清除断点'),
|
|
2226
|
+
),
|
|
2195
2227
|
),
|
|
2196
2228
|
)
|
|
2197
2229
|
}
|
|
@@ -2212,7 +2244,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2212
2244
|
return React.createElement('div', { className: 'iterate-obs-block' },
|
|
2213
2245
|
React.createElement('div', { className: 'iterate-obs-block-head' },
|
|
2214
2246
|
React.createElement('span', {}, '运行控制台'),
|
|
2215
|
-
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, live
|
|
2247
|
+
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
2216
2248
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, `批准策略:${policyLabel}`),
|
|
2217
2249
|
),
|
|
2218
2250
|
React.createElement('div', { className: 'iterate-obs-block-body' },
|
|
@@ -2338,6 +2370,9 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2338
2370
|
// falls back to copy-able query instructions when no snapshot exists yet.
|
|
2339
2371
|
const renderQualityGate = () => {
|
|
2340
2372
|
const gateInstruction = '请调用 `iterate_quality_gate` 查询当前质量门禁状态'
|
|
2373
|
+
// Reset a stale FAIL certificate before a fresh iteration — the tool's
|
|
2374
|
+
// `clear` operation existed since v3.5.1 but had no client entry point.
|
|
2375
|
+
const gateClearInstruction = `请调用 \`iterate_quality_gate\` 清除当前质量门禁证书:\n\n\`\`\`json\n${JSON.stringify({ operation: 'clear' }, null, 2)}\n\`\`\``
|
|
2341
2376
|
const gate = qualityGate
|
|
2342
2377
|
const dims = gate && gate.dimensions ? gate.dimensions : []
|
|
2343
2378
|
const hasGate = gate !== null && Boolean(gate.overallStatus || gate.overallScore != null || dims.length > 0)
|
|
@@ -2352,6 +2387,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2352
2387
|
onClick: () => copyInstruction('qgate', gateInstruction),
|
|
2353
2388
|
title: '复制 iterate_quality_gate 查询指令',
|
|
2354
2389
|
}, copiedKey === 'qgate' ? '已复制' : '查询门禁'),
|
|
2390
|
+
React.createElement('button', {
|
|
2391
|
+
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'qgate-clear' ? '' : undefined,
|
|
2392
|
+
onClick: () => copyInstruction('qgate-clear', gateClearInstruction),
|
|
2393
|
+
title: '复制 iterate_quality_gate clear 指令文本(重置陈旧的 FAIL 证书)',
|
|
2394
|
+
}, copiedKey === 'qgate-clear' ? '已复制' : '清除门禁'),
|
|
2355
2395
|
)
|
|
2356
2396
|
|
|
2357
2397
|
if (!hasGate) {
|
|
@@ -2538,6 +2578,11 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2538
2578
|
onClick: () => copyInstruction('defense-counts', '请调用 `iterate_defense_events` 查询事件统计'),
|
|
2539
2579
|
title: '复制统计指令',
|
|
2540
2580
|
}, copiedKey === 'defense-counts' ? '已复制' : '统计'),
|
|
2581
|
+
React.createElement('button', {
|
|
2582
|
+
className: 'iterate-btn', 'data-danger': '', 'data-copied': copiedKey === 'defense-clear' ? '' : undefined,
|
|
2583
|
+
onClick: () => copyInstruction('defense-clear', `请调用 \`iterate_defense_events\` 清除防御事件流:\n\n\`\`\`json\n${JSON.stringify({ operation: 'clear' }, null, 2)}\n\`\`\``),
|
|
2584
|
+
title: '复制 iterate_defense_events clear 指令文本(重置陈旧事件流)',
|
|
2585
|
+
}, copiedKey === 'defense-clear' ? '已复制' : '清除事件'),
|
|
2541
2586
|
)
|
|
2542
2587
|
|
|
2543
2588
|
if (allEvents.length === 0 && totalCount === 0) {
|
|
@@ -2706,7 +2751,7 @@ function ObservatoryPanel(props: SlotProps) {
|
|
|
2706
2751
|
return React.createElement('div', { 'data-iterate-root': '', 'data-iterate': 'obs', className: 'iterate-obs' },
|
|
2707
2752
|
React.createElement('div', { className: 'iterate-obs-head', 'data-closed': open ? undefined : '', onClick: () => setOpen((v) => !v) },
|
|
2708
2753
|
React.createElement('span', { className: 'iterate-obs-title' }, 'iterate 观测台'),
|
|
2709
|
-
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, live
|
|
2754
|
+
React.createElement('span', { className: 'iterate-obs-badge', 'data-live': live ? '' : undefined }, runStatusText(live, manifest)),
|
|
2710
2755
|
React.createElement('span', { className: 'iterate-obs-head-meta' }, headMeta || 'runtime'),
|
|
2711
2756
|
React.createElement('button', {
|
|
2712
2757
|
className: 'iterate-btn', 'data-ghost': '',
|
package/src/session-hooks.ts
CHANGED
|
@@ -7,12 +7,22 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - `allow` policy → the call runs.
|
|
9
9
|
* - `deny` policy → the call is refused (fail-closed), surfaced as an
|
|
10
|
-
* error to the model.
|
|
10
|
+
* error to the model. Since dsh 0.1.6-alpha.1 the
|
|
11
|
+
* denial carries a structured `info` (name
|
|
12
|
+
* `iterate-approval-gate`, code `APPROVAL_DENIED`,
|
|
13
|
+
* plus the human-readable reason) so durable
|
|
14
|
+
* projections can route it distinctly from a normal
|
|
15
|
+
* tool failure.
|
|
11
16
|
* - `ask` policy → return `{ kind: 'ask', reason }`; dsh's own
|
|
12
17
|
* scheduler routes it through the `approval` service
|
|
13
18
|
* (see `@deepseek-ai/dsh-user-approval`), which
|
|
14
19
|
* prompts the human and audits an approve/deny pair
|
|
15
20
|
* on the session.
|
|
21
|
+
* - canceled → if the caller aborted the invocation before
|
|
22
|
+
* dispatch, return `{ kind: 'cancel' }` (the
|
|
23
|
+
* canonical 0.1.6-alpha.1 cancellation result) so a
|
|
24
|
+
* destructive call never runs (or asks) for a dead
|
|
25
|
+
* request.
|
|
16
26
|
*
|
|
17
27
|
* We deliberately do NOT also add `approved` flags inside the tool bodies:
|
|
18
28
|
* the pre-execute waterfall consumes the human decision before the tool runs,
|
|
@@ -29,7 +39,15 @@
|
|
|
29
39
|
import { loadEffectiveConfig, resolveProjectRoot } from './config-loader.ts'
|
|
30
40
|
import { decideApproval, isDestructiveIterateTool } from './approval-gate.ts'
|
|
31
41
|
import type { Context } from '@deepseek-ai/cordis'
|
|
32
|
-
import type { ToolExecution, PreToolDecision } from '@deepseek-ai/dsh-tools'
|
|
42
|
+
import type { ToolErrorInfo, ToolExecution, PreToolDecision } from '@deepseek-ai/dsh-tools'
|
|
43
|
+
|
|
44
|
+
/** Structured error identity attached to policy denials (dsh 0.1.6-alpha.1:
|
|
45
|
+
* `deny.info` lets durable projections / retry / replay distinguish an
|
|
46
|
+
* approval-policy denial from a tool-thrown failure). */
|
|
47
|
+
const DENY_INFO: ToolErrorInfo = {
|
|
48
|
+
name: 'iterate-approval-gate',
|
|
49
|
+
code: 'APPROVAL_DENIED',
|
|
50
|
+
}
|
|
33
51
|
|
|
34
52
|
/**
|
|
35
53
|
* Build the per-call approval decision for a tool execution.
|
|
@@ -49,6 +67,20 @@ export function gateDecision(exec: ToolExecution): PreToolDecision {
|
|
|
49
67
|
}
|
|
50
68
|
if (!isDestructiveIterateTool(name)) return { kind: 'allow' }
|
|
51
69
|
|
|
70
|
+
// dsh 0.1.6-alpha.1: if the caller already aborted this invocation before
|
|
71
|
+
// dispatch, there is nothing to gate — select the canonical cancellation
|
|
72
|
+
// result (`cancel`) rather than prompting a human for a dead call or running
|
|
73
|
+
// `next()` (which would allow) on an aborted one.
|
|
74
|
+
let signal: Readonly<AbortSignal> | undefined
|
|
75
|
+
try {
|
|
76
|
+
signal = exec.signal
|
|
77
|
+
} catch {
|
|
78
|
+
signal = undefined
|
|
79
|
+
}
|
|
80
|
+
if (signal && typeof signal.aborted === 'boolean' && signal.aborted) {
|
|
81
|
+
return { kind: 'cancel' }
|
|
82
|
+
}
|
|
83
|
+
|
|
52
84
|
// Resolve the project root (use the call's own `path` arg, else the agent's
|
|
53
85
|
// session cwd) to read the effective observatory policy.
|
|
54
86
|
let argPath: string | undefined
|
|
@@ -74,7 +106,18 @@ export function gateDecision(exec: ToolExecution): PreToolDecision {
|
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
const decision = decideApproval(exec, policy)
|
|
77
|
-
if (decision.kind === 'deny')
|
|
109
|
+
if (decision.kind === 'deny') {
|
|
110
|
+
return {
|
|
111
|
+
kind: 'deny',
|
|
112
|
+
reason: decision.reason,
|
|
113
|
+
info: {
|
|
114
|
+
...DENY_INFO,
|
|
115
|
+
// ToolErrorInfo.reason (0.1.6-alpha.1): raw user-facing detail kept in
|
|
116
|
+
// durable state; the model-facing text still only sees `reason` above.
|
|
117
|
+
reason: decision.reason,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
78
121
|
if (decision.kind === 'ask') return { kind: 'ask', reason: decision.reason }
|
|
79
122
|
return { kind: 'allow' }
|
|
80
123
|
}
|
|
@@ -104,7 +147,10 @@ export function registerSessionHooks(ctx: Context): void {
|
|
|
104
147
|
// to allow, which would bypass consent — so return our ask decision.
|
|
105
148
|
return Promise.resolve(decision)
|
|
106
149
|
}
|
|
107
|
-
if (decision.kind === 'deny') {
|
|
150
|
+
if (decision.kind === 'deny' || decision.kind === 'cancel') {
|
|
151
|
+
// deny: refuse the call. cancel: the caller already aborted this
|
|
152
|
+
// invocation — selecting the canonical cancellation result instead of
|
|
153
|
+
// letting `next()` allow it or prompting consent for a dead request.
|
|
108
154
|
return Promise.resolve(decision)
|
|
109
155
|
}
|
|
110
156
|
return next()
|