@lazyingart/agintiflow 0.20.213 → 0.20.215
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/docs/supervision-campaign-ledger.md +56 -0
- package/package.json +1 -1
- package/scripts/smoke-dynamic-step-budget.js +68 -0
- package/scripts/smoke-model-roles.js +23 -12
- package/scripts/smoke-perception-research.js +30 -0
- package/scripts/smoke-scs-evidence-visibility.js +43 -31
- package/scripts/smoke-truthful-completion.js +57 -4
- package/scripts/smoke-web-api.js +19 -4
- package/src/agent-runner.js +68 -1
- package/src/guardrails.js +2 -1
- package/src/model-client.js +5 -1
- package/src/scs-evidence.js +89 -13
- package/web.js +23 -7
|
@@ -130,3 +130,59 @@ checker exactly once, performed no file, canvas, commit, or push side effect,
|
|
|
130
130
|
and completed with a clean repository-state check. The full npm suite and the
|
|
131
131
|
focused dynamic-budget, SCS/model-role, and web-canvas regressions pass for
|
|
132
132
|
AgInTiFlow `0.20.213`.
|
|
133
|
+
|
|
134
|
+
### Java repair, permission pause, and durable artifact evidence
|
|
135
|
+
|
|
136
|
+
`java-event-window-013` passed after exposing two runtime defects with a normal,
|
|
137
|
+
underspecified Java repair prompt. DeepSeek correctly implemented decimal
|
|
138
|
+
duration parsing, non-mutating percentile interpolation, deterministic window
|
|
139
|
+
summaries, project guidance, and generated-output ignores. The first run then
|
|
140
|
+
encountered a host permission blocker while invoking the checked-in test script.
|
|
141
|
+
The runtime continued spending model and SCS turns instead of persisting a
|
|
142
|
+
single actionable pause. After trusted-host approval, loose artifact inference
|
|
143
|
+
also treated a generated test transcript as a mandatory deliverable, prompting
|
|
144
|
+
the agent to create and commit an unrequested `docs/test-results.txt`.
|
|
145
|
+
|
|
146
|
+
Permission advice that cannot auto-recover now stops the run immediately with
|
|
147
|
+
durable resume data. Approval is single-use, and a resolved blocker cannot be
|
|
148
|
+
replayed by a stale web request. Same-task continuation still preserves the
|
|
149
|
+
original goal when an approval sentence precedes the continuation instruction.
|
|
150
|
+
Artifact evidence from commands is accepted only when it names a supported,
|
|
151
|
+
existing, nonempty path; removed files and label-only prose no longer satisfy
|
|
152
|
+
completion. Exclusion language such as "ignore generated build and session
|
|
153
|
+
outputs" no longer invents an artifact requirement.
|
|
154
|
+
|
|
155
|
+
The queued correction was applied to the same live session, the stray transcript
|
|
156
|
+
commit was removed, and the intended repair remains at target commit `f2792f3`.
|
|
157
|
+
Independent verification passed the checked-in Java test script, the hidden
|
|
158
|
+
event-window contract, generated-output tracking checks, clean-worktree checks,
|
|
159
|
+
the focused permission/evidence regressions, and the complete AgInTiFlow npm
|
|
160
|
+
suite.
|
|
161
|
+
|
|
162
|
+
### Long-context memo synthesis and local visual verification
|
|
163
|
+
|
|
164
|
+
`memo-full-context-pdf-014` used a normal, short writing prompt against a
|
|
165
|
+
realistic interrupted chat export containing corrections, cancellations,
|
|
166
|
+
deadlines, dependencies, completed work, publication boundaries, research
|
|
167
|
+
ideas, and personal errands. The DeepSeek-backed writing agent read the full
|
|
168
|
+
history and produced an editable two-page XeLaTeX/PDF memo instead of copying
|
|
169
|
+
transport rows. Independent acceptance checked every critical commitment,
|
|
170
|
+
rejected raw timestamps and internal identifiers, verified the PDF structure,
|
|
171
|
+
and required a clean intentional commit.
|
|
172
|
+
|
|
173
|
+
The first draft generalized a concrete Nutstore private-backup destination and
|
|
174
|
+
left generated agent/perception directories unignored. The same durable session
|
|
175
|
+
accepted those exact external findings, restored the actionable destination,
|
|
176
|
+
added narrow generated-state ignores, rebuilt the PDF, inspected both rendered
|
|
177
|
+
pages, and committed the correction as `a275878`. The hidden full-context
|
|
178
|
+
checker, `qpdf`, `pdfinfo`, extracted-text review, independent page renders, and
|
|
179
|
+
clean-worktree check then passed.
|
|
180
|
+
|
|
181
|
+
The visual-review turn exposed a reusable provider-boundary defect: automatic
|
|
182
|
+
`read_image` was blocked merely because DeepSeek was the active reasoning
|
|
183
|
+
provider, even though the configured local image-perception handoff was safe
|
|
184
|
+
and the perception runtime already supported it. Guardrails now allow the
|
|
185
|
+
DeepSeek-to-LocalLLM handoff when local perception is enabled, continue to block
|
|
186
|
+
hosted vision without explicit authorization, and block all automatic vision
|
|
187
|
+
when both local and hosted routes are disabled. A focused regression covers the
|
|
188
|
+
guard and the actual LocalLLM client route.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.215",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -196,6 +196,74 @@ try {
|
|
|
196
196
|
sourceRepairContract.requiredEvidence.some((item) => item.category === "file"),
|
|
197
197
|
"an explicit source repair lost its file-change evidence gate"
|
|
198
198
|
);
|
|
199
|
+
const ignoredGeneratedOutputsContract = deriveScsTaskContract({
|
|
200
|
+
goal: [
|
|
201
|
+
"You have explicit trusted-host approval for this isolated Java fixture.",
|
|
202
|
+
"Continue the same task from the current edits. Run the checked-in project test script, repair any failures,",
|
|
203
|
+
"create the required project guidance, ignore generated build and session outputs, commit only intentional work,",
|
|
204
|
+
"and finish with verified evidence.",
|
|
205
|
+
].join(" "),
|
|
206
|
+
taskProfile: "java",
|
|
207
|
+
});
|
|
208
|
+
assert(
|
|
209
|
+
ignoredGeneratedOutputsContract.requiredEvidence.some((item) => item.category === "file") &&
|
|
210
|
+
ignoredGeneratedOutputsContract.requiredEvidence.some((item) => item.category === "command") &&
|
|
211
|
+
ignoredGeneratedOutputsContract.requiredEvidence.some((item) => item.category === "git") &&
|
|
212
|
+
!ignoredGeneratedOutputsContract.requiredEvidence.some((item) => item.category === "artifact"),
|
|
213
|
+
"ignoring generated build/session outputs invented a standalone artifact requirement"
|
|
214
|
+
);
|
|
215
|
+
const durableArtifactPath = path.join(workspace, "reports", "durable-report.pdf");
|
|
216
|
+
await fs.mkdir(path.dirname(durableArtifactPath), { recursive: true });
|
|
217
|
+
await fs.writeFile(durableArtifactPath, "%PDF-1.4\nsmoke\n", "utf8");
|
|
218
|
+
const artifactEvents = [
|
|
219
|
+
{
|
|
220
|
+
type: "tool.completed",
|
|
221
|
+
data: {
|
|
222
|
+
ok: true,
|
|
223
|
+
toolName: "run_command",
|
|
224
|
+
args: { command: "printf smoke > reports/durable-report.pdf" },
|
|
225
|
+
stdout: "created reports/durable-report.pdf",
|
|
226
|
+
exitCode: 0,
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
const durableArtifactLedger = buildScsEvidenceLedger({
|
|
231
|
+
context: { events: artifactEvents, commandCwd: workspace },
|
|
232
|
+
});
|
|
233
|
+
assert(
|
|
234
|
+
durableArtifactLedger.categories.includes("artifact"),
|
|
235
|
+
"an existing shell-generated PDF did not count as durable artifact evidence"
|
|
236
|
+
);
|
|
237
|
+
await fs.rm(durableArtifactPath);
|
|
238
|
+
const removedArtifactLedger = buildScsEvidenceLedger({
|
|
239
|
+
context: { events: artifactEvents, commandCwd: workspace },
|
|
240
|
+
});
|
|
241
|
+
assert(
|
|
242
|
+
!removedArtifactLedger.categories.includes("artifact") &&
|
|
243
|
+
removedArtifactLedger.items.some((item) => item.category === "artifact" && item.verified === false),
|
|
244
|
+
"a removed artifact continued to satisfy the final evidence ledger"
|
|
245
|
+
);
|
|
246
|
+
const labelOnlyArtifactLedger = buildScsEvidenceLedger({
|
|
247
|
+
context: {
|
|
248
|
+
events: [
|
|
249
|
+
{
|
|
250
|
+
type: "tool.completed",
|
|
251
|
+
data: {
|
|
252
|
+
ok: true,
|
|
253
|
+
toolName: "run_command",
|
|
254
|
+
args: { command: "echo ARTIFACT READY" },
|
|
255
|
+
stdout: "ARTIFACT READY",
|
|
256
|
+
exitCode: 0,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
commandCwd: workspace,
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
assert(
|
|
264
|
+
!labelOnlyArtifactLedger.categories.includes("artifact"),
|
|
265
|
+
"an artifact label in generic shell text counted as a durable artifact"
|
|
266
|
+
);
|
|
199
267
|
assert(
|
|
200
268
|
completionEvidenceNeedsCommand({ missingProjectCommands: ["python analysis.py"] }),
|
|
201
269
|
"a pending canonical command did not reopen command execution"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
import {
|
|
@@ -1086,18 +1088,27 @@ assert(
|
|
|
1086
1088
|
!missingWriterToolEval.ok && missingWriterToolEval.missingToolCalls.includes("writing_specialist"),
|
|
1087
1089
|
"SCS should reject finish when required specialist call is missing"
|
|
1088
1090
|
);
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1091
|
+
const writerEvidenceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "aginti-writer-evidence-"));
|
|
1092
|
+
try {
|
|
1093
|
+
const writerArtifactPath = path.join(writerEvidenceRoot, "artifacts", "writer.json");
|
|
1094
|
+
fs.mkdirSync(path.dirname(writerArtifactPath), { recursive: true });
|
|
1095
|
+
fs.writeFileSync(writerArtifactPath, JSON.stringify({ status: "completed" }), "utf8");
|
|
1096
|
+
const presentWriterToolEval = evaluateScsEvidence(
|
|
1097
|
+
requiredWriterToolContract,
|
|
1098
|
+
buildScsEvidenceLedger({
|
|
1099
|
+
context: {
|
|
1100
|
+
commandCwd: writerEvidenceRoot,
|
|
1101
|
+
events: [
|
|
1102
|
+
{ type: "file.changed", data: { path: "final/story.md" } },
|
|
1103
|
+
{ type: "tool.completed", data: { toolName: "writing_specialist", ok: true, artifactPath: writerArtifactPath } },
|
|
1104
|
+
],
|
|
1105
|
+
},
|
|
1106
|
+
})
|
|
1107
|
+
);
|
|
1108
|
+
assert(presentWriterToolEval.ok, "SCS should accept required specialist call when tool evidence is present");
|
|
1109
|
+
} finally {
|
|
1110
|
+
fs.rmSync(writerEvidenceRoot, { recursive: true, force: true });
|
|
1111
|
+
}
|
|
1101
1112
|
|
|
1102
1113
|
const blockedFileFinish = await reviewScsFinish(
|
|
1103
1114
|
{ mock: true },
|
|
@@ -83,6 +83,36 @@ async function verifyProviderBoundary({ pngPath, store, config }) {
|
|
|
83
83
|
config: { ...config, provider: "localllm", allowHostedImagePerception: false },
|
|
84
84
|
});
|
|
85
85
|
assert(!guardedImage.allowed, "guardrails allowed unapproved hosted image perception");
|
|
86
|
+
const guardedDeepSeekLocalHandoff = checkToolUse({
|
|
87
|
+
toolName: "read_image",
|
|
88
|
+
args: { path: path.relative(config.commandCwd, pngPath) },
|
|
89
|
+
snapshot: { elements: [] },
|
|
90
|
+
config: {
|
|
91
|
+
...config,
|
|
92
|
+
provider: "deepseek",
|
|
93
|
+
allowLocalImagePerception: true,
|
|
94
|
+
allowHostedImagePerception: false,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
assert(
|
|
98
|
+
guardedDeepSeekLocalHandoff.allowed,
|
|
99
|
+
"guardrails blocked the safe DeepSeek-to-LocalLLM image handoff"
|
|
100
|
+
);
|
|
101
|
+
const blockedDeepSeekWithoutHandoff = checkToolUse({
|
|
102
|
+
toolName: "read_image",
|
|
103
|
+
args: { path: path.relative(config.commandCwd, pngPath) },
|
|
104
|
+
snapshot: { elements: [] },
|
|
105
|
+
config: {
|
|
106
|
+
...config,
|
|
107
|
+
provider: "deepseek",
|
|
108
|
+
allowLocalImagePerception: false,
|
|
109
|
+
allowHostedImagePerception: false,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
assert(
|
|
113
|
+
!blockedDeepSeekWithoutHandoff.allowed,
|
|
114
|
+
"guardrails allowed auto image perception after every backend handoff was disabled"
|
|
115
|
+
);
|
|
86
116
|
const guardedResearch = checkToolUse({
|
|
87
117
|
toolName: "web_research",
|
|
88
118
|
args: { query: "provider boundary smoke", mode: "openai" },
|
|
@@ -152,38 +152,50 @@ const finishDecision = await reviewScsFinish(
|
|
|
152
152
|
assert.equal(finishDecision.decision, "finish_allowed", "SCS should not reject finish as no-evidence when the contract ledger is satisfied");
|
|
153
153
|
assert.match(finishDecision.reason, /Overrode a no-evidence finish rejection/);
|
|
154
154
|
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
155
|
+
const researchEvidenceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "aginti-research-evidence-"));
|
|
156
|
+
try {
|
|
157
|
+
const reportPath = path.join(researchEvidenceRoot, "reports", "cited-research.md");
|
|
158
|
+
const artifactPath = path.join(researchEvidenceRoot, "artifacts", "deep-research.json");
|
|
159
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
160
|
+
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
161
|
+
fs.writeFileSync(reportPath, "# Cited research\n\nVerified report fixture.\n", "utf8");
|
|
162
|
+
fs.writeFileSync(artifactPath, JSON.stringify({ status: "completed" }), "utf8");
|
|
163
|
+
|
|
164
|
+
const researchReportLedger = buildScsEvidenceLedger({
|
|
165
|
+
context: {
|
|
166
|
+
events: [{
|
|
167
|
+
type: "tool.completed",
|
|
168
|
+
data: {
|
|
169
|
+
ok: true,
|
|
170
|
+
toolName: "deep_research",
|
|
171
|
+
status: "completed",
|
|
172
|
+
reportPath,
|
|
173
|
+
artifactPath,
|
|
174
|
+
coverage: {
|
|
175
|
+
verifiedClaimCount: 12,
|
|
176
|
+
quoteVerificationRate: 1,
|
|
177
|
+
},
|
|
178
|
+
audit: {
|
|
179
|
+
citationCoverage: 1,
|
|
180
|
+
unknownEvidenceIds: [],
|
|
181
|
+
},
|
|
172
182
|
},
|
|
173
|
-
},
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
}],
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
assert(
|
|
187
|
+
["file", "command", "artifact"].every((category) => researchReportLedger.categories.includes(category)),
|
|
188
|
+
"a completed and audited deep-research report did not satisfy file, validation-command, and artifact evidence categories"
|
|
189
|
+
);
|
|
190
|
+
assert(
|
|
191
|
+
researchReportLedger.items.some(
|
|
192
|
+
(item) => item.category === "command" && /deterministic audit completed/.test(item.proof)
|
|
193
|
+
),
|
|
194
|
+
"deep-research validation evidence did not preserve its deterministic audit provenance"
|
|
195
|
+
);
|
|
196
|
+
} finally {
|
|
197
|
+
fs.rmSync(researchEvidenceRoot, { recursive: true, force: true });
|
|
198
|
+
}
|
|
187
199
|
|
|
188
200
|
const recoverableLedger = buildScsEvidenceLedger({
|
|
189
201
|
context: {
|
|
@@ -233,6 +233,29 @@ try {
|
|
|
233
233
|
"same-task resume used the new-request boundary marker"
|
|
234
234
|
);
|
|
235
235
|
|
|
236
|
+
const prefixedSameTaskContinuation = await runCase({
|
|
237
|
+
id: "ordinary-explanation",
|
|
238
|
+
goal: [
|
|
239
|
+
"You have explicit trusted-host approval for this isolated fixture.",
|
|
240
|
+
"Continue the same task from the current edits. Finish it with verified evidence.",
|
|
241
|
+
].join(" "),
|
|
242
|
+
resume: true,
|
|
243
|
+
responses: [assistant("A base case remains the condition that terminates recursive expansion.")],
|
|
244
|
+
});
|
|
245
|
+
const prefixedContinuationEvent = [...prefixedSameTaskContinuation.events]
|
|
246
|
+
.reverse()
|
|
247
|
+
.find((event) => event.type === "conversation.continued");
|
|
248
|
+
assert.equal(
|
|
249
|
+
prefixedContinuationEvent?.data?.preservesTaskBoundary,
|
|
250
|
+
true,
|
|
251
|
+
"a same-task continuation prefixed by a permission statement opened a new task boundary"
|
|
252
|
+
);
|
|
253
|
+
assert.equal(
|
|
254
|
+
prefixedSameTaskContinuation.state.meta?.goalContract?.taskGoal,
|
|
255
|
+
"Explain why recursion needs a base case.",
|
|
256
|
+
"a prefixed same-task continuation replaced the durable task goal"
|
|
257
|
+
);
|
|
258
|
+
|
|
236
259
|
const quotedChatClassification = await runCase({
|
|
237
260
|
id: "quoted-chat-classification",
|
|
238
261
|
taskProfile: "chatops",
|
|
@@ -266,6 +289,36 @@ try {
|
|
|
266
289
|
assert.equal(proseOnlyAction.events.filter((event) => event.type === "completion.evidence_rejected").length, 2);
|
|
267
290
|
assert(!proseOnlyAction.events.some((event) => event.type === "session.finished"));
|
|
268
291
|
|
|
292
|
+
const permissionPause = await runCase({
|
|
293
|
+
id: "permission-pause",
|
|
294
|
+
goal: "Run the checked-in project test script and report its verified result.",
|
|
295
|
+
taskProfile: "java",
|
|
296
|
+
allowShellTool: true,
|
|
297
|
+
scsActive: true,
|
|
298
|
+
setup: async (workspace) => {
|
|
299
|
+
await fs.mkdir(path.join(workspace, "scripts"), { recursive: true });
|
|
300
|
+
await fs.writeFile(path.join(workspace, "scripts", "test.sh"), "#!/usr/bin/env bash\necho pass\n", "utf8");
|
|
301
|
+
},
|
|
302
|
+
responses: [
|
|
303
|
+
assistant("", [toolCall("permission-test", "run_command", { command: "bash scripts/test.sh" })]),
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
assert.equal(permissionPause.calls.length, 1, "permission blocker consumed another model turn");
|
|
307
|
+
assert.equal(permissionPause.result.stopped, true);
|
|
308
|
+
assert.equal(permissionPause.result.reason, "permission_required");
|
|
309
|
+
assert(permissionPause.result.permissionAdvice?.suggestedCommand, "permission pause lost its exact resume command");
|
|
310
|
+
assert.equal(
|
|
311
|
+
permissionPause.events.filter((event) => event.type === "session.stopped" && event.data?.reason === "permission_required").length,
|
|
312
|
+
1,
|
|
313
|
+
"permission blocker did not persist exactly one paused state"
|
|
314
|
+
);
|
|
315
|
+
assert(
|
|
316
|
+
!permissionPause.events.some((event) =>
|
|
317
|
+
["scs.student.rethink_plan", "scs.student.reject_phase", "scs.committee.replan_drafted"].includes(event.type)
|
|
318
|
+
),
|
|
319
|
+
"permission blocker triggered an SCS replan instead of waiting for approval"
|
|
320
|
+
);
|
|
321
|
+
|
|
269
322
|
const reasoningTruncation = await runCase({
|
|
270
323
|
id: "reasoning-only-tool-continuation",
|
|
271
324
|
goal: "Run pwd and report the verified working directory.",
|
|
@@ -371,12 +424,12 @@ try {
|
|
|
371
424
|
|
|
372
425
|
const approvalNarrativeWithBlockerEvidence = await runCase({
|
|
373
426
|
id: "approval-narrative-with-blocker-evidence",
|
|
374
|
-
goal: "Run definitely_missing_aginti_command and report the result.",
|
|
427
|
+
goal: "Run which definitely_missing_aginti_command and report the result.",
|
|
375
428
|
taskProfile: "shell",
|
|
376
429
|
allowShellTool: true,
|
|
377
430
|
responses: [
|
|
378
431
|
assistant("", [
|
|
379
|
-
toolCall("missing-command", "run_command", { command: "definitely_missing_aginti_command" }),
|
|
432
|
+
toolCall("missing-command", "run_command", { command: "which definitely_missing_aginti_command" }),
|
|
380
433
|
]),
|
|
381
434
|
assistant("The command is unavailable. Approve installing it and I will continue after approval."),
|
|
382
435
|
assistant("Unable to execute the requested command because it is not installed in this environment."),
|
|
@@ -695,11 +748,11 @@ try {
|
|
|
695
748
|
|
|
696
749
|
const verifiedBlocker = await runCase({
|
|
697
750
|
id: "verified-blocker",
|
|
698
|
-
goal: "
|
|
751
|
+
goal: "Run which definitely_not_an_aginti_command and report the result.",
|
|
699
752
|
taskProfile: "shell",
|
|
700
753
|
allowShellTool: true,
|
|
701
754
|
responses: [
|
|
702
|
-
assistant("", [toolCall("run-blocked", "run_command", { command: "definitely_not_an_aginti_command" })]),
|
|
755
|
+
assistant("", [toolCall("run-blocked", "run_command", { command: "which definitely_not_an_aginti_command" })]),
|
|
703
756
|
assistant("", [
|
|
704
757
|
toolCall("finish-blocked", "finish", {
|
|
705
758
|
result: "Unable to execute the requested command because it is not installed in this environment.",
|
package/scripts/smoke-web-api.js
CHANGED
|
@@ -64,11 +64,12 @@ async function waitForHealth() {
|
|
|
64
64
|
throw new Error(`web server did not become healthy. stdout=${stdout.slice(-500)} stderr=${stderr.slice(-500)}`);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
async function waitForRun(sessionId) {
|
|
67
|
+
async function waitForRun(sessionId, terminalStatuses = ["finished", "failed"]) {
|
|
68
|
+
const acceptedStatuses = new Set(terminalStatuses);
|
|
68
69
|
const deadline = Date.now() + 20000;
|
|
69
70
|
while (Date.now() < deadline) {
|
|
70
71
|
const run = await fetchJson(`/api/runs/${encodeURIComponent(sessionId)}`);
|
|
71
|
-
if (
|
|
72
|
+
if (acceptedStatuses.has(run.status)) return run;
|
|
72
73
|
await delay(400);
|
|
73
74
|
}
|
|
74
75
|
throw new Error(`run ${sessionId} did not finish in time`);
|
|
@@ -548,7 +549,10 @@ try {
|
|
|
548
549
|
headless: true,
|
|
549
550
|
}),
|
|
550
551
|
});
|
|
551
|
-
const approvalRaceBlocked = await waitForRun(approvalRaceStart.sessionId);
|
|
552
|
+
const approvalRaceBlocked = await waitForRun(approvalRaceStart.sessionId, ["stopped", "failed"]);
|
|
553
|
+
if (approvalRaceBlocked.status !== "stopped") {
|
|
554
|
+
throw new Error(`permission/message race fixture did not stop for approval: ${approvalRaceBlocked.status}`);
|
|
555
|
+
}
|
|
552
556
|
if (!approvalRaceBlocked.logs?.some((entry) => entry.message === "tool.blocked" && entry.data?.permissionAdvice)) {
|
|
553
557
|
throw new Error("permission/message race fixture did not produce pending permission advice");
|
|
554
558
|
}
|
|
@@ -595,7 +599,10 @@ try {
|
|
|
595
599
|
headless: true,
|
|
596
600
|
}),
|
|
597
601
|
});
|
|
598
|
-
const safeRun = await waitForRun(safeRunStart.sessionId);
|
|
602
|
+
const safeRun = await waitForRun(safeRunStart.sessionId, ["stopped", "failed"]);
|
|
603
|
+
if (safeRun.status !== "stopped") {
|
|
604
|
+
throw new Error(`safe mode web run did not stop for approval: ${safeRun.status}`);
|
|
605
|
+
}
|
|
599
606
|
if (!safeRun.logs?.some((entry) => entry.message === "tool.blocked" && entry.data?.permissionAdvice?.category === "workspace-write")) {
|
|
600
607
|
throw new Error("safe mode web run did not expose workspace-write permission advice");
|
|
601
608
|
}
|
|
@@ -630,6 +637,14 @@ try {
|
|
|
630
637
|
if (!safeApproved.includes("Created by AgInTiFlow mock mode.")) {
|
|
631
638
|
throw new Error("permission-approved continuation did not create the requested file");
|
|
632
639
|
}
|
|
640
|
+
const staleApproval = await fetch(`${baseUrl}/api/sessions/${encodeURIComponent(safeRunStart.sessionId)}/approve-permission`, {
|
|
641
|
+
method: "POST",
|
|
642
|
+
headers: { "Content-Type": "application/json" },
|
|
643
|
+
body: JSON.stringify({ action: "once" }),
|
|
644
|
+
});
|
|
645
|
+
if (staleApproval.status !== 404) {
|
|
646
|
+
throw new Error(`resolved permission advice remained reusable: ${staleApproval.status}`);
|
|
647
|
+
}
|
|
633
648
|
await fetchJson("/api/preferences", {
|
|
634
649
|
method: "POST",
|
|
635
650
|
headers: { "Content-Type": "application/json" },
|
package/src/agent-runner.js
CHANGED
|
@@ -2216,7 +2216,7 @@ function isGenericTaskContinuationText(value = "") {
|
|
|
2216
2216
|
const normalized = String(value || "").replace(/\s+/g, " ").trim();
|
|
2217
2217
|
if (!normalized || normalized.length > 600) return false;
|
|
2218
2218
|
const explicitSameTaskContinuation =
|
|
2219
|
-
|
|
2219
|
+
/(?:^|[.!?]\s+)(?:(?:please|kindly)\s+)?(?:continue|resume|keep\s+working|finish|complete)\b.{0,180}\b(?:same|current|previous|existing|retained|saved|unfinished)\b.{0,80}\b(?:task|work|run|session|job|state)\b/i.test(normalized);
|
|
2220
2220
|
return explicitSameTaskContinuation || /^(?:(?:please|kindly)\s+)?(?:continue|resume|finish|complete|keep\s+working)(?:\s+(?:and\s+)?(?:continue|finish|complete|working))?(?:\s+(?:the\s+)?(?:same|current|previous|existing|retained|saved|unfinished)\s+(?:task|work|run|session|job))?(?:\s+from\s+(?:the\s+)?(?:retained|saved|current|previous)\s+state)?[.!?]*$/i.test(normalized) ||
|
|
2221
2221
|
/^(?:请)?(?:继续|接着|恢复|完成)(?:之前|上次|当前|同一|这个)?(?:的)?(?:任务|工作|会话|进度)?(?:并完成)?[。!?.!?]*$/u.test(normalized) ||
|
|
2222
2222
|
/^(?:このまま|前回から|保存した状態から)?(?:同じ|現在の|前の)?(?:タスク|作業|セッション)?(?:を)?(?:続けて|再開して|完了して)(?:ください)?[。!?.!?]*$/u.test(normalized);
|
|
@@ -2907,6 +2907,14 @@ export function shouldShortCircuitToolBatch(toolResult) {
|
|
|
2907
2907
|
);
|
|
2908
2908
|
}
|
|
2909
2909
|
|
|
2910
|
+
export function shouldPauseForPermissionAdvice(toolResult = {}) {
|
|
2911
|
+
return Boolean(
|
|
2912
|
+
toolResult?.blocked &&
|
|
2913
|
+
toolResult?.permissionAdvice &&
|
|
2914
|
+
toolResult.permissionAdvice.autoRecover !== true
|
|
2915
|
+
);
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2910
2918
|
export function skippedAfterBlockedToolResult(toolCall, blockedResult) {
|
|
2911
2919
|
const toolName = toolCall?.function?.name || "unknown";
|
|
2912
2920
|
const args = sanitizeToolArgs(toolName, safeParseToolArgs(toolCall));
|
|
@@ -6441,6 +6449,51 @@ async function stopForMissingCompletionEvidence({ config, state, store, observer
|
|
|
6441
6449
|
};
|
|
6442
6450
|
}
|
|
6443
6451
|
|
|
6452
|
+
async function stopForPermissionAdvice({ config, state, store, observers, sessionId, step, toolResult }) {
|
|
6453
|
+
const advice = toolResult?.permissionAdvice && typeof toolResult.permissionAdvice === "object"
|
|
6454
|
+
? toolResult.permissionAdvice
|
|
6455
|
+
: {};
|
|
6456
|
+
const result = [
|
|
6457
|
+
advice.summary || toolResult?.reason || "The requested action needs a stronger permission mode.",
|
|
6458
|
+
advice.instruction || "Resume after approving the required mode or choose a safer alternative.",
|
|
6459
|
+
advice.suggestedCommand ? `Contained resume: ${advice.suggestedCommand}` : "",
|
|
6460
|
+
advice.trustedHostCommand ? `Trusted-host resume: ${advice.trustedHostCommand}` : "",
|
|
6461
|
+
].filter(Boolean).join("\n");
|
|
6462
|
+
const detail = {
|
|
6463
|
+
step,
|
|
6464
|
+
toolName: toolResult?.toolName || "",
|
|
6465
|
+
category: toolResult?.category || advice.category || "permission-required",
|
|
6466
|
+
reason: toolResult?.reason || advice.reason || "",
|
|
6467
|
+
permissionAdvice: advice,
|
|
6468
|
+
};
|
|
6469
|
+
state.stepsCompleted = step;
|
|
6470
|
+
state.updatedAt = new Date().toISOString();
|
|
6471
|
+
state.meta = state.meta || {};
|
|
6472
|
+
state.meta.pendingPermissionAdvice = detail;
|
|
6473
|
+
updateGoalStatus(state, "paused", "permission_required", state.updatedAt);
|
|
6474
|
+
await store.appendEvent("session.stopped", {
|
|
6475
|
+
reason: "permission_required",
|
|
6476
|
+
step,
|
|
6477
|
+
detail,
|
|
6478
|
+
});
|
|
6479
|
+
observers.event("session.stopped", {
|
|
6480
|
+
reason: "permission_required",
|
|
6481
|
+
sessionId,
|
|
6482
|
+
toolName: detail.toolName,
|
|
6483
|
+
category: detail.category,
|
|
6484
|
+
});
|
|
6485
|
+
await store.saveState(state);
|
|
6486
|
+
emitConsole(config, result, { kind: "error", error: true });
|
|
6487
|
+
return {
|
|
6488
|
+
sessionId,
|
|
6489
|
+
result,
|
|
6490
|
+
stopped: true,
|
|
6491
|
+
reason: "permission_required",
|
|
6492
|
+
permissionAdvice: advice,
|
|
6493
|
+
...goalRunMetadata(state),
|
|
6494
|
+
};
|
|
6495
|
+
}
|
|
6496
|
+
|
|
6444
6497
|
export function resetPerTurnToolContractState(state = {}, at = new Date().toISOString()) {
|
|
6445
6498
|
const prior = state.meta?.toolContractViolation;
|
|
6446
6499
|
if (!prior) return null;
|
|
@@ -8040,6 +8093,7 @@ export async function runAgent(config) {
|
|
|
8040
8093
|
|
|
8041
8094
|
let continueForQueuedInput = false;
|
|
8042
8095
|
let continueForCompletionRepair = false;
|
|
8096
|
+
let pendingPermissionPause = null;
|
|
8043
8097
|
const postBatchToolResults = [];
|
|
8044
8098
|
for (let toolIndex = 0; toolIndex < toolCalls.length; toolIndex += 1) {
|
|
8045
8099
|
const toolCall = toolCalls[toolIndex];
|
|
@@ -8128,6 +8182,7 @@ export async function runAgent(config) {
|
|
|
8128
8182
|
priorBlockedTool: skippedResult.priorBlockedTool,
|
|
8129
8183
|
});
|
|
8130
8184
|
}
|
|
8185
|
+
if (shouldPauseForPermissionAdvice(toolResult)) pendingPermissionPause = toolResult;
|
|
8131
8186
|
break;
|
|
8132
8187
|
}
|
|
8133
8188
|
|
|
@@ -8256,6 +8311,18 @@ export async function runAgent(config) {
|
|
|
8256
8311
|
}
|
|
8257
8312
|
}
|
|
8258
8313
|
|
|
8314
|
+
if (pendingPermissionPause) {
|
|
8315
|
+
return await stopForPermissionAdvice({
|
|
8316
|
+
config,
|
|
8317
|
+
state,
|
|
8318
|
+
store,
|
|
8319
|
+
observers,
|
|
8320
|
+
sessionId,
|
|
8321
|
+
step,
|
|
8322
|
+
toolResult: pendingPermissionPause,
|
|
8323
|
+
});
|
|
8324
|
+
}
|
|
8325
|
+
|
|
8259
8326
|
if (continueForCompletionRepair) continue;
|
|
8260
8327
|
|
|
8261
8328
|
for (const toolResult of postBatchToolResults) {
|
package/src/guardrails.js
CHANGED
|
@@ -230,11 +230,12 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
|
|
|
230
230
|
!args.dryRun &&
|
|
231
231
|
["", "auto", "default"].includes(provider) &&
|
|
232
232
|
!["localllm", "openai"].includes(activeProvider) &&
|
|
233
|
+
config.allowLocalImagePerception === false &&
|
|
233
234
|
config.allowHostedImagePerception !== true
|
|
234
235
|
) {
|
|
235
236
|
return {
|
|
236
237
|
allowed: false,
|
|
237
|
-
reason: `No automatic image-reading backend is enabled for active provider ${activeProvider}.
|
|
238
|
+
reason: `No automatic image-reading backend is enabled for active provider ${activeProvider}. Enable the local image-perception handoff, select localllm, or explicitly enable a hosted image backend.`,
|
|
238
239
|
category: "perception-tools",
|
|
239
240
|
};
|
|
240
241
|
}
|
package/src/model-client.js
CHANGED
|
@@ -85,7 +85,11 @@ function mockToolCall(name, args = {}) {
|
|
|
85
85
|
|
|
86
86
|
function latestToolPayload(messages) {
|
|
87
87
|
for (const message of [...messages].reverse()) {
|
|
88
|
-
|
|
88
|
+
const userContent = String(message.content || "");
|
|
89
|
+
if (
|
|
90
|
+
message.role === "user" &&
|
|
91
|
+
/^(?:Continue with this new request:|Continue the current task from saved state:|Goal:)/i.test(userContent)
|
|
92
|
+
) {
|
|
89
93
|
return null;
|
|
90
94
|
}
|
|
91
95
|
if (message.role !== "tool" || !message.content) continue;
|
package/src/scs-evidence.js
CHANGED
|
@@ -580,6 +580,10 @@ function inferRequirementCategories(goal = "", taskProfile = "", acceptanceCrite
|
|
|
580
580
|
/\b(?:clean(?:\s+up)?|remove|delete|clear|purge)\b[^.\n;]{0,120}\b(?:generated|temporary|stale|test)?\s*(?:test\s+)?(?:debris|caches?|byproducts?)\b/gi,
|
|
581
581
|
""
|
|
582
582
|
)
|
|
583
|
+
.replace(
|
|
584
|
+
/\b(?:ignore|exclude|omit|skip|leave\s+out)\b[^.\n;]{0,160}\b(?:generated|temporary|stale|build|session)\b[^.\n;]{0,100}\b(?:outputs?|artifacts?|files?|directories?|folders?)\b/gi,
|
|
585
|
+
""
|
|
586
|
+
)
|
|
583
587
|
.replace(/\bfigure\s+out\b/gi, "");
|
|
584
588
|
const mandatoryEvidenceText = artifactSignalText.replace(
|
|
585
589
|
/[^.\n]{0,240}\b(?:as appropriate|if appropriate|when useful|where applicable)\b/gi,
|
|
@@ -2172,6 +2176,7 @@ function eventToEvidence(event = {}) {
|
|
|
2172
2176
|
target: data.path || data.artifactId || data.outputPath || "",
|
|
2173
2177
|
proof: type,
|
|
2174
2178
|
verified: true,
|
|
2179
|
+
virtualArtifact: Boolean(data.artifactId && !data.path && !data.outputPath),
|
|
2175
2180
|
});
|
|
2176
2181
|
if (type === "image.generated") {
|
|
2177
2182
|
evidence.push({
|
|
@@ -2280,16 +2285,31 @@ function toolPayloadToEvidence(payload = {}, source = "tool") {
|
|
|
2280
2285
|
if (["open_url", "click", "type", "scroll", "press", "back"].includes(toolName) || /\b(browser|chrome|cdp|playwright|selenium|upload|attach|submit|click|tab|page)\b/.test(text)) {
|
|
2281
2286
|
push("browser", `${toolName || "browser tool"} affected or inspected browser/UI state`, payload.url || args.url || args.command || "");
|
|
2282
2287
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2288
|
+
const artifactTools = new Set([
|
|
2289
|
+
"open_workspace_file",
|
|
2290
|
+
"preview_workspace",
|
|
2291
|
+
"send_to_canvas",
|
|
2292
|
+
"generate_image",
|
|
2293
|
+
"read_image",
|
|
2294
|
+
"writing_specialist",
|
|
2295
|
+
"json_specialist",
|
|
2296
|
+
"json_specialist_batch",
|
|
2297
|
+
]);
|
|
2298
|
+
const artifactPath = firstArtifactPath(
|
|
2299
|
+
payload.artifactPath,
|
|
2300
|
+
payload.outputPath,
|
|
2301
|
+
payload.reportPath,
|
|
2302
|
+
payload.path,
|
|
2303
|
+
args.path,
|
|
2304
|
+
text
|
|
2305
|
+
);
|
|
2306
|
+
if (artifactTools.has(toolName) || payload.artifactId || artifactPath) {
|
|
2307
|
+
push(
|
|
2308
|
+
"artifact",
|
|
2309
|
+
`${toolName || "tool"} produced or inspected an artifact`,
|
|
2310
|
+
artifactPath || payload.artifactId || "",
|
|
2311
|
+
{ virtualArtifact: Boolean(payload.artifactId && !artifactPath) }
|
|
2312
|
+
);
|
|
2293
2313
|
}
|
|
2294
2314
|
if (["read_image", "generate_image"].includes(toolName) || /\b(screenshot|visible|thumbnail|preview|image)\b/.test(text)) {
|
|
2295
2315
|
push("visual", `${toolName || "tool"} supplied visual evidence`, payload.path || payload.outputPath || args.path || "");
|
|
@@ -2313,6 +2333,59 @@ function toolPayloadToEvidence(payload = {}, source = "tool") {
|
|
|
2313
2333
|
return evidence;
|
|
2314
2334
|
}
|
|
2315
2335
|
|
|
2336
|
+
const ARTIFACT_EXTENSION_PATTERN = /\.(?:md|json|csv|txt|html?|tex|pdf|docx|pptx|xlsx|png|jpe?g|webp|svg|mp4|mov|mkv|webm|wav|mp3|flac|zip|7z|tar|gz|step|stp|stl|3mf)$/i;
|
|
2337
|
+
const ARTIFACT_PATH_PATTERN = /(?:^|[\s"'`(=])([^\s"'`()=]+\.(?:md|json|csv|txt|html?|tex|pdf|docx|pptx|xlsx|png|jpe?g|webp|svg|mp4|mov|mkv|webm|wav|mp3|flac|zip|7z|tar|gz|step|stp|stl|3mf))(?:$|[\s"'`),;:])/i;
|
|
2338
|
+
|
|
2339
|
+
function firstArtifactPath(...values) {
|
|
2340
|
+
for (const value of values) {
|
|
2341
|
+
const candidate = String(value || "").trim();
|
|
2342
|
+
if (!candidate) continue;
|
|
2343
|
+
if (!/\s/.test(candidate) && ARTIFACT_EXTENSION_PATTERN.test(candidate)) {
|
|
2344
|
+
return candidate;
|
|
2345
|
+
}
|
|
2346
|
+
const match = candidate.match(ARTIFACT_PATH_PATTERN);
|
|
2347
|
+
if (match?.[1]) return match[1];
|
|
2348
|
+
}
|
|
2349
|
+
return "";
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
function revalidateArtifactEvidence(item = {}, state = {}, context = {}) {
|
|
2353
|
+
if (item?.category !== "artifact" || item?.verified === false || item?.virtualArtifact === true) return item;
|
|
2354
|
+
const candidate = firstArtifactPath(item.target, item.proof);
|
|
2355
|
+
if (!candidate) {
|
|
2356
|
+
return item.toolName === "run_command"
|
|
2357
|
+
? {
|
|
2358
|
+
...item,
|
|
2359
|
+
verified: false,
|
|
2360
|
+
proof: `${item.proof || "artifact evidence"}; no durable artifact path was reported`,
|
|
2361
|
+
}
|
|
2362
|
+
: item;
|
|
2363
|
+
}
|
|
2364
|
+
const commandCwd = String(
|
|
2365
|
+
context.commandCwd ||
|
|
2366
|
+
state.commandCwd ||
|
|
2367
|
+
state.meta?.runtimeConfig?.commandCwd ||
|
|
2368
|
+
process.cwd()
|
|
2369
|
+
);
|
|
2370
|
+
const resolved = path.isAbsolute(candidate) ? candidate : path.resolve(commandCwd, candidate);
|
|
2371
|
+
let durable = false;
|
|
2372
|
+
try {
|
|
2373
|
+
const stat = fs.statSync(resolved);
|
|
2374
|
+
durable = stat.isDirectory() || (stat.isFile() && stat.size > 0);
|
|
2375
|
+
} catch {
|
|
2376
|
+
durable = false;
|
|
2377
|
+
}
|
|
2378
|
+
return {
|
|
2379
|
+
...item,
|
|
2380
|
+
target: candidate,
|
|
2381
|
+
resolvedTarget: resolved,
|
|
2382
|
+
verified: durable,
|
|
2383
|
+
proof: durable
|
|
2384
|
+
? item.proof
|
|
2385
|
+
: `${item.proof || "artifact evidence"}; artifact path no longer exists or is empty`,
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2316
2389
|
function messageToEvidence(message = {}) {
|
|
2317
2390
|
if (message.role !== "tool") return [];
|
|
2318
2391
|
try {
|
|
@@ -2327,9 +2400,12 @@ export function buildScsEvidenceLedger({ state = {}, context = {} } = {}) {
|
|
|
2327
2400
|
const messages = Array.isArray(state.messages) ? state.messages : [];
|
|
2328
2401
|
const eventEvidence = events.flatMap(eventToEvidence);
|
|
2329
2402
|
const messageEvidence = messages.flatMap(messageToEvidence);
|
|
2330
|
-
const items = [...eventEvidence, ...messageEvidence]
|
|
2331
|
-
|
|
2332
|
-
|
|
2403
|
+
const items = [...eventEvidence, ...messageEvidence]
|
|
2404
|
+
.slice(-80)
|
|
2405
|
+
.map((item) => revalidateArtifactEvidence(item, state, context));
|
|
2406
|
+
const verifiedItems = items.filter((item) => item?.verified !== false);
|
|
2407
|
+
const categories = unique(verifiedItems.map((item) => item.category));
|
|
2408
|
+
const toolNames = unique(verifiedItems.map((item) => item.toolName).filter(Boolean));
|
|
2333
2409
|
const blockers = [...events.map(eventToBlocker), ...messages.map(messageToBlocker)]
|
|
2334
2410
|
.filter(Boolean)
|
|
2335
2411
|
.slice(-20)
|
package/web.js
CHANGED
|
@@ -1367,6 +1367,17 @@ async function ensureNotRunning(sessionId) {
|
|
|
1367
1367
|
}
|
|
1368
1368
|
|
|
1369
1369
|
async function latestPermissionAdvice(sessionId) {
|
|
1370
|
+
const events = await sessionStore(sessionId).loadEvents().catch(() => []);
|
|
1371
|
+
for (const event of [...events].reverse()) {
|
|
1372
|
+
if (["permission.approval_granted", "permission.approval_declined"].includes(event.type)) return null;
|
|
1373
|
+
if (event.type === "tool.blocked" && event.data?.permissionAdvice) {
|
|
1374
|
+
return {
|
|
1375
|
+
...event.data.permissionAdvice,
|
|
1376
|
+
category: event.data.permissionAdvice.category || event.data.category || "",
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1370
1381
|
const inMemory = runs.get(sessionId);
|
|
1371
1382
|
const memoryEntry = [...(inMemory?.logs || [])]
|
|
1372
1383
|
.reverse()
|
|
@@ -1378,13 +1389,7 @@ async function latestPermissionAdvice(sessionId) {
|
|
|
1378
1389
|
};
|
|
1379
1390
|
}
|
|
1380
1391
|
|
|
1381
|
-
|
|
1382
|
-
const event = [...events].reverse().find((candidate) => candidate.type === "tool.blocked" && candidate.data?.permissionAdvice);
|
|
1383
|
-
if (!event?.data?.permissionAdvice) return null;
|
|
1384
|
-
return {
|
|
1385
|
-
...event.data.permissionAdvice,
|
|
1386
|
-
category: event.data.permissionAdvice.category || event.data.category || "",
|
|
1387
|
-
};
|
|
1392
|
+
return null;
|
|
1388
1393
|
}
|
|
1389
1394
|
|
|
1390
1395
|
function permissionApprovalPrompt(action, advice = {}, originalGoal = "") {
|
|
@@ -2421,6 +2426,12 @@ app.post("/api/sessions/:sessionId/approve-permission", async (req, res) => {
|
|
|
2421
2426
|
source: "web",
|
|
2422
2427
|
category: advice.category || "",
|
|
2423
2428
|
});
|
|
2429
|
+
const state = await store.loadState();
|
|
2430
|
+
if (state?.meta?.pendingPermissionAdvice) {
|
|
2431
|
+
delete state.meta.pendingPermissionAdvice;
|
|
2432
|
+
state.updatedAt = new Date().toISOString();
|
|
2433
|
+
await store.saveState(state);
|
|
2434
|
+
}
|
|
2424
2435
|
const existing = runs.get(sessionId);
|
|
2425
2436
|
if (existing) {
|
|
2426
2437
|
existing.logs.push({
|
|
@@ -2473,6 +2484,11 @@ app.post("/api/sessions/:sessionId/approve-permission", async (req, res) => {
|
|
|
2473
2484
|
category: advice.category || "",
|
|
2474
2485
|
permissionMode: targetMode,
|
|
2475
2486
|
});
|
|
2487
|
+
if (state.meta?.pendingPermissionAdvice) {
|
|
2488
|
+
delete state.meta.pendingPermissionAdvice;
|
|
2489
|
+
state.updatedAt = new Date().toISOString();
|
|
2490
|
+
await store.saveState(state);
|
|
2491
|
+
}
|
|
2476
2492
|
|
|
2477
2493
|
const stored = await loadStoredRun(sessionId);
|
|
2478
2494
|
if (runs.get(sessionId)?.status === "running") {
|