@namzu/sdk 54.0.0 → 55.0.0
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/CHANGELOG.md +32 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +1 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/compaction/extractor.d.ts.map +1 -1
- package/dist/compaction/extractor.js +5 -1
- package/dist/compaction/extractor.js.map +1 -1
- package/dist/compaction/managers/structured.d.ts.map +1 -1
- package/dist/compaction/managers/structured.js +5 -0
- package/dist/compaction/managers/structured.js.map +1 -1
- package/dist/compaction/manual.d.ts.map +1 -1
- package/dist/compaction/manual.js +6 -0
- package/dist/compaction/manual.js.map +1 -1
- package/dist/compaction/serializer.d.ts +6 -0
- package/dist/compaction/serializer.d.ts.map +1 -1
- package/dist/compaction/serializer.js +21 -3
- package/dist/compaction/serializer.js.map +1 -1
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +27 -6
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/constants/compaction/index.d.ts +1 -0
- package/dist/constants/compaction/index.d.ts.map +1 -1
- package/dist/constants/compaction/index.js +1 -0
- package/dist/constants/compaction/index.js.map +1 -1
- package/dist/manager/resident/proposal.d.ts +2 -2
- package/dist/manager/resident/store.d.ts +4 -4
- package/dist/prompt/coding-agent-doctrine.d.ts +3 -3
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +18 -20
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/checkpoint.d.ts +12 -2
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +42 -8
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +39 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +115 -7
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +18 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +20 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +17 -8
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +24 -2
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js +22 -22
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/per-call-review.d.ts +78 -0
- package/dist/runtime/query/iteration/phases/per-call-review.d.ts.map +1 -0
- package/dist/runtime/query/iteration/phases/per-call-review.js +494 -0
- package/dist/runtime/query/iteration/phases/per-call-review.js.map +1 -0
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +129 -62
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/step-context.d.ts +14 -0
- package/dist/runtime/query/iteration/step-context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-context.js +29 -0
- package/dist/runtime/query/iteration/step-context.js.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.d.ts.map +1 -1
- package/dist/runtime/query/iteration/step-shaping.js +6 -2
- package/dist/runtime/query/iteration/step-shaping.js.map +1 -1
- package/dist/runtime/query/prepare-turn.d.ts.map +1 -1
- package/dist/runtime/query/prepare-turn.js +16 -3
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/runtime/query/project-instructions.d.ts +24 -5
- package/dist/runtime/query/project-instructions.d.ts.map +1 -1
- package/dist/runtime/query/project-instructions.js +107 -35
- package/dist/runtime/query/project-instructions.js.map +1 -1
- package/dist/runtime/query/prompt.d.ts +6 -0
- package/dist/runtime/query/prompt.d.ts.map +1 -1
- package/dist/runtime/query/prompt.js +10 -49
- package/dist/runtime/query/prompt.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts +8 -0
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +101 -0
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +18 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +10 -0
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +55 -56
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +78 -72
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +8 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +13 -18
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/browser.d.ts.map +1 -1
- package/dist/tools/builtins/browser.js +38 -12
- package/dist/tools/builtins/browser.js.map +1 -1
- package/dist/tools/builtins/edit.js +4 -4
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/job.js +1 -1
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/skill.js +1 -1
- package/dist/tools/builtins/skill.js.map +1 -1
- package/dist/tools/builtins/structuredOutput.js +1 -1
- package/dist/tools/builtins/structuredOutput.js.map +1 -1
- package/dist/tools/builtins/verify-outputs.js +1 -1
- package/dist/tools/builtins/verify-outputs.js.map +1 -1
- package/dist/tools/builtins/write-file.js +4 -4
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/ask-user-question.d.ts +6 -0
- package/dist/tools/coordinator/ask-user-question.d.ts.map +1 -1
- package/dist/tools/coordinator/ask-user-question.js +6 -1
- package/dist/tools/coordinator/ask-user-question.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +10 -3
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/schedules/loop-tool.d.ts.map +1 -1
- package/dist/tools/schedules/loop-tool.js +4 -2
- package/dist/tools/schedules/loop-tool.js.map +1 -1
- package/dist/tools/schedules/schedule-tool.js +2 -2
- package/dist/tools/schedules/schedule-tool.js.map +1 -1
- package/dist/tools/task/create.js +1 -1
- package/dist/tools/task/create.js.map +1 -1
- package/dist/tools/untrusted-envelope.js +1 -1
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/turn/evidence-query.d.ts.map +1 -1
- package/dist/turn/evidence-query.js +16 -1
- package/dist/turn/evidence-query.js.map +1 -1
- package/dist/turn/evidence-recall.js +1 -1
- package/dist/turn/evidence-recall.js.map +1 -1
- package/dist/types/agent/base.d.ts +7 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +66 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +8 -0
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +12 -2
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/message/notice-frame.d.ts +41 -0
- package/dist/types/message/notice-frame.d.ts.map +1 -0
- package/dist/types/message/notice-frame.js +65 -0
- package/dist/types/message/notice-frame.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/compaction/extractor.ts +8 -1
- package/src/compaction/managers/structured.ts +3 -0
- package/src/compaction/manual.ts +5 -0
- package/src/compaction/serializer.ts +28 -3
- package/src/compaction/verifier.ts +32 -6
- package/src/constants/compaction/index.ts +1 -0
- package/src/prompt/coding-agent-doctrine.ts +18 -20
- package/src/public-runtime.ts +1 -0
- package/src/runtime/query/checkpoint.ts +49 -8
- package/src/runtime/query/executor.ts +165 -15
- package/src/runtime/query/index.ts +45 -1
- package/src/runtime/query/iteration/index.ts +19 -6
- package/src/runtime/query/iteration/phases/context.ts +37 -11
- package/src/runtime/query/iteration/phases/per-call-review.ts +598 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +168 -83
- package/src/runtime/query/iteration/step-context.ts +29 -0
- package/src/runtime/query/iteration/step-shaping.ts +6 -2
- package/src/runtime/query/prepare-turn.ts +18 -1
- package/src/runtime/query/project-instructions.ts +119 -38
- package/src/runtime/query/prompt.ts +11 -54
- package/src/runtime/query/resume-pending.ts +131 -0
- package/src/runtime/query/review-policy.ts +28 -0
- package/src/runtime/query/steering.ts +100 -96
- package/src/tools/builtins/bash.ts +13 -19
- package/src/tools/builtins/browser.ts +38 -12
- package/src/tools/builtins/edit.ts +4 -4
- package/src/tools/builtins/job.ts +1 -1
- package/src/tools/builtins/skill.ts +1 -1
- package/src/tools/builtins/structuredOutput.ts +1 -1
- package/src/tools/builtins/verify-outputs.ts +1 -1
- package/src/tools/builtins/write-file.ts +4 -4
- package/src/tools/coordinator/ask-user-question.ts +12 -1
- package/src/tools/coordinator/index.ts +10 -3
- package/src/tools/schedules/loop-tool.ts +4 -2
- package/src/tools/schedules/schedule-tool.ts +2 -2
- package/src/tools/task/create.ts +1 -1
- package/src/tools/untrusted-envelope.ts +1 -1
- package/src/turn/evidence-query.ts +19 -1
- package/src/turn/evidence-recall.ts +1 -1
- package/src/types/agent/base.ts +8 -0
- package/src/types/hitl/index.ts +67 -0
- package/src/types/message/index.ts +20 -2
- package/src/types/message/notice-frame.ts +88 -0
|
@@ -2,20 +2,35 @@ import { allowRuleScope } from '../../../../authorization/gate.js'
|
|
|
2
2
|
import type { AuthorizationGate } from '../../../../authorization/index.js'
|
|
3
3
|
import { isTrustedReadOnly } from '../../../../tools/trusted-read-only.js'
|
|
4
4
|
import type { ToolSourceRef } from '../../../../toolsets/types.js'
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
HITLResumeDecision,
|
|
7
|
+
ToolCallDeclined,
|
|
8
|
+
ToolCallSummary,
|
|
9
|
+
} from '../../../../types/hitl/index.js'
|
|
6
10
|
import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
|
|
7
11
|
import type { SessionEvent } from '../../../../types/session/index.js'
|
|
8
12
|
import type { ShellDialect } from '../../../../types/tool/index.js'
|
|
9
13
|
import { DECLINED_TOOL_CALL_FEEDBACK } from '../../declined.js'
|
|
10
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
PreparedToolBatch,
|
|
16
|
+
ReviewedExecution,
|
|
17
|
+
ToolCallDeclines,
|
|
18
|
+
ToolCallDenials,
|
|
19
|
+
} from '../../executor.js'
|
|
11
20
|
import {
|
|
12
21
|
awaitProjectInstructionCallback,
|
|
13
22
|
replaceProjectInstructionSnapshot,
|
|
14
23
|
} from '../../project-instructions.js'
|
|
15
24
|
import { attachRepeatNotice } from '../../repeat-call.js'
|
|
16
25
|
import { isReviewExempt } from '../../review-policy.js'
|
|
17
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
formatJobNote,
|
|
28
|
+
formatMonitorNote,
|
|
29
|
+
takeNoticeMessage,
|
|
30
|
+
takeSteeringMessage,
|
|
31
|
+
} from '../../steering.js'
|
|
18
32
|
import { type IterationContext, awaitDecisionDurably } from './context.js'
|
|
33
|
+
import { PerCallReview } from './per-call-review.js'
|
|
19
34
|
|
|
20
35
|
interface VerificationAwareContext extends IterationContext {
|
|
21
36
|
readonly verificationGate?: AuthorizationGate
|
|
@@ -197,7 +212,10 @@ export async function* runToolReview(
|
|
|
197
212
|
let declines: ToolCallDeclines | undefined
|
|
198
213
|
|
|
199
214
|
/** Executes the batch, answering every call, and appends the results. */
|
|
200
|
-
const settleBatch = async (
|
|
215
|
+
const settleBatch = async (
|
|
216
|
+
denials?: ToolCallDenials,
|
|
217
|
+
review?: ReviewedExecution,
|
|
218
|
+
): Promise<void> => {
|
|
201
219
|
const startedAt = Date.now()
|
|
202
220
|
const batch = await ctx.toolExecutor.executeBatch(
|
|
203
221
|
response,
|
|
@@ -205,6 +223,7 @@ export async function* runToolReview(
|
|
|
205
223
|
undefined,
|
|
206
224
|
preparedBatch,
|
|
207
225
|
declines,
|
|
226
|
+
review,
|
|
208
227
|
)
|
|
209
228
|
toolMs += Date.now() - startedAt
|
|
210
229
|
executed = batch.results
|
|
@@ -240,40 +259,42 @@ export async function* runToolReview(
|
|
|
240
259
|
const notice = ctx.repeatCalls?.record(summary.name, summary.input)
|
|
241
260
|
if (notice) notices.push(notice)
|
|
242
261
|
}
|
|
243
|
-
//
|
|
244
|
-
// the
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
// delivery a denial already uses, without the refusal.
|
|
262
|
+
// What the host or the kernel queued while this batch was running reaches
|
|
263
|
+
// the model as messages of their own right after the batch. A tool result
|
|
264
|
+
// stays what the tool returned: instructions written into it read as tool
|
|
265
|
+
// output, and a `tool_use` block still gets its `tool_result` first.
|
|
248
266
|
let jobNoticeDelivered = false
|
|
249
267
|
let monitorNoticeDelivered = false
|
|
250
|
-
for (const msg of attachRepeatNotice(
|
|
251
|
-
attachNotice(
|
|
252
|
-
attachNotice(
|
|
253
|
-
attachSteering(batch.messages, ctx.steering, ctx.onSteeringDelivered),
|
|
254
|
-
ctx.jobNotices,
|
|
255
|
-
formatJobNote,
|
|
256
|
-
// The exits that text accounts for have now been read, so the
|
|
257
|
-
// record of them stops being pending work. Left standing, it
|
|
258
|
-
// buys the model a turn the next time any job queues a notice.
|
|
259
|
-
() => {
|
|
260
|
-
ctx.awaitedJobs?.noticesDelivered()
|
|
261
|
-
jobNoticeDelivered = true
|
|
262
|
-
},
|
|
263
|
-
),
|
|
264
|
-
ctx.monitorNotices,
|
|
265
|
-
formatMonitorNote,
|
|
266
|
-
// Same rule for what a monitor said: read here, so a turn holding
|
|
267
|
-
// itself open for the monitor does not spend a turn re-reading it.
|
|
268
|
-
() => {
|
|
269
|
-
ctx.awaitedJobs?.monitorNoticesDelivered()
|
|
270
|
-
monitorNoticeDelivered = true
|
|
271
|
-
},
|
|
272
|
-
),
|
|
273
|
-
notices,
|
|
274
|
-
)) {
|
|
268
|
+
for (const msg of attachRepeatNotice(batch.messages, notices)) {
|
|
275
269
|
ctx.recorder.pushMessage(msg)
|
|
276
270
|
}
|
|
271
|
+
const steering = takeSteeringMessage(batch.messages, ctx.steering, ctx.onSteeringDelivered)
|
|
272
|
+
if (steering) ctx.recorder.pushMessage(steering)
|
|
273
|
+
const jobNotice = takeNoticeMessage(
|
|
274
|
+
batch.messages,
|
|
275
|
+
ctx.jobNotices,
|
|
276
|
+
formatJobNote,
|
|
277
|
+
// The exits that text accounts for have now been read, so the
|
|
278
|
+
// record of them stops being pending work. Left standing, it
|
|
279
|
+
// buys the model a turn the next time any job queues a notice.
|
|
280
|
+
() => {
|
|
281
|
+
ctx.awaitedJobs?.noticesDelivered()
|
|
282
|
+
jobNoticeDelivered = true
|
|
283
|
+
},
|
|
284
|
+
)
|
|
285
|
+
if (jobNotice) ctx.recorder.pushMessage(jobNotice)
|
|
286
|
+
const monitorNotice = takeNoticeMessage(
|
|
287
|
+
batch.messages,
|
|
288
|
+
ctx.monitorNotices,
|
|
289
|
+
formatMonitorNote,
|
|
290
|
+
// Same rule for what a monitor said: read here, so a turn holding
|
|
291
|
+
// itself open for the monitor does not spend a turn re-reading it.
|
|
292
|
+
() => {
|
|
293
|
+
ctx.awaitedJobs?.monitorNoticesDelivered()
|
|
294
|
+
monitorNoticeDelivered = true
|
|
295
|
+
},
|
|
296
|
+
)
|
|
297
|
+
if (monitorNotice) ctx.recorder.pushMessage(monitorNotice)
|
|
277
298
|
if (jobNoticeDelivered) ctx.onJobNoticeDelivered?.()
|
|
278
299
|
if (monitorNoticeDelivered) ctx.onMonitorNoticeDelivered?.()
|
|
279
300
|
// The complete tool-result batch is already in history before host policy
|
|
@@ -305,9 +326,10 @@ export async function* runToolReview(
|
|
|
305
326
|
/** Stream recorded starts and progress while execution itself remains pending. */
|
|
306
327
|
const settle = async function* (
|
|
307
328
|
denials?: ToolCallDenials,
|
|
329
|
+
review?: ReviewedExecution,
|
|
308
330
|
): AsyncGenerator<SessionEvent, void, unknown> {
|
|
309
331
|
if (!ctx.onPendingEvents) {
|
|
310
|
-
await settleBatch(denials)
|
|
332
|
+
await settleBatch(denials, review)
|
|
311
333
|
return
|
|
312
334
|
}
|
|
313
335
|
const wakeup = () => {
|
|
@@ -322,7 +344,7 @@ export async function* runToolReview(
|
|
|
322
344
|
const unsubscribe = ctx.onPendingEvents(() => wake.notify())
|
|
323
345
|
// Start the captured batch before yielding. Live observation must not
|
|
324
346
|
// create a new mutation window before execution validates its payload.
|
|
325
|
-
const execution = settleBatch(denials).then(
|
|
347
|
+
const execution = settleBatch(denials, review).then(
|
|
326
348
|
() => {
|
|
327
349
|
settled = { ok: true }
|
|
328
350
|
wake.notify()
|
|
@@ -711,24 +733,6 @@ export async function* runToolReview(
|
|
|
711
733
|
}
|
|
712
734
|
}
|
|
713
735
|
|
|
714
|
-
const reviewCheckpoint = await ctx.checkpointMgr.create(ctx.recorder, iterationNum)
|
|
715
|
-
|
|
716
|
-
await ctx.emitEvent({
|
|
717
|
-
type: 'tool_review_requested',
|
|
718
|
-
turnId: ctx.recorder.turnId,
|
|
719
|
-
toolCalls: toolCallSummaries,
|
|
720
|
-
iteration: iterationNum,
|
|
721
|
-
})
|
|
722
|
-
yield* ctx.drainPending()
|
|
723
|
-
|
|
724
|
-
const reviewDecision = await awaitDecisionDurably(ctx, reviewCheckpoint, {
|
|
725
|
-
type: 'tool_review',
|
|
726
|
-
sessionId: ctx.recorder.sessionId,
|
|
727
|
-
turnId: ctx.recorder.turnId,
|
|
728
|
-
checkpointId: reviewCheckpoint.id,
|
|
729
|
-
toolCalls: toolCallSummaries,
|
|
730
|
-
})
|
|
731
|
-
|
|
732
736
|
/**
|
|
733
737
|
* The escalations this decision lets through, on the record, and the
|
|
734
738
|
* escapes it did not confirm, refused.
|
|
@@ -742,8 +746,9 @@ export async function* runToolReview(
|
|
|
742
746
|
const settleEscalations = async (
|
|
743
747
|
denials: Map<string, string>,
|
|
744
748
|
confirmed: readonly string[] | undefined,
|
|
749
|
+
only?: readonly ToolCallSummary[],
|
|
745
750
|
): Promise<void> => {
|
|
746
|
-
for (const tc of escalated()) {
|
|
751
|
+
for (const tc of only ? only.filter((call) => call.escalation !== undefined) : escalated()) {
|
|
747
752
|
const denied = denials.get(tc.id)
|
|
748
753
|
if (denied !== undefined) {
|
|
749
754
|
// A crossing that was asked about and refused is on the record
|
|
@@ -825,6 +830,105 @@ export async function* runToolReview(
|
|
|
825
830
|
}
|
|
826
831
|
}
|
|
827
832
|
|
|
833
|
+
/**
|
|
834
|
+
* The record of calls approved without anyone being asked: by a skill's
|
|
835
|
+
* `allowed-tools`, or by a `pre_tool_use` hook's `allow`. Only a call that
|
|
836
|
+
* carried the mark counts: a policy that lists an unmarked id has not been
|
|
837
|
+
* given a skill's or a hook's word for it.
|
|
838
|
+
*/
|
|
839
|
+
const recordGrantAudits = async (
|
|
840
|
+
decision: HITLResumeDecision,
|
|
841
|
+
calls: readonly ToolCallSummary[],
|
|
842
|
+
): Promise<void> => {
|
|
843
|
+
if (decision.action !== 'approve_tools') return
|
|
844
|
+
if (decision.skillGranted) {
|
|
845
|
+
const listed = new Set(decision.skillGranted)
|
|
846
|
+
for (const tc of calls) {
|
|
847
|
+
if (!listed.has(tc.id) || !tc.skillGrant || gateDenied.has(tc.id)) continue
|
|
848
|
+
await ctx.recorder.recordAudit({
|
|
849
|
+
what: { action: 'tool_call', tool: tc.name },
|
|
850
|
+
outcome: 'approved',
|
|
851
|
+
reason: `pre-approved by the allowed-tools of skill "${tc.skillGrant.skill}" for this turn; nobody was asked`,
|
|
852
|
+
})
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
if (decision.hookGranted) {
|
|
856
|
+
const listed = new Set(decision.hookGranted)
|
|
857
|
+
for (const tc of calls) {
|
|
858
|
+
if (!listed.has(tc.id) || !tc.hookGrant || gateDenied.has(tc.id)) continue
|
|
859
|
+
await ctx.recorder.recordAudit({
|
|
860
|
+
what: { action: 'tool_call', tool: tc.name },
|
|
861
|
+
outcome: 'approved',
|
|
862
|
+
reason: `pre-approved by a pre_tool_use hook${tc.hookGrant.reason ? ` (${tc.hookGrant.reason})` : ''}; nobody was asked`,
|
|
863
|
+
})
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Opt-in: ask about one call at a time while the batch runs. Everything
|
|
869
|
+
// above has already run on the batch as a whole, because those checks are
|
|
870
|
+
// about the calls and not about who answers.
|
|
871
|
+
if (ctx.perCallReview) {
|
|
872
|
+
const reviewedIds = new Set(toolCallSummaries.map((tc) => tc.id))
|
|
873
|
+
const walk = new PerCallReview({
|
|
874
|
+
ctx,
|
|
875
|
+
iteration: iterationNum,
|
|
876
|
+
toolCalls,
|
|
877
|
+
summaries: () => toolCallSummaries,
|
|
878
|
+
gateDenied,
|
|
879
|
+
reviewAllowed,
|
|
880
|
+
unreviewedIds: new Set(
|
|
881
|
+
preparedBatch ? toolCalls.filter((tc) => !reviewedIds.has(tc.id)).map((tc) => tc.id) : [],
|
|
882
|
+
),
|
|
883
|
+
settleEscalations,
|
|
884
|
+
recordGrantAudits,
|
|
885
|
+
evaluateGate: (tc) =>
|
|
886
|
+
ctx.verificationGate?.evaluate({
|
|
887
|
+
toolName: tc.name,
|
|
888
|
+
toolInput: tc.input,
|
|
889
|
+
toolDef: ctx.tools.get(tc.name),
|
|
890
|
+
...dialectFor(tc.name),
|
|
891
|
+
...sourceFor(tc.name),
|
|
892
|
+
}).decision,
|
|
893
|
+
})
|
|
894
|
+
try {
|
|
895
|
+
await walk.askTogetherFirst()
|
|
896
|
+
const denials = new Map([...gateDenied, ...walk.denials])
|
|
897
|
+
if (walk.declines.size > 0) declines = walk.declines
|
|
898
|
+
yield* settle(denials, walk)
|
|
899
|
+
yield* ctx.drainPending()
|
|
900
|
+
if (walk.endsTurn) {
|
|
901
|
+
// Every call is answered by now. The turn stops the way an
|
|
902
|
+
// interrupt stops it, so nothing downstream reads the stop as a
|
|
903
|
+
// result the model should react to.
|
|
904
|
+
ctx.recorder.setStopReason('cancelled')
|
|
905
|
+
ctx.recorder.markCancelled()
|
|
906
|
+
return finish('stop')
|
|
907
|
+
}
|
|
908
|
+
return finish(denials.size + walk.refusedCount >= toolCalls.length ? 'rejected' : 'executed')
|
|
909
|
+
} finally {
|
|
910
|
+
walk.dispose()
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const reviewCheckpoint = await ctx.checkpointMgr.create(ctx.recorder, iterationNum)
|
|
915
|
+
|
|
916
|
+
await ctx.emitEvent({
|
|
917
|
+
type: 'tool_review_requested',
|
|
918
|
+
turnId: ctx.recorder.turnId,
|
|
919
|
+
toolCalls: toolCallSummaries,
|
|
920
|
+
iteration: iterationNum,
|
|
921
|
+
})
|
|
922
|
+
yield* ctx.drainPending()
|
|
923
|
+
|
|
924
|
+
const reviewDecision = await awaitDecisionDurably(ctx, reviewCheckpoint, {
|
|
925
|
+
type: 'tool_review',
|
|
926
|
+
sessionId: ctx.recorder.sessionId,
|
|
927
|
+
turnId: ctx.recorder.turnId,
|
|
928
|
+
checkpointId: reviewCheckpoint.id,
|
|
929
|
+
toolCalls: toolCallSummaries,
|
|
930
|
+
})
|
|
931
|
+
|
|
828
932
|
switch (reviewDecision.action) {
|
|
829
933
|
case 'reject_tools': {
|
|
830
934
|
await ctx.emitEvent({
|
|
@@ -846,6 +950,12 @@ export async function* runToolReview(
|
|
|
846
950
|
await settleEscalations(denials, undefined)
|
|
847
951
|
yield* settle(denials)
|
|
848
952
|
yield* ctx.drainPending()
|
|
953
|
+
if (reviewDecision.endTurn) {
|
|
954
|
+
// Every call is answered above; the turn then stops as an interrupt does.
|
|
955
|
+
ctx.recorder.setStopReason('cancelled')
|
|
956
|
+
ctx.recorder.markCancelled()
|
|
957
|
+
return finish('stop')
|
|
958
|
+
}
|
|
849
959
|
return finish('rejected')
|
|
850
960
|
}
|
|
851
961
|
|
|
@@ -961,34 +1071,9 @@ export async function* runToolReview(
|
|
|
961
1071
|
if (reviewDecision.action === 'approve_tools' && reviewDecision.remember) {
|
|
962
1072
|
ctx.toolGrants?.grant(reviewDecision.remember)
|
|
963
1073
|
}
|
|
964
|
-
// A call nobody was asked about, approved because a skill
|
|
965
|
-
// is on the record naming
|
|
966
|
-
|
|
967
|
-
// given a skill's word for it.
|
|
968
|
-
if (reviewDecision.action === 'approve_tools' && reviewDecision.skillGranted) {
|
|
969
|
-
const listed = new Set(reviewDecision.skillGranted)
|
|
970
|
-
for (const tc of toolCallSummaries) {
|
|
971
|
-
if (!listed.has(tc.id) || !tc.skillGrant || gateDenied.has(tc.id)) continue
|
|
972
|
-
await ctx.recorder.recordAudit({
|
|
973
|
-
what: { action: 'tool_call', tool: tc.name },
|
|
974
|
-
outcome: 'approved',
|
|
975
|
-
reason: `pre-approved by the allowed-tools of skill "${tc.skillGrant.skill}" for this turn; nobody was asked`,
|
|
976
|
-
})
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// The same for a call a hook's `allow` approved.
|
|
981
|
-
if (reviewDecision.action === 'approve_tools' && reviewDecision.hookGranted) {
|
|
982
|
-
const listed = new Set(reviewDecision.hookGranted)
|
|
983
|
-
for (const tc of toolCallSummaries) {
|
|
984
|
-
if (!listed.has(tc.id) || !tc.hookGrant || gateDenied.has(tc.id)) continue
|
|
985
|
-
await ctx.recorder.recordAudit({
|
|
986
|
-
what: { action: 'tool_call', tool: tc.name },
|
|
987
|
-
outcome: 'approved',
|
|
988
|
-
reason: `pre-approved by a pre_tool_use hook${tc.hookGrant.reason ? ` (${tc.hookGrant.reason})` : ''}; nobody was asked`,
|
|
989
|
-
})
|
|
990
|
-
}
|
|
991
|
-
}
|
|
1074
|
+
// A call nobody was asked about, approved because a skill or a hook
|
|
1075
|
+
// said so, is on the record naming which.
|
|
1076
|
+
await recordGrantAudits(reviewDecision, toolCallSummaries)
|
|
992
1077
|
|
|
993
1078
|
await ctx.emitEvent({
|
|
994
1079
|
type: 'tool_review_completed',
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createRuntimeContextMessage } from '../../../types/message/index.js'
|
|
2
|
+
import type { Message } from '../../../types/message/index.js'
|
|
3
|
+
import { isOperatorUserMessage } from '../steering.js'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Request-only context, as the model reads it: a runtime-context user
|
|
@@ -15,3 +17,30 @@ export function stepContextMessage(content: string) {
|
|
|
15
17
|
'step-context',
|
|
16
18
|
)
|
|
17
19
|
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Put `blocks` in front of the operator's new message when that message is
|
|
23
|
+
* what the model is about to answer, so the request is the last thing it
|
|
24
|
+
* reads and the longest data comes before it. Only the reminders and other
|
|
25
|
+
* request-only context may follow that message: once the turn has an
|
|
26
|
+
* assistant or tool message after it, the blocks stay at the end of the
|
|
27
|
+
* history, where a caching driver ends its breakpoint before them, so the
|
|
28
|
+
* tool loop of a long turn is not re-read at full price on every step.
|
|
29
|
+
*
|
|
30
|
+
* Reminders and notices that follow the operator's message stay where they
|
|
31
|
+
* are: they are short, and the edge after the request is where they are read.
|
|
32
|
+
*/
|
|
33
|
+
export function placeBeforeLatestOperatorMessage(history: Message[], blocks: readonly Message[]) {
|
|
34
|
+
if (blocks.length === 0) return
|
|
35
|
+
let at = history.length
|
|
36
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
37
|
+
const message = history[i] as Message
|
|
38
|
+
if (isOperatorUserMessage(message)) {
|
|
39
|
+
at = i
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
// A reply or a tool result after the message: the model has moved on.
|
|
43
|
+
if (message.role === 'assistant' || message.role === 'tool') break
|
|
44
|
+
}
|
|
45
|
+
history.splice(at, 0, ...blocks)
|
|
46
|
+
}
|
|
@@ -20,7 +20,7 @@ import { toErrorMessage } from '../../../utils/error.js'
|
|
|
20
20
|
import { createCallbackInference } from '../callback-inference.js'
|
|
21
21
|
import { measureContext } from './phases/compaction.js'
|
|
22
22
|
import type { IterationContext } from './phases/index.js'
|
|
23
|
-
import { stepContextMessage } from './step-context.js'
|
|
23
|
+
import { placeBeforeLatestOperatorMessage, stepContextMessage } from './step-context.js'
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* How the next request is shaped: admission, preparation, and the context
|
|
@@ -58,6 +58,7 @@ export function appendWorkContext(
|
|
|
58
58
|
ctx.toolExecutor.describeFileEvidence(messages),
|
|
59
59
|
].filter((content): content is string => Boolean(content))
|
|
60
60
|
if (contributions.length === 0) return
|
|
61
|
+
const placed: Message[] = []
|
|
61
62
|
let room = stepContext(shaping, stepNumber, prepared).contextBudget?.remainingTokens ?? 0
|
|
62
63
|
// Leave room for the actual task; admit whole contributions, never dangling partial references.
|
|
63
64
|
if (room < 1_500) return
|
|
@@ -66,9 +67,12 @@ export function appendWorkContext(
|
|
|
66
67
|
const message = stepContextMessage(content)
|
|
67
68
|
const tokens = estimateMessageTokens(message)
|
|
68
69
|
if (tokens > Math.min(2_000, room - 1_000)) continue
|
|
69
|
-
|
|
70
|
+
placed.push(message)
|
|
70
71
|
room -= tokens
|
|
71
72
|
}
|
|
73
|
+
// Per-turn data goes in front of the operator's new message while that is
|
|
74
|
+
// what the model is answering, like the other blocks of request-only data.
|
|
75
|
+
placeBeforeLatestOperatorMessage(messages, placed)
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
export function stepContext(
|
|
@@ -53,7 +53,9 @@ import {
|
|
|
53
53
|
awaitProjectInstructionCallback,
|
|
54
54
|
collapseProjectInstructionSnapshots,
|
|
55
55
|
isProjectInstructionMessage,
|
|
56
|
+
latestProjectInstructionSnapshot,
|
|
56
57
|
replaceProjectInstructionSnapshot,
|
|
58
|
+
sameProjectInstructionSnapshot,
|
|
57
59
|
} from './project-instructions.js'
|
|
58
60
|
import type { PromptCache } from './prompt-cache.js'
|
|
59
61
|
import { resolveMaxRequestRichContentBytes } from './request-rich-content.js'
|
|
@@ -640,11 +642,26 @@ export async function prepareTurn(params: QueryParams): Promise<PreparedTurn> {
|
|
|
640
642
|
// be erased merely because cancellation arrived before this catch ran.
|
|
641
643
|
if (!preparationSignal.aborted || error !== preparationSignal.reason) throw error
|
|
642
644
|
}
|
|
643
|
-
|
|
645
|
+
// The snapshot a conversation already holds stays where it is. Moving an
|
|
646
|
+
// unchanged copy to this turn's front would rewrite everything after its
|
|
647
|
+
// old position on every request, so the provider's cache would reuse
|
|
648
|
+
// almost none of the history. Only a changed snapshot is sent again, at
|
|
649
|
+
// the tail just before the new message, and it replaces the old copy.
|
|
650
|
+
const standing = latestProjectInstructionSnapshot([
|
|
651
|
+
...history.map((entry) => entry.message),
|
|
652
|
+
...resolvedInitialMessages,
|
|
653
|
+
])
|
|
654
|
+
const unchanged =
|
|
655
|
+
snapshot !== undefined &&
|
|
656
|
+
snapshot !== null &&
|
|
657
|
+
standing !== undefined &&
|
|
658
|
+
sameProjectInstructionSnapshot(standing, snapshot)
|
|
659
|
+
if (snapshot !== undefined && !unchanged) {
|
|
644
660
|
resolvedInitialMessages = replaceProjectInstructionSnapshot(
|
|
645
661
|
resolvedInitialMessages,
|
|
646
662
|
snapshot,
|
|
647
663
|
'before-latest-user',
|
|
664
|
+
history.map((entry) => entry.message),
|
|
648
665
|
)
|
|
649
666
|
}
|
|
650
667
|
}
|
|
@@ -126,10 +126,31 @@ function assertsProjectInstructionMessage(message: UserMessage): asserts message
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
* The line that opens a changed copy of the project instructions. The copy is
|
|
130
|
+
* complete, so the model can drop the earlier one outright.
|
|
131
|
+
*/
|
|
132
|
+
export const PROJECT_INSTRUCTION_UPDATE_HEADER =
|
|
133
|
+
'Updated project instructions. These replace the project instructions given earlier in this conversation in full; a rule that appears only in the earlier copy no longer applies.\n\n'
|
|
134
|
+
|
|
135
|
+
function isUpdate(message: UserMessage): boolean {
|
|
136
|
+
return message.source?.type === 'project-instructions' && message.source.update === true
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The policy text of a snapshot, without the header an update carries. */
|
|
140
|
+
export function projectInstructionBody(message: UserMessage): string {
|
|
141
|
+
const content = typeof message.content === 'string' ? message.content : ''
|
|
142
|
+
return isUpdate(message) && content.startsWith(PROJECT_INSTRUCTION_UPDATE_HEADER)
|
|
143
|
+
? content.slice(PROJECT_INSTRUCTION_UPDATE_HEADER.length)
|
|
144
|
+
: content
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Keep the project policy a request needs, each at its own chronological
|
|
149
|
+
* position. A plain snapshot supersedes every earlier one. A changed copy
|
|
150
|
+
* (`source.update`) supersedes the copies before it but leaves the first one
|
|
151
|
+
* in place, so the request keeps its shape up to the change. Invalid tagged
|
|
152
|
+
* provenance refuses before a provider can be called; an untrusted persisted
|
|
153
|
+
* object cannot acquire policy status by spelling one discriminator.
|
|
133
154
|
*/
|
|
134
155
|
export function collapseProjectInstructionSnapshots(messages: readonly Message[]): Message[] {
|
|
135
156
|
let latest = -1
|
|
@@ -140,9 +161,21 @@ export function collapseProjectInstructionSnapshots(messages: readonly Message[]
|
|
|
140
161
|
latest = index
|
|
141
162
|
}
|
|
142
163
|
if (latest < 0) return [...messages]
|
|
164
|
+
const latestMessage = messages[latest] as UserMessage
|
|
165
|
+
// The base is the earliest copy, kept only while the latest is an update of it.
|
|
166
|
+
let base = -1
|
|
167
|
+
if (isUpdate(latestMessage)) {
|
|
168
|
+
for (let index = 0; index < latest; index += 1) {
|
|
169
|
+
const message = messages[index]
|
|
170
|
+
if (message && isProjectInstructionMessage(message)) {
|
|
171
|
+
base = index
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
143
176
|
return messages.flatMap((message, index) => {
|
|
144
177
|
if (message.role === 'user' && message.source?.type === 'project-instructions') {
|
|
145
|
-
return index === latest
|
|
178
|
+
return index === latest || index === base
|
|
146
179
|
? [message.retain === true ? message : { ...message, retain: true }]
|
|
147
180
|
: []
|
|
148
181
|
}
|
|
@@ -150,9 +183,42 @@ export function collapseProjectInstructionSnapshots(messages: readonly Message[]
|
|
|
150
183
|
})
|
|
151
184
|
}
|
|
152
185
|
|
|
186
|
+
function canonical(message: UserMessage): unknown {
|
|
187
|
+
// Through JSON, as the durable record went: an explicitly undefined field and
|
|
188
|
+
// a missing one are the same message.
|
|
189
|
+
return JSON.parse(
|
|
190
|
+
JSON.stringify({
|
|
191
|
+
content: projectInstructionBody(message),
|
|
192
|
+
attachments: message.attachments,
|
|
193
|
+
source: message.source ? { ...message.source, update: undefined } : message.source,
|
|
194
|
+
cacheHint: message.cacheHint,
|
|
195
|
+
}),
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Whether two snapshots say the same thing, ignoring ids, timestamps and `retain`. */
|
|
200
|
+
export function sameProjectInstructionSnapshot(a: UserMessage, b: UserMessage): boolean {
|
|
201
|
+
return isDeepStrictEqual(canonical(a), canonical(b))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The latest project-instruction snapshot in `messages`, if there is one. */
|
|
205
|
+
export function latestProjectInstructionSnapshot(
|
|
206
|
+
messages: readonly Message[],
|
|
207
|
+
): UserMessage | undefined {
|
|
208
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
209
|
+
const message = messages[index]
|
|
210
|
+
if (message && isProjectInstructionMessage(message)) return message
|
|
211
|
+
}
|
|
212
|
+
return undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
153
215
|
/**
|
|
154
216
|
* Replace project policy without mutating the caller's history.
|
|
155
217
|
*
|
|
218
|
+
* The first copy a conversation holds keeps its place. A changed snapshot is
|
|
219
|
+
* sent as a complete update at the tail (`source.update`), which replaces the
|
|
220
|
+
* earlier updates and tells the model it supersedes the first copy; the
|
|
221
|
+
* request is unchanged up to the update, so a provider's cache still covers it.
|
|
156
222
|
* Runtime updates append after the finalized tool batch. A host seeding a new
|
|
157
223
|
* human turn uses `before-latest-user`, so the policy is context for that
|
|
158
224
|
* request rather than a reply after it.
|
|
@@ -161,53 +227,68 @@ export function replaceProjectInstructionSnapshot(
|
|
|
161
227
|
messages: readonly Message[],
|
|
162
228
|
snapshot: UserMessage | null,
|
|
163
229
|
placement: 'append' | 'before-latest-user' = 'append',
|
|
230
|
+
/** Messages the conversation already holds ahead of `messages`; read for the first copy, never changed. */
|
|
231
|
+
held: readonly Message[] = [],
|
|
164
232
|
): Message[] {
|
|
165
233
|
if (snapshot) assertsProjectInstructionMessage(snapshot)
|
|
166
234
|
// Validate every tagged predecessor before removing it. A host-authoritative
|
|
167
235
|
// replacement must not turn malformed persisted provenance into a silent
|
|
168
236
|
// success merely because the bad record happens to be superseded.
|
|
169
237
|
const validated = collapseProjectInstructionSnapshots(messages)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const message = validated[index]
|
|
173
|
-
if (message && isProjectInstructionMessage(message)) {
|
|
174
|
-
current = message
|
|
175
|
-
break
|
|
176
|
-
}
|
|
177
|
-
}
|
|
238
|
+
const all = collapseProjectInstructionSnapshots([...held, ...messages])
|
|
239
|
+
const current = latestProjectInstructionSnapshot(all)
|
|
178
240
|
if (
|
|
179
241
|
snapshot !== null &&
|
|
180
242
|
current !== undefined &&
|
|
181
|
-
|
|
182
|
-
{
|
|
183
|
-
content: current.content,
|
|
184
|
-
attachments: current.attachments,
|
|
185
|
-
source: current.source,
|
|
186
|
-
cacheHint: current.cacheHint,
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
content: snapshot.content,
|
|
190
|
-
attachments: snapshot.attachments,
|
|
191
|
-
source: snapshot.source,
|
|
192
|
-
cacheHint: snapshot.cacheHint,
|
|
193
|
-
},
|
|
194
|
-
)
|
|
243
|
+
sameProjectInstructionSnapshot(current, snapshot)
|
|
195
244
|
) {
|
|
196
245
|
return validated
|
|
197
246
|
}
|
|
198
247
|
if (snapshot === null && current === undefined) return validated
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
)
|
|
248
|
+
const isPolicy = (message: Message): boolean =>
|
|
249
|
+
message.role === 'user' && message.source?.type === 'project-instructions'
|
|
250
|
+
const without = validated.filter((message) => !isPolicy(message))
|
|
202
251
|
if (!snapshot) return without
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
252
|
+
const place = (list: readonly Message[], message: UserMessage): Message[] => {
|
|
253
|
+
if (placement === 'append') return [...list, message]
|
|
254
|
+
let insertion = list.length
|
|
255
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
256
|
+
if (list[index]?.role === 'user') {
|
|
257
|
+
insertion = index
|
|
258
|
+
break
|
|
259
|
+
}
|
|
210
260
|
}
|
|
261
|
+
return [...list.slice(0, insertion), message, ...list.slice(insertion)]
|
|
262
|
+
}
|
|
263
|
+
const base = all.find(
|
|
264
|
+
(message): message is UserMessage => isProjectInstructionMessage(message) && !isUpdate(message),
|
|
265
|
+
)
|
|
266
|
+
const body = projectInstructionBody(snapshot)
|
|
267
|
+
if (base === undefined) {
|
|
268
|
+
const first: UserMessage =
|
|
269
|
+
snapshot.retain === true && !isUpdate(snapshot)
|
|
270
|
+
? snapshot
|
|
271
|
+
: {
|
|
272
|
+
...snapshot,
|
|
273
|
+
content: body,
|
|
274
|
+
source: {
|
|
275
|
+
type: 'project-instructions',
|
|
276
|
+
files: snapshot.source.files,
|
|
277
|
+
},
|
|
278
|
+
retain: true,
|
|
279
|
+
}
|
|
280
|
+
return place(without, first)
|
|
281
|
+
}
|
|
282
|
+
const update: UserMessage = {
|
|
283
|
+
...snapshot,
|
|
284
|
+
content: PROJECT_INSTRUCTION_UPDATE_HEADER + body,
|
|
285
|
+
source: {
|
|
286
|
+
type: 'project-instructions',
|
|
287
|
+
files: snapshot.source.files,
|
|
288
|
+
update: true,
|
|
289
|
+
},
|
|
290
|
+
retain: true,
|
|
211
291
|
}
|
|
212
|
-
|
|
292
|
+
const kept = validated.filter((message) => !isPolicy(message) || message === base)
|
|
293
|
+
return place(kept, update)
|
|
213
294
|
}
|