@capybara-hq/kernel 0.1.0-rc.25 → 0.1.0-rc.27

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.
Files changed (91) hide show
  1. package/dist/admin/outbox-dead.service.d.ts +7 -3
  2. package/dist/admin/outbox-dead.service.js +17 -6
  3. package/dist/admin/overview.service.d.ts +6 -6
  4. package/dist/admin/overview.service.js +7 -7
  5. package/dist/background/wake-rules.d.ts +5 -0
  6. package/dist/background/wake-rules.js +16 -5
  7. package/dist/catalog/catalog-content.d.ts +6 -1
  8. package/dist/catalog/catalog-content.js +2 -1
  9. package/dist/catalog/marketplaces.service.d.ts +1 -1
  10. package/dist/catalog/org-catalog.service.js +5 -2
  11. package/dist/catalog/plugin-install.d.ts +5 -0
  12. package/dist/catalog/plugin-install.js +37 -1
  13. package/dist/channels/channels.module.d.ts +9 -8
  14. package/dist/channels/channels.module.js +11 -10
  15. package/dist/channels/connections.service.d.ts +3 -3
  16. package/dist/channels/connections.service.js +6 -6
  17. package/dist/channels/deliver-job.d.ts +10 -3
  18. package/dist/channels/deliver-job.js +7 -3
  19. package/dist/channels/jobs.d.ts +11 -4
  20. package/dist/channels/jobs.js +18 -3
  21. package/dist/channels/outbound.d.ts +5 -2
  22. package/dist/channels/outbound.js +9 -2
  23. package/dist/conversations/conversations.controller.d.ts +4 -2
  24. package/dist/conversations/conversations.controller.js +4 -2
  25. package/dist/conversations/conversations.service.d.ts +37 -4
  26. package/dist/conversations/conversations.service.js +77 -20
  27. package/dist/conversations/events.d.ts +6 -0
  28. package/dist/conversations/events.js +6 -0
  29. package/dist/db/kernel-schema.d.ts +10 -1
  30. package/dist/devices/devices.service.d.ts +14 -6
  31. package/dist/devices/devices.service.js +20 -7
  32. package/dist/evals/evals.module.js +5 -1
  33. package/dist/evals/jobs.d.ts +14 -6
  34. package/dist/evals/jobs.js +21 -19
  35. package/dist/evals/runs.service.d.ts +5 -6
  36. package/dist/evals/runs.service.js +11 -33
  37. package/dist/infra/outbox/outbox.service.d.ts +6 -0
  38. package/dist/infra/outbox/outbox.service.js +51 -17
  39. package/dist/infra/outbox/registry.d.ts +52 -0
  40. package/dist/infra/outbox/registry.js +57 -0
  41. package/dist/local-devices/device-policy.service.d.ts +131 -26
  42. package/dist/local-devices/device-policy.service.js +341 -145
  43. package/dist/local-devices/machines.d.ts +17 -2
  44. package/dist/local-devices/machines.js +66 -5
  45. package/dist/local-devices/policy-op-jobs.d.ts +15 -0
  46. package/dist/local-devices/policy-op-jobs.js +12 -0
  47. package/dist/local-devices/policy-summary.d.ts +36 -3
  48. package/dist/local-devices/policy-summary.js +47 -1
  49. package/dist/orgs/orgs.service.js +4 -1
  50. package/dist/overrides.d.ts +4 -0
  51. package/dist/overrides.js +2 -0
  52. package/dist/platform/ops/conversations.ops.js +1 -1
  53. package/dist/runtime/maintenance.d.ts +6 -3
  54. package/dist/runtime/maintenance.js +21 -7
  55. package/dist/runtime/plugin-context.service.d.ts +9 -3
  56. package/dist/runtime/plugin-context.service.js +17 -5
  57. package/dist/runtime/run-context.service.d.ts +7 -3
  58. package/dist/runtime/run-context.service.js +12 -2
  59. package/dist/runtime/run-finish.d.ts +2 -2
  60. package/dist/runtime/run-finish.js +11 -6
  61. package/dist/runtime/run-jobs.d.ts +21 -4
  62. package/dist/runtime/run-jobs.js +29 -6
  63. package/dist/runtime/run-orphan-check.job.d.ts +14 -11
  64. package/dist/runtime/run-orphan-check.job.js +16 -11
  65. package/dist/runtime/run-queue.d.ts +23 -31
  66. package/dist/runtime/run-queue.js +62 -64
  67. package/dist/runtime/run-status.d.ts +12 -0
  68. package/dist/runtime/run-status.js +21 -0
  69. package/dist/runtime/run-worker.d.ts +10 -2
  70. package/dist/runtime/run-worker.js +44 -19
  71. package/dist/runtime/step-log.d.ts +6 -5
  72. package/dist/runtime/step-log.js +6 -5
  73. package/dist/runtime/usage-report.d.ts +3 -1
  74. package/dist/runtime/usage-report.js +4 -2
  75. package/dist/runtime/waits.d.ts +16 -6
  76. package/dist/runtime/waits.js +37 -15
  77. package/dist/schedules/scan.job.d.ts +6 -6
  78. package/dist/schedules/scan.job.js +6 -6
  79. package/dist/skills/catalog.d.ts +7 -3
  80. package/dist/skills/catalog.js +12 -2
  81. package/dist/tools/approvals.service.d.ts +96 -19
  82. package/dist/tools/approvals.service.js +300 -67
  83. package/dist/tools/builtins/sandbox/task-output.js +21 -6
  84. package/dist/tools/connectors.service.js +0 -4
  85. package/dist/tools/device-op-resolver.d.ts +40 -0
  86. package/dist/tools/device-op-resolver.js +121 -0
  87. package/dist/tools/network-asks.d.ts +1 -1
  88. package/dist/tools/network-asks.js +4 -3
  89. package/dist/tools/tools.module.js +4 -0
  90. package/package.json +2 -2
  91. package/src/migrations/0125_conversation_tree_stopped_at.sql +5 -0
