@ykurilov/agent-chat-flows 0.76.0 → 0.78.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/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  sideTaskFieldKnown,
8
8
  sideTaskRecord,
9
9
  sideTaskStanding
10
- } from "./chunk-PLY57QT5.js";
10
+ } from "./chunk-CW6VSYZC.js";
11
11
  import {
12
12
  FACT_PREFIX,
13
13
  FIRST_MESSAGE_RETRY_MS,
@@ -44,6 +44,7 @@ import {
44
44
  isFactEntry,
45
45
  judgeExits,
46
46
  pageTimeZone,
47
+ pendingEvent,
47
48
  personTimeZone,
48
49
  reportTransition,
49
50
  runOutsideTurn,
@@ -56,7 +57,7 @@ import {
56
57
  transitionReport,
57
58
  withPending,
58
59
  withReopen
59
- } from "./chunk-DQQ2GCQ2.js";
60
+ } from "./chunk-CZ3EDVYW.js";
60
61
  import {
61
62
  AGENT_FLOW_FORMAT,
62
63
  AGENT_FLOW_FORMAT_VERSION,
@@ -140,7 +141,7 @@ import {
140
141
  withDayAnchor,
141
142
  withNudgeStep,
142
143
  withoutUndeliveredTouch
143
- } from "./chunk-2LAVG3YR.js";
144
+ } from "./chunk-RMWVLMGY.js";
144
145
  import {
145
146
  appliedInTime,
146
147
  byVersionAndState,
@@ -167,10 +168,175 @@ var FLOW_SERVICE_BLOCKS = [
167
168
  }
168
169
  ];
169
170
 
171
+ // src/runtime/confirmations.ts
172
+ import { createHash } from "crypto";
173
+ var confirmationHash = (spec) => createHash("sha256").update(spec.judge).digest("hex");
174
+ function confirmationHolds(record, spec, scope) {
175
+ return record?.status === "confirmed" && record.key === spec.key && record.revision === spec.revision && record.criterionHash === confirmationHash(spec) && record.scope?.ownerId === scope.ownerId && record.scope.chatId === scope.chatId && record.scope.flowId === scope.flowId && scope.flowId === spec.flowId && record.source?.kind === "judge" && typeof record.source.versionId === "string" && record.source.versionId.length > 0 && typeof record.source.answerId === "string" && record.source.answerId.length > 0 && typeof record.source.humanMessageId === "string" && record.source.humanMessageId.length > 0 && Number.isFinite(record.source.probability) && record.source.probability >= 0 && record.source.probability <= 1;
176
+ }
177
+ function createConfirmations(specs = [], guards = []) {
178
+ const registered = structuredClone(specs);
179
+ const registeredGuards = structuredClone(guards);
180
+ const short = (value) => typeof value === "string" && value.trim().length > 0 && value.length <= 100;
181
+ const seen = /* @__PURE__ */ new Set();
182
+ for (const spec of registered) {
183
+ const id = `${spec.flowId}\0${spec.key}`;
184
+ if (![spec.flowId, spec.key, spec.revision].every(short) || !spec.judge?.trim() || spec.judge.length > 800 || !Array.isArray(spec.states) || !spec.states.length || !spec.states.every(short) || seen.has(id)) throw new TypeError("confirmations: \u043D\u0443\u0436\u043D\u044B \u0443\u043D\u0438\u043A\u0430\u043B\u044C\u043D\u044B\u0435 flowId/key, revision, states \u0438 judge \u0434\u043E 800 \u0437\u043D\u0430\u043A\u043E\u0432");
185
+ seen.add(id);
186
+ }
187
+ const seenGuards = /* @__PURE__ */ new Set();
188
+ for (const guard of registeredGuards) {
189
+ const id = `${guard.flowId}\0${guard.state}\0${guard.exitId}`;
190
+ if (![guard.flowId, guard.state, guard.exitId].every(short) || guard.judgeWindow !== "current-human-turn" || seenGuards.has(id) || !registered.some((spec) => spec.flowId === guard.flowId && spec.key === guard.requires.key && spec.revision === guard.requires.revision)) throw new TypeError("exitGuards: \u043E\u0434\u0438\u043D guard \u043D\u0430 \u0432\u044B\u0445\u043E\u0434, \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 requires \u0438 current-human-turn");
191
+ seenGuards.add(id);
192
+ }
193
+ const specFor = (flowId, ref) => registered.find((spec) => spec.flowId === flowId && spec.key === ref.key && spec.revision === ref.revision);
194
+ const guardFor = (flowId, state, exitId) => registeredGuards.find((guard) => guard.flowId === flowId && guard.state === state && guard.exitId === exitId);
195
+ return {
196
+ validate(flowId, flow) {
197
+ for (const guard of registeredGuards.filter((item) => item.flowId === flowId)) {
198
+ const state = flow.states.find((item) => item.id === guard.state);
199
+ const exit = state && activeExits(flow, state).find((item) => item.exit.id === guard.exitId)?.exit;
200
+ if (exit && exit.when.type !== "judge") throw new TypeError(`exitGuards: ${flowId}/${guard.state}/${guard.exitId} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C judge-\u0432\u044B\u0445\u043E\u0434\u043E\u043C`);
201
+ }
202
+ },
203
+ forState: (flowId, state) => registered.filter((spec) => spec.flowId === flowId && spec.states.includes(state)),
204
+ specFor,
205
+ guardFor,
206
+ revision: (flowId) => createHash("sha256").update(JSON.stringify([registered.filter((spec) => spec.flowId === flowId), registeredGuards.filter((guard) => guard.flowId === flowId)])).digest("hex"),
207
+ allows(scope, run, exitId) {
208
+ const guard = guardFor(scope.flowId, run.state, exitId);
209
+ if (!guard) return true;
210
+ const spec = specFor(scope.flowId, guard.requires);
211
+ const decision = run.checkedTransition;
212
+ return confirmationHolds(run.confirmations?.[spec.key], spec, scope) && decision?.transition.exitId === exitId && decision.answerId === run.answerId && decision.requires.key === spec.key && decision.requires.revision === spec.revision && decision.criterionHash === confirmationHash(spec);
213
+ }
214
+ };
215
+ }
216
+
217
+ // src/runtime/confirmation-judge.ts
218
+ import { flowStateCommitter } from "@ykurilov/agent-chat-core";
219
+ var textOf = (message) => message.parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
220
+ var messagesOf = (messages) => messages.flatMap((message) => {
221
+ const text = textOf(message).trim();
222
+ return text && (message.role === "user" || message.role === "assistant") ? [{ role: message.role === "user" ? "human" : "agent", text }] : [];
223
+ });
224
+ async function judgeConfirmations(input) {
225
+ const { confirmations, pinned, run, result, port } = input;
226
+ const configurationHash = confirmations.revision(pinned.flowId);
227
+ if (run.confirmationCheck?.answerId === result.messageId && run.confirmationCheck.configurationHash === configurationHash) return { saved: true };
228
+ const scope = { ownerId: result.ownerId, chatId: result.chatId, flowId: pinned.flowId };
229
+ const commit2 = flowStateCommitter(port);
230
+ if (!commit2) throw new Error("confirmations: \u0441\u043B\u0443\u0436\u0431\u0430 \u0447\u0430\u0442\u0430 \u043D\u0435 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044E\u044E flows capability");
231
+ const all = (await port.peekHistory(scope.ownerId, scope.chatId)).messages;
232
+ const answerIndex = all.findIndex((message) => message.id === result.messageId);
233
+ if (answerIndex < 0) return { saved: false };
234
+ const branch = all.slice(0, answerIndex + 1);
235
+ const humanIndex = branch.findLastIndex((message) => message.role === "user" && Boolean(textOf(message).trim()));
236
+ if (humanIndex < 0) return { saved: false };
237
+ const humanMessageId = branch[humanIndex].id;
238
+ const records = { ...run.confirmations };
239
+ const onBranch = (record) => record?.status === "confirmed" && branch.some((message) => message.id === record.source.answerId) && branch.some((message) => message.id === record.source.humanMessageId);
240
+ const query = async (conditions, messages) => input.judge.checkAll({
241
+ ...scope,
242
+ conditions,
243
+ messages,
244
+ ...input.goal && { goal: input.goal },
245
+ ...run.timeOffset !== void 0 && { testChat: true }
246
+ });
247
+ for (const spec of confirmations.forState(scope.flowId, run.state)) {
248
+ const old = records[spec.key];
249
+ if (confirmationHolds(old, spec, scope) && onBranch(old)) continue;
250
+ const recordedVersion = old?.status === "confirmed" ? old.source?.versionId : old?.versionId;
251
+ if (recordedVersion === pinned.versionId && (old.revision !== spec.revision || old.criterionHash !== confirmationHash(spec))) continue;
252
+ let evidence = input.messages;
253
+ if (old?.status === "revoked" && old.scope.ownerId === scope.ownerId && old.scope.chatId === scope.chatId && old.scope.flowId === scope.flowId) {
254
+ const boundary = old.afterMessageId === null ? -1 : branch.findIndex((message) => message.id === old.afterMessageId);
255
+ if (old.afterMessageId !== null && boundary < 0 || humanIndex <= boundary) continue;
256
+ evidence = messagesOf(branch.slice(boundary + 1)).slice(-6);
257
+ }
258
+ const id = `confirmation:${spec.key}`;
259
+ const verdict = (await query([{ exitId: id, text: spec.judge }], evidence)).find((item) => item.exitId === id && item.confirmed && item.probability >= input.threshold);
260
+ if (!verdict) continue;
261
+ records[spec.key] = {
262
+ key: spec.key,
263
+ revision: spec.revision,
264
+ status: "confirmed",
265
+ scope,
266
+ criterionHash: confirmationHash(spec),
267
+ checkedAt: input.now,
268
+ source: { kind: "judge", versionId: pinned.versionId, answerId: result.messageId, humanMessageId, probability: verdict.probability }
269
+ };
270
+ }
271
+ const next = { ...run, confirmationCheck: { answerId: result.messageId, configurationHash }, ...Object.keys(records).length && { confirmations: records } };
272
+ const ordinary = input.conditions.filter((condition) => !confirmations.guardFor(scope.flowId, run.state, condition.exitId));
273
+ const guarded2 = input.conditions.filter((condition) => {
274
+ const guard = confirmations.guardFor(scope.flowId, run.state, condition.exitId);
275
+ if (!guard) return false;
276
+ const spec = confirmations.specFor(scope.flowId, guard.requires);
277
+ const receipt = records[spec.key];
278
+ return confirmationHolds(receipt, spec, scope) && onBranch(receipt);
279
+ });
280
+ const verdicts = [
281
+ ...ordinary.length ? await query(ordinary, input.messages) : [],
282
+ ...guarded2.length ? await query(guarded2, messagesOf([branch[humanIndex], branch[answerIndex]])) : []
283
+ ];
284
+ const met = input.conditions.map(({ exitId }) => verdicts.find((verdict) => verdict.exitId === exitId && verdict.confirmed && verdict.probability >= input.threshold)).find(Boolean);
285
+ const transition = met ? nextTransition(pinned.flow, next, { type: "verdict", exitId: met.exitId, probability: met.probability, decidedBy: "judge" }, input.now) : null;
286
+ if (transition) {
287
+ const guard = confirmations.guardFor(scope.flowId, run.state, transition.exitId);
288
+ if (guard) next.checkedTransition = { versionId: pinned.versionId, transition, requires: guard.requires, criterionHash: confirmationHash(confirmations.specFor(scope.flowId, guard.requires)), answerId: result.messageId, humanMessageId };
289
+ }
290
+ const changed = JSON.stringify(next) !== JSON.stringify(run);
291
+ if (!changed) return { ...transition && { transition }, saved: true };
292
+ for (const wait2 of [0, 10, 50, 100, 250, 500, 1e3]) {
293
+ if (wait2) await new Promise((resolve) => setTimeout(resolve, wait2));
294
+ const saved = await commit2({ ...scope, revision: confirmations.revision(scope.flowId), expectedAtMessageId: input.atMessageId, idempotencyKey: `judge:${result.messageId}`, flow: next });
295
+ if (saved.status === "applied" || saved.status === "same") return { ...transition && { transition }, saved: true };
296
+ if (saved.status !== "busy") return { saved: false };
297
+ }
298
+ return { saved: false };
299
+ }
300
+
301
+ // src/runtime/confirmation-migration.ts
302
+ async function previewConfirmedMigration(input, deps) {
303
+ const { store, confirmations } = deps;
304
+ const spec = confirmations.specFor(input.flowId, input.requiresConfirmation);
305
+ if (!spec) throw new TypeError("previewMigration: requiresConfirmation \u043D\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C");
306
+ const eligible = [];
307
+ const keepOriginal = [];
308
+ const rows = await store.listFlowChats({ flowId: input.flowId, exceptVersionId: input.toVersionId, ...input.chatIds && { chatIds: input.chatIds } });
309
+ for (const row of rows) {
310
+ if (row.endedAt && !input.includeEnded) continue;
311
+ const port = await deps.portFor(row);
312
+ const data = await port.getChatData(row.ownerId, row.chatId);
313
+ const test = testChatOffset(data) !== null;
314
+ if (test && !input.includeTestChats) continue;
315
+ if (input.filter && !await input.filter({ chatId: row.chatId, ownerId: row.ownerId, flowId: row.flowId, versionId: row.versionId, stateId: row.stateId, ended: row.endedAt !== null, endReason: row.endReason, test, data })) continue;
316
+ const snapshot = await port.getChatState(row.ownerId, row.chatId);
317
+ const run = storedRun(snapshot.values.flow);
318
+ const receipt = run?.confirmations?.[spec.key];
319
+ const scope = { ownerId: row.ownerId, chatId: row.chatId, flowId: row.flowId };
320
+ let reason;
321
+ if (!receipt) reason = "missing_receipt";
322
+ else if (receipt.status === "revoked") reason = "revoked_receipt";
323
+ else if (!confirmationHolds(receipt, spec, scope)) reason = "incompatible_receipt";
324
+ else if (!await store.getVersion(row.flowId, receipt.source.versionId)) reason = "source_version_missing";
325
+ else {
326
+ const branch = (await port.peekHistory(row.ownerId, row.chatId)).messages;
327
+ if (!branch.some((message) => message.id === receipt.source.answerId) || !branch.some((message) => message.id === receipt.source.humanMessageId)) reason = "source_not_on_branch";
328
+ }
329
+ if (reason) keepOriginal.push({ chatId: row.chatId, status: "ambiguous", reason });
330
+ else eligible.push(row.chatId);
331
+ }
332
+ return { eligible, keepOriginal };
333
+ }
334
+
170
335
  // src/runtime/agent-flow.ts
171
- import { createHash, randomUUID as randomUUID4 } from "crypto";
336
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
172
337
  import {
173
- ChatNotFoundError as ChatNotFoundError4
338
+ ChatNotFoundError as ChatNotFoundError4,
339
+ flowStateCommitter as flowStateCommitter2
174
340
  } from "@ykurilov/agent-chat-core";
175
341
 
176
342
  // src/runtime/turn-tool-names.ts
@@ -1606,6 +1772,7 @@ function flowExitTool(input, model) {
1606
1772
  const { flow } = turn.pinned;
1607
1773
  const chosen = judgeExits(flow, turn.run.state)[exit - 1];
1608
1774
  if (!chosen) return `\u041D\u0435\u0442 \u0442\u0430\u043A\u043E\u0433\u043E \u0443\u0441\u043B\u043E\u0432\u0438\u044F: \u043D\u043E\u043C\u0435\u0440 ${exit}. \u041D\u043E\u043C\u0435\u0440\u0430 \u0443\u0441\u043B\u043E\u0432\u0438\u0439 \u2014 \u0432 \u043A\u0430\u0440\u0442\u043E\u0447\u043A\u0435 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F.`;
1775
+ if (runtime.confirmations?.guardFor(turn.pinned.flowId, turn.run.state, chosen.exitId)) return "\u042D\u0442\u043E\u0442 \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0435\u0442\u0441\u044F \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C \u043F\u043E\u0441\u043B\u0435 \u043F\u043E\u043B\u043D\u043E\u0433\u043E \u043E\u0442\u0432\u0435\u0442\u0430: \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0451\u043D\u043D\u0430\u044F \u0432\u0435\u0445\u0430 \u0438 \u0430\u043A\u0442\u0443\u0430\u043B\u044C\u043D\u043E\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0434\u043E\u043B\u0436\u043D\u044B \u0431\u044B\u0442\u044C \u043F\u0440\u043E\u0432\u0435\u0440\u0435\u043D\u044B \u0432\u043C\u0435\u0441\u0442\u0435. \u041F\u0435\u0440\u0435\u0445\u043E\u0434 \u043F\u043E\u043A\u0430 \u043D\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D.";
1609
1776
  if (!claimAllowed(turn.run, chosen.exitId)) return "\u042D\u0442\u043E \u0443\u0441\u043B\u043E\u0432\u0438\u0435 \u0443\u0436\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u043B\u0438: \u043F\u043E\u0434\u043E\u0436\u0434\u0438 \u043E\u0442\u0432\u0435\u0442\u0430 \u0447\u0435\u043B\u043E\u0432\u0435\u043A\u0430 \u0438 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u0439 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440.";
1610
1777
  const state = stateById(flow, turn.run.state);
1611
1778
  let verdict;
@@ -1887,6 +2054,7 @@ function buildAgentFlow(options, modules = {}) {
1887
2054
  const now = () => clock().getTime();
1888
2055
  const iso = () => clock().toISOString();
1889
2056
  const logError = options.logError ?? ((message) => console.error(message));
2057
+ const confirmations = createConfirmations(options.confirmations, options.exitGuards);
1890
2058
  const defaultTimeZone = options.defaultTimeZone === void 0 ? void 0 : ianaTimeZone(options.defaultTimeZone);
1891
2059
  if (defaultTimeZone === null) throw new TypeError(`createAgentFlow: defaultTimeZone \xAB${options.defaultTimeZone}\xBB \u2014 \u043D\u0435 \u043F\u043E\u044F\u0441 IANA`);
1892
2060
  let port;
@@ -2014,7 +2182,10 @@ function buildAgentFlow(options, modules = {}) {
2014
2182
  const key = `${flowId}\0${versionId}`;
2015
2183
  let found = versions.get(key);
2016
2184
  if (!found) {
2017
- found = store.getVersion(flowId, versionId).then((saved) => saved?.flow ?? null);
2185
+ found = store.getVersion(flowId, versionId).then((saved) => {
2186
+ if (saved) confirmations.validate(flowId, saved.flow);
2187
+ return saved?.flow ?? null;
2188
+ });
2018
2189
  versions.set(key, found);
2019
2190
  void found.then((flow) => flow ?? versions.delete(key), () => versions.delete(key));
2020
2191
  }
@@ -2030,12 +2201,15 @@ function buildAgentFlow(options, modules = {}) {
2030
2201
  }
2031
2202
  function checkedFlow(flowId, versionId, value) {
2032
2203
  const checked = flowOrProblem(flowId, versionId, value);
2033
- if ("flow" in checked) return checked.flow;
2204
+ if ("flow" in checked) {
2205
+ confirmations.validate(flowId, checked.flow);
2206
+ return checked.flow;
2207
+ }
2034
2208
  logError(checked.problem);
2035
2209
  return null;
2036
2210
  }
2037
2211
  async function saveChecked(loaded, flow) {
2038
- const saved = await store.saveVersion({ flowId: loaded.flowId, versionId: loaded.versionId, hash: createHash("sha256").update(stableJson(flow)).digest("hex"), flow, at: iso() });
2212
+ const saved = await store.saveVersion({ flowId: loaded.flowId, versionId: loaded.versionId, hash: createHash2("sha256").update(stableJson(flow)).digest("hex"), flow, at: iso() });
2039
2213
  if (saved === "conflict") logError(`[\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439] ${loaded.flowId}@${loaded.versionId}: \u043F\u043E\u0434 \u044D\u0442\u0438\u043C \u043D\u043E\u043C\u0435\u0440\u043E\u043C \u0443\u0436\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u0434\u0440\u0443\u0433\u043E\u0439 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439, \u0447\u0430\u0442 \u0438\u0434\u0451\u0442 \u043F\u043E \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D\u043D\u043E\u043C\u0443`);
2040
2214
  return saved;
2041
2215
  }
@@ -2156,6 +2330,7 @@ function buildAgentFlow(options, modules = {}) {
2156
2330
  return remember(chatId, await pinned);
2157
2331
  }
2158
2332
  const runtime = {
2333
+ confirmations,
2159
2334
  store,
2160
2335
  now,
2161
2336
  logError,
@@ -2239,7 +2414,7 @@ function buildAgentFlow(options, modules = {}) {
2239
2414
  logError(`[\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439] \u0447\u0430\u0442=${record.chatId} \u0442\u0430\u0439\u043C\u0435\u0440 \u043D\u0435 \u043F\u0435\u0440\u0435\u0441\u0447\u0438\u0442\u0430\u043D: ${errorText5(error)}`);
2240
2415
  }
2241
2416
  }
2242
- async function deliverFirstText(ownerId, chatId, messageId, condition, text) {
2417
+ async function deliverFirstText(ownerId, chatId, messageId, condition, text, guarded2 = false) {
2243
2418
  for (let attempt = 0; ; attempt += 1) {
2244
2419
  const result = await (await portOf({ ownerId, chatId })).deliverNotices({
2245
2420
  ownerId,
@@ -2247,7 +2422,8 @@ function buildAgentFlow(options, modules = {}) {
2247
2422
  notices: [{ kind: "app", name: "flow_judge", title: `\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440\u0430: ${condition}` }],
2248
2423
  wakeAgent: true,
2249
2424
  deliveryKey: `flow:${chatId}:judge:${messageId}`,
2250
- reply: { text }
2425
+ // Guarded decision может быть отозван после judge: готовый reply обошёл бы результат prepareTurn.
2426
+ ...!guarded2 && { reply: { text } }
2251
2427
  });
2252
2428
  if (result.status !== "busy") return;
2253
2429
  if (attempt >= DELIVERY_RETRY_MS.length) return;
@@ -2258,7 +2434,8 @@ function buildAgentFlow(options, modules = {}) {
2258
2434
  const { ownerId, chatId, messageId } = result;
2259
2435
  const pinned = await chatFlow(ownerId, chatId);
2260
2436
  if (!pinned) return;
2261
- const { values } = await (await portOf({ ownerId, chatId })).getChatState(ownerId, chatId);
2437
+ const service = await portOf({ ownerId, chatId });
2438
+ const { values, atMessageId } = await service.getChatState(ownerId, chatId);
2262
2439
  const run = storedRun(values[FLOW_STATE_KEY]);
2263
2440
  if (!run || run.answerId !== messageId) return;
2264
2441
  await clearPlacement(chatId);
@@ -2269,7 +2446,30 @@ function buildAgentFlow(options, modules = {}) {
2269
2446
  const state = stateById(pinned.flow, run.state);
2270
2447
  if (!state || isFinal(state)) return;
2271
2448
  const conditions = judgeExits(pinned.flow, run.state);
2272
- if (conditions.length === 0) return;
2449
+ const withConfirmations = confirmations.forState(pinned.flowId, run.state).length > 0 || conditions.some(({ exitId }) => confirmations.guardFor(pinned.flowId, run.state, exitId));
2450
+ if (conditions.length === 0 && !withConfirmations) return;
2451
+ if (withConfirmations) {
2452
+ const checked = await judgeConfirmations({
2453
+ confirmations,
2454
+ pinned,
2455
+ run,
2456
+ result,
2457
+ port: service,
2458
+ atMessageId,
2459
+ judge: judge(),
2460
+ threshold: options.judge.threshold ?? 0.7,
2461
+ now: chatTime(run, now()),
2462
+ conditions,
2463
+ goal: stateGoal(state.instructions),
2464
+ messages: await history(ownerId, chatId, { run, answer: { id: messageId, parts: result.parts } })
2465
+ });
2466
+ if (!checked.saved || !checked.transition) return;
2467
+ const transition2 = checked.transition;
2468
+ if (!confirmations.guardFor(pinned.flowId, run.state, transition2.exitId)) await store.addPending(chatId, { kind: "transition", key: `judge:${messageId}`, transition: transition2, branchHead: messageId });
2469
+ const first2 = exactFirstText(pinned.flow, transition2.to);
2470
+ if (first2) await deliverFirstText(ownerId, chatId, messageId, conditions.find((item) => item.exitId === transition2.exitId).text, first2, Boolean(confirmations.guardFor(pinned.flowId, run.state, transition2.exitId)));
2471
+ return;
2472
+ }
2273
2473
  const verdicts = await judge().checkAll({
2274
2474
  ownerId,
2275
2475
  chatId,
@@ -2287,6 +2487,34 @@ function buildAgentFlow(options, modules = {}) {
2287
2487
  const first = exactFirstText(pinned.flow, transition.to);
2288
2488
  if (first) await deliverFirstText(ownerId, chatId, messageId, conditions.find((item) => item.exitId === met.exitId).text, first);
2289
2489
  }
2490
+ async function revokeConfirmation(input) {
2491
+ if (!input.reason?.trim() || input.reason.length > 1e3) throw new TypeError("revokeConfirmation: reason \u2014 \u043D\u0435\u043F\u0443\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0434\u043E 1000 \u0437\u043D\u0430\u043A\u043E\u0432");
2492
+ if (typeof input.idempotencyKey !== "string" || !input.idempotencyKey.trim() || input.idempotencyKey.length > 180) throw new TypeError("revokeConfirmation: idempotencyKey \u2014 \u043D\u0435\u043F\u0443\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0434\u043E 180 \u0437\u043D\u0430\u043A\u043E\u0432");
2493
+ const pinned = await chatFlow(input.ownerId, input.chatId);
2494
+ if (!pinned || pinned.flowId !== input.flowId) return { status: "not_found" };
2495
+ const spec = confirmations.specFor(input.flowId, input);
2496
+ if (!spec) return { status: "stale" };
2497
+ const service = await portOf(input);
2498
+ const commit2 = flowStateCommitter2(service);
2499
+ if (!commit2) throw new Error("confirmations: \u0441\u043B\u0443\u0436\u0431\u0430 \u0447\u0430\u0442\u0430 \u043D\u0435 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u044E\u044E flows capability");
2500
+ const snapshot = await service.getChatState(input.ownerId, input.chatId);
2501
+ const run = storedRun(snapshot.values[FLOW_STATE_KEY]);
2502
+ if (!run) return { status: "not_found" };
2503
+ const old = run.confirmations?.[spec.key];
2504
+ if (old && (old.scope?.ownerId !== input.ownerId || old.scope.chatId !== input.chatId || old.scope.flowId !== input.flowId)) return { status: "stale" };
2505
+ if (old?.status === "revoked" && old.operationKey === input.idempotencyKey) {
2506
+ if (old.reason !== input.reason || old.revision !== input.revision) throw new TypeError("revokeConfirmation: idempotencyKey \u0443\u0436\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D \u0441 \u0434\u0440\u0443\u0433\u0438\u043C payload");
2507
+ return { status: "same", atMessageId: snapshot.atMessageId };
2508
+ }
2509
+ if (snapshot.atMessageId !== input.expectedAtMessageId || old && (old.revision !== spec.revision || old.criterionHash !== confirmationHash(spec))) return { status: "stale" };
2510
+ const scope = { ownerId: input.ownerId, chatId: input.chatId, flowId: input.flowId };
2511
+ const { checkedTransition, ...withoutDecision } = run;
2512
+ const next = { ...withoutDecision, ...checkedTransition && checkedTransition.requires.key !== spec.key && { checkedTransition }, confirmations: {
2513
+ ...run.confirmations,
2514
+ [spec.key]: { key: spec.key, revision: spec.revision, status: "revoked", versionId: pinned.versionId, scope, criterionHash: confirmationHash(spec), revokedAt: chatTime(run, now()), afterMessageId: run.answerId ?? null, reason: input.reason, operationKey: input.idempotencyKey }
2515
+ } };
2516
+ return commit2({ ...scope, revision: confirmations.revision(input.flowId), expectedAtMessageId: input.expectedAtMessageId, idempotencyKey: `revoke:${input.idempotencyKey}`, flow: next });
2517
+ }
2290
2518
  async function buttonPressed(context) {
2291
2519
  const pinned = await chatFlow(context.ownerId, context.chatId);
2292
2520
  if (!pinned) return;
@@ -2418,7 +2646,13 @@ function buildAgentFlow(options, modules = {}) {
2418
2646
  const transitions = [];
2419
2647
  const input = { now: at, card: await runtime.cardValues(ownerId), out: transitions, onChainLimit: () => {
2420
2648
  }, timeZone };
2421
- run = withPending(pinned.flow, run, record?.pending ?? [], input);
2649
+ const decision = run.checkedTransition;
2650
+ if (decision && decision.versionId === pinned.versionId && decision.answerId === run.answerId && decision.transition.from === run.state && confirmations.allows({ ownerId, chatId, flowId: pinned.flowId }, run, decision.transition.exitId)) {
2651
+ const { checkedTransition, ...withoutDecision } = run;
2652
+ run = advance(pinned.flow, withoutDecision, pendingEvent(decision.transition, timeZone), input);
2653
+ }
2654
+ const pending = (record?.pending ?? []).filter((change) => change.kind !== "transition" || !confirmations.guardFor(pinned.flowId, change.transition.from, change.transition.exitId));
2655
+ run = withPending(pinned.flow, run, pending, input);
2422
2656
  const schedule = pinned.flow.schedule;
2423
2657
  if (record?.endedAt) run = endedRun(pinned.flow, run, { endedAt: Date.parse(record.endedAt), endReason: record.endReason ?? "final" });
2424
2658
  const { answerId: _answerId, firstReasons: _firstReasons, ...plain } = run;
@@ -2510,7 +2744,7 @@ ${rules}`;
2510
2744
  } catch (error) {
2511
2745
  logError(`[\u0441\u0446\u0435\u043D\u0430\u0440\u0438\u0439] \u0447\u0430\u0442=${input.chatId} \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u043D\u0435 \u0437\u0430\u043F\u0438\u0441\u0430\u043D\u043E \u0432 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435: ${errorText5(error)}`);
2512
2746
  }
2513
- await reschedule(input.ownerId, input.chatId, run, input.atMessageId);
2747
+ await reschedule(input.ownerId, input.chatId, run, input.reason === "flowCommit" ? run.answerId ?? null : input.atMessageId);
2514
2748
  })();
2515
2749
  track(moved);
2516
2750
  await moved;
@@ -2549,10 +2783,14 @@ ${rules}`;
2549
2783
  noteStaffReply: (input) => pauses().noteStaffReply(input),
2550
2784
  reportDelivery: (input) => delivery().report(input),
2551
2785
  readRun,
2786
+ revokeConfirmation,
2787
+ previewMigration: (input) => previewConfirmedMigration(input, { store, confirmations, portFor: portOf }),
2552
2788
  chatsByState: (flowId, options2) => migrator().chatsByState(flowId, options2),
2553
2789
  async migrateChats(input) {
2554
2790
  try {
2555
- return await migrator().migrateChats(input);
2791
+ if (!input.requiresConfirmation) return await migrator().migrateChats(input);
2792
+ const preview = await previewConfirmedMigration({ ...input, requiresConfirmation: input.requiresConfirmation }, { store, confirmations, portFor: portOf });
2793
+ return await migrator().migrateChats({ ...input, chatIds: preview.eligible, filter: void 0 });
2556
2794
  } finally {
2557
2795
  forgetPorts();
2558
2796
  }