@@ -607,6 +607,13 @@ let Waits = class Waits {
607
607
  * background tasks of every covered sub-agent conversation stop, whether it was running or idle.
608
608
  * Returns the number of runs asked to stop.
609
609
  *
610
+ * `windowsUpTo` (a root run's id; only after that root run a person stopped ended,
611
+ * {@link stopDescendantsOfStopped}) narrows the stop to the runs of the budget windows opened up
612
+ * to that run (`runs.budget_run_id`, agent map spec §6): a run of a window a later root run
613
+ * opened (the person's next message, started meanwhile) is never stopped by the late call, and
614
+ * the background task sweep is left out (a stopped child's own end stops its tasks,
615
+ * {@link childFinished}).
616
+ *
610
617
  * Lock order: nothing is held across runs. Each run's stop is its own transaction (or none), and
611
618
  * one that writes the terminal state locks that run's conversation row, then its run row, the
612
619
  * order of `StepLog.appendCounted` (conversation row, then the root's budget run row) and of
@@ -619,12 +626,22 @@ let Waits = class Waits {
619
626
  if (ids.length === 0)
620
627
  return 0;
621
628
  const level = new Map(tree.map((c) => [c.id, c.level]));
622
- const runs = (await this.db
629
+ let q = this.db
623
630
  .selectFrom("runs")
624
631
  .select(["id", "conversation_id", "status"])
625
632
  .where("conversation_id", "in", ids)
626
- .where("status", "in", ACTIVE_RUN_STATUSES)
627
- .execute()).sort((a, b) => (level.get(a.conversation_id) ?? 0) - (level.get(b.conversation_id) ?? 0));
633
+ .where("status", "in", ACTIVE_RUN_STATUSES);
634
+ const upTo = opts.windowsUpTo;
635
+ if (upTo !== undefined) {
636
+ // a window is opened by a root run without a budget run of its own (`budgetWindowOf`)
637
+ q = q.where("budget_run_id", "in", (eb) => eb
638
+ .selectFrom("runs as w")
639
+ .select("w.id")
640
+ .where("w.conversation_id", "=", conversationId)
641
+ .where("w.budget_run_id", "is", null)
642
+ .where("w.created_at", "<=", (eb2) => eb2.selectFrom("runs as s").select("s.created_at").where("s.id", "=", upTo)));
643
+ }
644
+ const runs = (await q.execute()).sort((a, b) => (level.get(a.conversation_id) ?? 0) - (level.get(b.conversation_id) ?? 0));
628
645
  for (const r of runs) {
629
646
  try {
630
647
  // a stopped wait's card updates are owed by its transaction's appends (outbox spec §8.1)
@@ -639,34 +656,39 @@ let Waits = class Waits {
639
656
  // all of each conversation's tasks, as a stopped child's run end does (`childFinished`), with
640
657
  // which this is idempotent (only running tasks are stopped; `stoppedBy: "subagent_end"`
641
658
  // never wakes). A root conversation is skipped (`stopSubagentTasks`); never throws.
642
- await Promise.all(ids.map((id) => this.background.stopSubagentTasks(id)));
659
+ if (upTo === undefined) {
660
+ await Promise.all(ids.map((id) => this.background.stopSubagentTasks(id)));
661
+ }
643
662
  return runs.length;
644
663
  }
645
664
  /**
646
- * After a sub-agent run's terminal commit (its worker): a run a person stopped (`run_aborted`
647
- * with an account and no `detail`; only {@link stopTree} stops a sub-agent's run that way) stops
648
- * the active runs below its conversation as well, so a descendant it dispatched after the tree
649
- * stop read the tree still stops (dispatch needs its run `running`, so after its terminal commit
650
- * the set is final). Any other end leaves its children running (agent map spec §5.4). Returns
651
- * the number of runs asked to stop. A root conversation's run never stops its children here.
665
+ * After a run's terminal commit (its worker): a run a person stopped (`run_aborted` with an
666
+ * account and no `detail`; {@link stopTree} or a person's stop of the root) stops the active runs
667
+ * below its conversation as well, so a descendant it dispatched after the tree stop read the tree
668
+ * still stops (dispatch needs its run `running`, so after its terminal commit the set is final).
669
+ * A root's run stops only the runs of the budget windows up to it (`windowsUpTo`): the person's
670
+ * next message may already have started a new run with sub-agents of its own (agent map spec §6,
671
+ * changed in acceptance 2026-11-13). Any other end leaves its children running (§5.4). Returns
672
+ * the number of runs asked to stop.
652
673
  */
653
674
  async stopDescendantsOfStopped(runId) {
654
675
  const ev = await this.db
655
676
  .selectFrom("runs as r")
656
677
  .innerJoin("conversation_events as e", (j) => j.onRef("e.conversation_id", "=", "r.conversation_id").onRef("e.run_id", "=", "r.id"))
657
678
  .innerJoin("conversations as c", "c.id", "r.conversation_id")
658
- .select(["r.conversation_id", "e.payload"])
679
+ .select(["r.conversation_id", "c.kind", "e.payload"])
659
680
  .where("r.id", "=", runId)
660
- // a root's stop leaves its children running (agent map spec §6): a person-stopped root run
661
- // reaching here (a redelivered job, the failure path) stops nothing
662
- .where("c.kind", "=", "subagent")
681
+ .where("c.kind", "in", ["chat", "subagent"])
663
682
  .where("r.status", "=", "aborted")
664
683
  .where("e.type", "=", "run_aborted")
665
684
  .executeTakeFirst();
666
685
  const p = ev?.payload;
667
686
  if (!ev || typeof p?.byAccountId !== "string" || p.detail !== undefined)
668
687
  return 0;
669
- return this.stopTree(ev.conversation_id, p.byAccountId, { includeSelf: false });
688
+ return this.stopTree(ev.conversation_id, p.byAccountId, {
689
+ includeSelf: false,
690
+ ...(ev.kind === "chat" && { windowsUpTo: runId }),
691
+ });
670
692
  }
671
693
  /**
672
694
  * Stops one active sub-agent run: a `waiting` one through its own {@link abortWaiting}, a
@@ -15,9 +15,9 @@ import { ScheduledTaskStore } from "./store.js";
15
15
  * transaction owes `sched-fire-<taskRunId>` (transactional outbox spec §8.1), so a written tick is
16
16
  * never left unfired and the scan needs no repair of lost fire jobs (a fire delivered too late
17
17
  * marks its tick missed itself, `ScheduledTaskFirer`), and the run's unfired settle check (a fire
18
- * job that failed for good before its claim: the tick is missed, `job-ids.ts`). A task whose stored trigger the schedule
19
- * math cannot evaluate is paused (`invalid_schedule`) instead of failing the scan; a database
20
- * error fails the pass (the next minute retries) and pauses nothing.
18
+ * job that failed for good before its claim: the tick is missed, `job-ids.ts`). A task whose stored
19
+ * trigger the schedule math cannot evaluate is paused (`invalid_schedule`) instead of failing the
20
+ * scan; a database error fails the pass (the next minute retries) and pauses nothing.
21
21
  */
22
22
  export declare class ScheduledTaskScanJob {
23
23
  private readonly kdb;
@@ -33,9 +33,9 @@ export declare class ScheduledTaskScanJob {
33
33
  /** One scan as of `now` (tests pass their own); returns how many due tasks it handled. */
34
34
  scanAt(now: Date): Promise<number>;
35
35
  /**
36
- * One due task in its own transaction (a task whose trigger cannot be evaluated rolls back
37
- * alone and is paused, so it cannot block the others): locked with `SKIP LOCKED`, so replicas take different tasks. False
38
- * when no task is due.
36
+ * One due task in its own transaction (a task whose trigger cannot be evaluated rolls back alone
37
+ * and is paused, so it cannot block the others): locked with `SKIP LOCKED`, so replicas take
38
+ * different tasks. False when no task is due.
39
39
  */
40
40
  private scanOne;
41
41
  /** A task the scan cannot advance stops being due: paused with `invalid_schedule`. */
@@ -46,9 +46,9 @@ class TaskScanFailed extends Error {
46
46
  * transaction owes `sched-fire-<taskRunId>` (transactional outbox spec §8.1), so a written tick is
47
47
  * never left unfired and the scan needs no repair of lost fire jobs (a fire delivered too late
48
48
  * marks its tick missed itself, `ScheduledTaskFirer`), and the run's unfired settle check (a fire
49
- * job that failed for good before its claim: the tick is missed, `job-ids.ts`). A task whose stored trigger the schedule
50
- * math cannot evaluate is paused (`invalid_schedule`) instead of failing the scan; a database
51
- * error fails the pass (the next minute retries) and pauses nothing.
49
+ * job that failed for good before its claim: the tick is missed, `job-ids.ts`). A task whose stored
50
+ * trigger the schedule math cannot evaluate is paused (`invalid_schedule`) instead of failing the
51
+ * scan; a database error fails the pass (the next minute retries) and pauses nothing.
52
52
  */
53
53
  let ScheduledTaskScanJob = class ScheduledTaskScanJob {
54
54
  kdb;
@@ -96,9 +96,9 @@ let ScheduledTaskScanJob = class ScheduledTaskScanJob {
96
96
  return total;
97
97
  }
98
98
  /**
99
- * One due task in its own transaction (a task whose trigger cannot be evaluated rolls back
100
- * alone and is paused, so it cannot block the others): locked with `SKIP LOCKED`, so replicas take different tasks. False
101
- * when no task is due.
99
+ * One due task in its own transaction (a task whose trigger cannot be evaluated rolls back alone
100
+ * and is paused, so it cannot block the others): locked with `SKIP LOCKED`, so replicas take
101
+ * different tasks. False when no task is due.
102
102
  */
103
103
  async scanOne(now, settings) {
104
104
  const t = this.db;
@@ -156,13 +156,17 @@ export declare class SkillsCatalogService {
156
156
  constructor(db: KernelDb, eventLog: EventLog, log: KernelLogger, txHost: TransactionHost, config: KernelConfigService);
157
157
  /**
158
158
  * Records the run start's snapshot (see the class comment). `opts.log`: the run's logger
159
- * (default the kernel logger).
159
+ * (default the kernel logger). False: the run had ended (a stop got there first), so nothing was
160
+ * appended and the run must not go on.
160
161
  */
161
162
  record(input: EffectiveSkillsInput & {
162
163
  runId: string;
163
164
  }, opts?: {
164
165
  log?: Pick<Logger, "warn" | "error">;
165
- }): Promise<void>;
166
- /** Under the conversation row lock, resolves the skills and appends a changed snapshot. */
166
+ }): Promise<boolean>;
167
+ /**
168
+ * Under the conversation row lock, resolves the skills and appends a changed snapshot; `ended`
169
+ * (nothing appended) when the run is terminal under the locks.
170
+ */
167
171
  private append;
168
172
  }
@@ -18,6 +18,7 @@ import { Transactional } from "../infra/database/transactional.decorator.js";
18
18
  import { KERNEL_LOGGER } from "../infra/tokens.js";
19
19
  import { CLAUDE_CODE_TOOL_MAP } from "../runtime/context/claude-tools.js";
20
20
  import { catalogLine, sameSnapshot, } from "../runtime/context/skills.js";
21
+ import { runEndedIn } from "../runtime/run-status.js";
21
22
  import { agentOwnSkillIds, agentSkills, builtinSkillIds, builtinSkillPlugins, } from "./usable.js";
22
23
  /**
23
24
  * The skills in effect for this conversation now, sorted by name (code point order), as
@@ -264,16 +265,25 @@ let SkillsCatalogService = class SkillsCatalogService {
264
265
  }
265
266
  /**
266
267
  * Records the run start's snapshot (see the class comment). `opts.log`: the run's logger
267
- * (default the kernel logger).
268
+ * (default the kernel logger). False: the run had ended (a stop got there first), so nothing was
269
+ * appended and the run must not go on.
268
270
  */
269
271
  async record(input, opts = {}) {
270
272
  const toSeq = await this.append(input, opts.log ?? this.log);
273
+ if (toSeq === "ended")
274
+ return false;
271
275
  if (toSeq !== null)
272
276
  await this.eventLog.notify(input.conversationId, toSeq);
277
+ return true;
273
278
  }
274
- /** Under the conversation row lock, resolves the skills and appends a changed snapshot. */
279
+ /**
280
+ * Under the conversation row lock, resolves the skills and appends a changed snapshot; `ended`
281
+ * (nothing appended) when the run is terminal under the locks.
282
+ */
275
283
  async append(input, log) {
276
284
  const last = await lockLatestCatalog(this.db.db, input.conversationId);
285
+ if (await runEndedIn(this.db.db, input.runId))
286
+ return "ended";
277
287
  const all = await resolveSkills(this.db.db, input, log);
278
288
  const resolved = forModel(all, input, log);
279
289
  const budget = this.config.get().skills.catalogBudgetChars;
@@ -6,7 +6,7 @@ import { KernelDb } from "../infra/database/kernel-db.js";
6
6
  import { TransactionHost } from "../infra/database/transaction-host.js";
7
7
  import { KernelEvents } from "../infra/events/kernel-events.js";
8
8
  import { LockService } from "../infra/lock/lock.service.js";
9
- import { DevicePolicyService } from "../local-devices/device-policy.service.js";
9
+ import { DevicePolicyService, type PolicyOpResult } from "../local-devices/device-policy.service.js";
10
10
  import { type OrgRole } from "../orgs/roles.js";
11
11
  import { RunResume } from "../runtime/run-jobs.js";
12
12
  import { SandboxService } from "../sandbox/sandbox.service.js";
@@ -47,7 +47,6 @@ export interface NetworkAskDetail {
47
47
  };
48
48
  /** The hosts the decision allowed (every approved decision writes them; none: nothing). */
49
49
  grantedHosts?: string[];
50
- /** What took effect: a run grant, the device's policy, or a run grant because the device did not apply it. */
51
50
  /**
52
51
  * What took effect: a run grant (`once`), the device's policy (`always`), a run grant because
53
52
  * the device could not be reached or lacks the feature (`once_fallback`), or nothing because the
@@ -56,6 +55,22 @@ export interface NetworkAskDetail {
56
55
  outcome?: "once" | "always" | "once_fallback" | "refused";
57
56
  /** Why 「一直允许」 did not reach the device's policy. */
58
57
  fallback?: "offline" | "refused" | "unsupported";
58
+ /**
59
+ * Set while the ask is still pending: the last 「一直允许」 whose device change was recorded only
60
+ * after a crash (transactional outbox spec §7.4) and did not take effect: `failed` (the device
61
+ * refused it, never received it or stopped while applying it) or `unknown` (the device did not
62
+ * answer; its next connection tells), with the reason. The owner may decide again.
63
+ */
64
+ attempt?: NetworkAskAttempt;
65
+ }
66
+ /** Why a pending network ask's last 「一直允许」 did not decide it ({@link NetworkAskDetail.attempt}). */
67
+ export interface NetworkAskAttempt {
68
+ /**
69
+ * `applied_undecided`: the device applied the change, but the account that chose it can no
70
+ * longer decide this ask (someone else must).
71
+ */
72
+ outcome: "failed" | "unknown" | "applied_undecided";
73
+ message: string;
59
74
  }
60
75
  /** A network ask's detail as the API shows it: without the kept command result. */
61
76
  export type NetworkAskView = Omit<NetworkAskDetail, "result">;
@@ -189,7 +204,7 @@ export interface NetworkAskInput {
189
204
  callId: string;
190
205
  /** The device owner: the only person who may decide. */
191
206
  requestedFor: string;
192
- detail: Omit<NetworkAskDetail, "grantedHosts" | "outcome" | "fallback">;
207
+ detail: Omit<NetworkAskDetail, "grantedHosts" | "outcome" | "fallback" | "attempt">;
193
208
  /** `["once", "always"]`, or `["once"]` when the device takes no remote grants. */
194
209
  scopes: ("once" | "always")[];
195
210
  }
@@ -223,7 +238,10 @@ export declare class ApprovalsService {
223
238
  private readonly schedules;
224
239
  /** A trial approval is kept only for a tool the task may hold as a grant (its agent's tools). */
225
240
  private readonly registry;
226
- /** 「这台设备一直允许」 sends the `policy` op to the device before the decision is recorded. */
241
+ /**
242
+ * 「这台设备一直允许」 records the device change's intent, sends the `policy` op, then records
243
+ * its outcome with the decision.
244
+ */
227
245
  private readonly policies;
228
246
  /** One decision of a network ask at a time, across replicas (the op is sent before the transaction). */
229
247
  readonly lockService: LockService;
@@ -234,8 +252,11 @@ export declare class ApprovalsService {
234
252
  constructor(db: KernelDb, config: Pick<KernelConfigService, "get">, eventLog: Pick<EventLog, "appendIn" | "notify">, events: Pick<KernelEvents, "emit">, txHost: TransactionHost, schedules: Pick<ScheduledTaskStore, "addGrant">,
235
253
  /** A trial approval is kept only for a tool the task may hold as a grant (its agent's tools). */
236
254
  registry: Pick<ToolRegistry, "forAgent">,
237
- /** 「这台设备一直允许」 sends the `policy` op to the device before the decision is recorded. */
238
- policies: Pick<DevicePolicyService, "apply" | "summary">,
255
+ /**
256
+ * 「这台设备一直允许」 records the device change's intent, sends the `policy` op, then records
257
+ * its outcome with the decision.
258
+ */
259
+ policies: Pick<DevicePolicyService, "begin" | "send" | "settleIn" | "summary">,
239
260
  /** One decision of a network ask at a time, across replicas (the op is sent before the transaction). */
240
261
  lockService: LockService,
241
262
  /** A trial run's 「这次允许」 becomes a task grant only within the org's egress. */
@@ -272,28 +293,84 @@ export declare class ApprovalsService {
272
293
  * A network ask's decision (network ask-to-allow spec §5.2), one at a time per ask across
273
294
  * replicas: only the device owner (`requested_for`) decides, in a signed-in session (a personal
274
295
  * access token is refused; the route already refuses service and embed tokens); the allowed
275
- * hosts are a subset of those asked. 「这台设备一直允许」 sends the `policy` op first, outside any
276
- * transaction; when the device cannot be reached or lacks the feature the decision is recorded
277
- * as a run grant with the reason, and when the device refuses (its owner takes no remote grants)
278
- * as `always` with outcome `refused` and nothing granted. Then {@link decideIn} records what took
279
- * effect and the run resumes after the commit; a change the device applied after the ask closed
280
- * is audited (`late`) and answered with a 409 that says so.
296
+ * hosts are a subset of those asked. 「这台设备一直允许」 goes through the device change's intent
297
+ * ({@link decideAlways}): when the device cannot be reached or lacks the feature the decision is
298
+ * recorded as a run grant with the reason, and when the device refuses (its owner takes no
299
+ * remote grants) as `always` with outcome `refused` and nothing granted; the decision and the
300
+ * op's outcome are one transaction. Every decision resumes the run in its transaction and is
301
+ * announced after the commit; a change the device applied after the ask closed is audited
302
+ * (`late`) and answered with a 409 that says so.
281
303
  */
282
304
  private decideNetwork;
283
- /** `device.network_allowed` of a policy change that reached the device after the ask closed. */
284
- private auditLateAllow;
285
- /** The owner of a live device of the org (null: none, deleted, or of another org). */
286
- private deviceOwnerOf;
287
305
  /**
288
- * Adds the hosts to the device's local policy (the `policy` op; never inside a transaction).
289
- * Null when it was applied, else why not: the decision falls back to a run grant.
306
+ * 「这台设备一直允许」 (transactional outbox spec §7): the intent of the device change (naming the
307
+ * ask) is recorded, the `policy` op sent (no transaction open), then {@link settleNetworkOp}
308
+ * decides the ask and records the op's outcome in one transaction. When nothing could be sent
309
+ * (the device is offline, deleted, or its program lacks the feature) the decision is a run
310
+ * grant with the reason. The device's answer decides: applied → `always`; refused → `always`
311
+ * with outcome `refused`, nothing granted; no answer → a run grant (`once_fallback`, `offline`)
312
+ * while the op stays `unknown` for the machine's next hello or the resolver. A device that
313
+ * reconnects during the call and reports the op applied lets the resolver approve the ask as
314
+ * this owner first: that decision is returned (`toSeq` null: already announced), never a 409;
315
+ * when it granted other hosts than this call's (another of the owner's changes) and this call's
316
+ * change was not confirmed, `approval.already_decided` says so. A device deleted during the
317
+ * call is `device.not_found` (the op recorded unknown).
290
318
  */
291
- private applyAlways;
319
+ private decideAlways;
320
+ /** Records only the op's outcome, in its own transaction (the ask is left as it is). */
321
+ private settleOpOnly;
322
+ /**
323
+ * The resolve transaction of a network ask's device change: the ask decided as `network` says
324
+ * ({@link decideTx}: lock order conversation → approval → run), then the op's outcome recorded
325
+ * (`DevicePolicyService.settleIn`: the machine's row). When the ask closed meanwhile (a reply,
326
+ * the expiry, a stop) only the op's outcome is recorded, and a change the device applied is
327
+ * audited `device.network_allowed` with `late: true` in the same transaction.
328
+ */
329
+ private settleNetworkOp;
330
+ /**
331
+ * The op's outcome for an ask that is no longer pending (in the caller's transaction): recorded,
332
+ * and a change the device applied audited `device.network_allowed` `late` (the change stands:
333
+ * the owner chose it; no inverse change is sent). True when it was audited late. `undecided`:
334
+ * the ask is still pending but the op's account can no longer decide it; the allow is audited
335
+ * without `late` (it is not after the ask) and the caller gives the ask the reason.
336
+ */
337
+ private settleClosedIn;
338
+ /** The live intent `opId` on machine `machineId` (null: settled already, or expired). */
339
+ private pendingOpOf;
340
+ /**
341
+ * The `device-op-resolve` job's outcome of an op that answers network ask `op.source.approvalId`
342
+ * (transactional outbox spec §7.4), in the resolver's transaction: under the ask's locks
343
+ * (conversation, then the approval) the op is read again (settled meanwhile: nothing to do).
344
+ * Applied, and the ask still pending → approved `always` by the op's account (`via` as it
345
+ * asked), the run resumed in this transaction as every decision does; the ask closed → audited
346
+ * `late`; still pending but the op's account can no longer decide it → the allow audited (not
347
+ * late) and the ask left pending with `applied_undecided`. Not applied (refused, never received,
348
+ * interrupted) or still unknown → the op's outcome recorded and the ask left pending with the
349
+ * reason (`detail.attempt`) and an `approval_updated` event so the card shows it: no person is
350
+ * there to fall back to a run grant (D2).
351
+ */
352
+ settleFromResolver(ref: {
353
+ machineId: string;
354
+ opId: string;
355
+ approvalId: string;
356
+ }, result: PolicyOpResult): Promise<void>;
357
+ /**
358
+ * A pending network ask's reason (`detail.attempt`) and its `approval_updated` event (one per
359
+ * op and outcome), in the caller's transaction with the ask locked; notified after the commit.
360
+ */
361
+ private attemptIn;
362
+ /** The owner of a live device of the org (null: none, deleted, or of another org). */
363
+ private deviceOwnerOf;
292
364
  /** After the commit: the decision's events reach the page, the run's waits and IM delivery. */
293
365
  private announceDecided;
294
366
  /** An account's display name (null when unknown). */
295
367
  private displayNameOf;
296
368
  private decideIn;
369
+ /**
370
+ * A decision in the caller's transaction: lock order conversation → approval → run (the resume).
371
+ * Refuses (`approval.already_decided`, `approval.not_found`, …) before any write.
372
+ */
373
+ private decideTx;
297
374
  /**
298
375
  * A trial run of a scheduled task (scheduled tasks spec §7): an approval its creator granted, in
299
376
  * any scope, is also added to the task's grants with the approved definition's hash, so its