@telora/daemon 0.22.85 → 0.22.91
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/build-info.json +4 -3
- package/dist/backends/index.d.ts +1 -1
- package/dist/backends/index.d.ts.map +1 -1
- package/dist/backends/index.js +1 -1
- package/dist/backends/index.js.map +1 -1
- package/dist/backends/resolve.d.ts +37 -8
- package/dist/backends/resolve.d.ts.map +1 -1
- package/dist/backends/resolve.js +62 -28
- package/dist/backends/resolve.js.map +1 -1
- package/dist/ci/ci-escalation.d.ts +17 -1
- package/dist/ci/ci-escalation.d.ts.map +1 -1
- package/dist/ci/ci-escalation.js +51 -3
- package/dist/ci/ci-escalation.js.map +1 -1
- package/dist/ci/ci-red-attribution.d.ts +85 -0
- package/dist/ci/ci-red-attribution.d.ts.map +1 -0
- package/dist/ci/ci-red-attribution.js +108 -0
- package/dist/ci/ci-red-attribution.js.map +1 -0
- package/dist/ci/ci-sensor-loop.d.ts +84 -80
- package/dist/ci/ci-sensor-loop.d.ts.map +1 -1
- package/dist/ci/ci-sensor-loop.js +132 -129
- package/dist/ci/ci-sensor-loop.js.map +1 -1
- package/dist/cli/connect/config-write.d.ts +7 -4
- package/dist/cli/connect/config-write.d.ts.map +1 -1
- package/dist/cli/connect/config-write.js +13 -7
- package/dist/cli/connect/config-write.js.map +1 -1
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +11 -1
- package/dist/cli/connect.js.map +1 -1
- package/dist/cli/update-commands.d.ts.map +1 -1
- package/dist/cli/update-commands.js +2 -1
- package/dist/cli/update-commands.js.map +1 -1
- package/dist/completion/coverage-stabilized.d.ts.map +1 -1
- package/dist/completion/coverage-stabilized.js +1 -1
- package/dist/completion/coverage-stabilized.js.map +1 -1
- package/dist/completion/escalations.d.ts +40 -5
- package/dist/completion/escalations.d.ts.map +1 -1
- package/dist/completion/escalations.js +19 -11
- package/dist/completion/escalations.js.map +1 -1
- package/dist/completion/event-escalations.d.ts +12 -1
- package/dist/completion/event-escalations.d.ts.map +1 -1
- package/dist/completion/event-escalations.js +73 -37
- package/dist/completion/event-escalations.js.map +1 -1
- package/dist/completion/event.d.ts +4 -1
- package/dist/completion/event.d.ts.map +1 -1
- package/dist/completion/event.js +1 -1
- package/dist/completion/event.js.map +1 -1
- package/dist/completion/human-only-act.d.ts +1 -1
- package/dist/completion/human-only-act.js +1 -1
- package/dist/completion/machine-remediable-demotion.d.ts +28 -1
- package/dist/completion/machine-remediable-demotion.d.ts.map +1 -1
- package/dist/completion/machine-remediable-demotion.js +9 -6
- package/dist/completion/machine-remediable-demotion.js.map +1 -1
- package/dist/completion/read-only-pristine.d.ts.map +1 -1
- package/dist/completion/read-only-pristine.js +4 -0
- package/dist/completion/read-only-pristine.js.map +1 -1
- package/dist/completion/review-exit-deps.js +1 -1
- package/dist/completion/review-exit-deps.js.map +1 -1
- package/dist/completion/review-exit-escalations.d.ts +10 -0
- package/dist/completion/review-exit-escalations.d.ts.map +1 -1
- package/dist/completion/review-exit-escalations.js +38 -3
- package/dist/completion/review-exit-escalations.js.map +1 -1
- package/dist/completion/review-exit-phase.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.js +8 -7
- package/dist/completion/review-exit-phase.js.map +1 -1
- package/dist/completion/review-exit-shortfall.d.ts +5 -1
- package/dist/completion/review-exit-shortfall.d.ts.map +1 -1
- package/dist/completion/review-exit-shortfall.js +8 -3
- package/dist/completion/review-exit-shortfall.js.map +1 -1
- package/dist/completion/status-advance-phase.d.ts +2 -0
- package/dist/completion/status-advance-phase.d.ts.map +1 -1
- package/dist/completion/status-advance-phase.js +2 -1
- package/dist/completion/status-advance-phase.js.map +1 -1
- package/dist/completion/terminated-work-reconcile.js +2 -2
- package/dist/completion/terminated-work-reconcile.js.map +1 -1
- package/dist/delivery/delivery-advance-block.d.ts +3 -3
- package/dist/delivery/delivery-advance-block.d.ts.map +1 -1
- package/dist/delivery/delivery-advance-block.js +13 -8
- package/dist/delivery/delivery-advance-block.js.map +1 -1
- package/dist/delivery/delivery-lifecycle.d.ts +10 -0
- package/dist/delivery/delivery-lifecycle.d.ts.map +1 -1
- package/dist/delivery/delivery-lifecycle.js +12 -6
- package/dist/delivery/delivery-lifecycle.js.map +1 -1
- package/dist/delivery/delivery-merge-ops.d.ts.map +1 -1
- package/dist/delivery/delivery-merge-ops.js +39 -21
- package/dist/delivery/delivery-merge-ops.js.map +1 -1
- package/dist/delivery/external-hold.d.ts +8 -2
- package/dist/delivery/external-hold.d.ts.map +1 -1
- package/dist/delivery/external-hold.js +12 -5
- package/dist/delivery/external-hold.js.map +1 -1
- package/dist/delivery/operator-hold.d.ts +6 -1
- package/dist/delivery/operator-hold.d.ts.map +1 -1
- package/dist/delivery/operator-hold.js +31 -10
- package/dist/delivery/operator-hold.js.map +1 -1
- package/dist/delivery/paused-revival.d.ts +6 -0
- package/dist/delivery/paused-revival.d.ts.map +1 -1
- package/dist/delivery/paused-revival.js +3 -1
- package/dist/delivery/paused-revival.js.map +1 -1
- package/dist/directive/directive-dispatch.d.ts.map +1 -1
- package/dist/directive/directive-dispatch.js +6 -3
- package/dist/directive/directive-dispatch.js.map +1 -1
- package/dist/directive/directive-queue.d.ts.map +1 -1
- package/dist/directive/directive-queue.js +4 -1
- package/dist/directive/directive-queue.js.map +1 -1
- package/dist/drift/staleness-sensor.d.ts +20 -0
- package/dist/drift/staleness-sensor.d.ts.map +1 -1
- package/dist/drift/staleness-sensor.js +54 -21
- package/dist/drift/staleness-sensor.js.map +1 -1
- package/dist/drift/staleness.d.ts +18 -5
- package/dist/drift/staleness.d.ts.map +1 -1
- package/dist/drift/staleness.js +28 -15
- package/dist/drift/staleness.js.map +1 -1
- package/dist/focus/focus-executor.d.ts.map +1 -1
- package/dist/focus/focus-executor.js +6 -8
- package/dist/focus/focus-executor.js.map +1 -1
- package/dist/focus/focus-loop-registry.d.ts.map +1 -1
- package/dist/focus/focus-loop-registry.js +23 -13
- package/dist/focus/focus-loop-registry.js.map +1 -1
- package/dist/focus/focus-respawn-guard.d.ts.map +1 -1
- package/dist/focus/focus-respawn-guard.js +21 -4
- package/dist/focus/focus-respawn-guard.js.map +1 -1
- package/dist/focus/listener-auto-advance.d.ts.map +1 -1
- package/dist/focus/listener-auto-advance.js +10 -6
- package/dist/focus/listener-auto-advance.js.map +1 -1
- package/dist/focus/verify-open-issues.d.ts.map +1 -1
- package/dist/focus/verify-open-issues.js +21 -1
- package/dist/focus/verify-open-issues.js.map +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts.map +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.js +35 -14
- package/dist/focus-stage-lifecycle/wedge-watchdog.js.map +1 -1
- package/dist/loops/index.d.ts.map +1 -1
- package/dist/loops/index.js +2 -1
- package/dist/loops/index.js.map +1 -1
- package/dist/merge/focus-merge.d.ts +1 -1
- package/dist/merge/focus-merge.d.ts.map +1 -1
- package/dist/merge/focus-merge.js +5 -1
- package/dist/merge/focus-merge.js.map +1 -1
- package/dist/merge/merge-back-loop.d.ts +2 -1
- package/dist/merge/merge-back-loop.d.ts.map +1 -1
- package/dist/merge/merge-back-loop.js +15 -12
- package/dist/merge/merge-back-loop.js.map +1 -1
- package/dist/merge/merge-conflict-escalation.d.ts.map +1 -1
- package/dist/merge/merge-conflict-escalation.js +4 -0
- package/dist/merge/merge-conflict-escalation.js.map +1 -1
- package/dist/nexus/return.d.ts +14 -15
- package/dist/nexus/return.d.ts.map +1 -1
- package/dist/nexus/return.js +41 -31
- package/dist/nexus/return.js.map +1 -1
- package/dist/prompt-sections/execution-sections.d.ts +13 -5
- package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
- package/dist/prompt-sections/execution-sections.js +78 -18
- package/dist/prompt-sections/execution-sections.js.map +1 -1
- package/dist/prompt-sections/routing-decider-sections.d.ts +60 -0
- package/dist/prompt-sections/routing-decider-sections.d.ts.map +1 -0
- package/dist/prompt-sections/routing-decider-sections.js +197 -0
- package/dist/prompt-sections/routing-decider-sections.js.map +1 -0
- package/dist/queries/agent-resolvable-aging.d.ts +4 -3
- package/dist/queries/agent-resolvable-aging.d.ts.map +1 -1
- package/dist/queries/agent-resolvable-aging.js +5 -4
- package/dist/queries/agent-resolvable-aging.js.map +1 -1
- package/dist/queries/agent-resolvable-promotion.d.ts +172 -124
- package/dist/queries/agent-resolvable-promotion.d.ts.map +1 -1
- package/dist/queries/agent-resolvable-promotion.js +332 -307
- package/dist/queries/agent-resolvable-promotion.js.map +1 -1
- package/dist/queries/agent-resolvable-retirement.d.ts +105 -0
- package/dist/queries/agent-resolvable-retirement.d.ts.map +1 -0
- package/dist/queries/agent-resolvable-retirement.js +227 -0
- package/dist/queries/agent-resolvable-retirement.js.map +1 -0
- package/dist/queries/cause-key.d.ts +109 -0
- package/dist/queries/cause-key.d.ts.map +1 -1
- package/dist/queries/cause-key.js +93 -1
- package/dist/queries/cause-key.js.map +1 -1
- package/dist/queries/deliveries.d.ts +14 -0
- package/dist/queries/deliveries.d.ts.map +1 -1
- package/dist/queries/deliveries.js +20 -2
- package/dist/queries/deliveries.js.map +1 -1
- package/dist/queries/delivery-gate-state.d.ts +130 -5
- package/dist/queries/delivery-gate-state.d.ts.map +1 -1
- package/dist/queries/delivery-gate-state.js +111 -2
- package/dist/queries/delivery-gate-state.js.map +1 -1
- package/dist/queries/escalation-consumption.d.ts +95 -15
- package/dist/queries/escalation-consumption.d.ts.map +1 -1
- package/dist/queries/escalation-consumption.js +162 -24
- package/dist/queries/escalation-consumption.js.map +1 -1
- package/dist/queries/escalation-dispositions.d.ts +19 -7
- package/dist/queries/escalation-dispositions.d.ts.map +1 -1
- package/dist/queries/escalation-dispositions.js +7 -0
- package/dist/queries/escalation-dispositions.js.map +1 -1
- package/dist/queries/escalation-suppression.d.ts +63 -19
- package/dist/queries/escalation-suppression.d.ts.map +1 -1
- package/dist/queries/escalation-suppression.js +64 -35
- package/dist/queries/escalation-suppression.js.map +1 -1
- package/dist/queries/escalations.d.ts +127 -70
- package/dist/queries/escalations.d.ts.map +1 -1
- package/dist/queries/escalations.js +170 -188
- package/dist/queries/escalations.js.map +1 -1
- package/dist/queries/filing-observation.d.ts +82 -0
- package/dist/queries/filing-observation.d.ts.map +1 -0
- package/dist/queries/filing-observation.js +157 -0
- package/dist/queries/filing-observation.js.map +1 -0
- package/dist/queries/holds.d.ts +69 -2
- package/dist/queries/holds.d.ts.map +1 -1
- package/dist/queries/holds.js +106 -11
- package/dist/queries/holds.js.map +1 -1
- package/dist/queries/incident-correlation.d.ts +291 -0
- package/dist/queries/incident-correlation.d.ts.map +1 -0
- package/dist/queries/incident-correlation.js +408 -0
- package/dist/queries/incident-correlation.js.map +1 -0
- package/dist/queries/routing-measurement.d.ts +48 -1
- package/dist/queries/routing-measurement.d.ts.map +1 -1
- package/dist/queries/routing-measurement.js +64 -2
- package/dist/queries/routing-measurement.js.map +1 -1
- package/dist/queries/routing-quality.d.ts +8 -0
- package/dist/queries/routing-quality.d.ts.map +1 -1
- package/dist/queries/routing-quality.js +12 -3
- package/dist/queries/routing-quality.js.map +1 -1
- package/dist/queries/shared.js +1 -1
- package/dist/shell/crash-recovery.d.ts.map +1 -1
- package/dist/shell/crash-recovery.js +6 -6
- package/dist/shell/crash-recovery.js.map +1 -1
- package/dist/shell/heartbeat-metadata.d.ts +18 -0
- package/dist/shell/heartbeat-metadata.d.ts.map +1 -1
- package/dist/shell/heartbeat-metadata.js +24 -0
- package/dist/shell/heartbeat-metadata.js.map +1 -1
- package/dist/shell/heartbeat.d.ts.map +1 -1
- package/dist/shell/heartbeat.js +6 -17
- package/dist/shell/heartbeat.js.map +1 -1
- package/dist/shell/install-probe-tick.d.ts.map +1 -1
- package/dist/shell/install-probe-tick.js +3 -1
- package/dist/shell/install-probe-tick.js.map +1 -1
- package/dist/shell/model-resolution.d.ts +9 -4
- package/dist/shell/model-resolution.d.ts.map +1 -1
- package/dist/shell/model-resolution.js +46 -7
- package/dist/shell/model-resolution.js.map +1 -1
- package/dist/shell/unified-init.d.ts +24 -0
- package/dist/shell/unified-init.d.ts.map +1 -1
- package/dist/shell/unified-init.js +22 -17
- package/dist/shell/unified-init.js.map +1 -1
- package/dist/spawn/model-id-guards.d.ts +50 -0
- package/dist/spawn/model-id-guards.d.ts.map +1 -0
- package/dist/spawn/model-id-guards.js +110 -0
- package/dist/spawn/model-id-guards.js.map +1 -0
- package/dist/spawn/model-routing.d.ts +1 -43
- package/dist/spawn/model-routing.d.ts.map +1 -1
- package/dist/spawn/model-routing.js +3 -94
- package/dist/spawn/model-routing.js.map +1 -1
- package/dist/spawn/respawn-cycle-escalation.d.ts +10 -2
- package/dist/spawn/respawn-cycle-escalation.d.ts.map +1 -1
- package/dist/spawn/respawn-cycle-escalation.js +16 -2
- package/dist/spawn/respawn-cycle-escalation.js.map +1 -1
- package/dist/spawn/routing-decider-spawn.d.ts +93 -0
- package/dist/spawn/routing-decider-spawn.d.ts.map +1 -0
- package/dist/spawn/routing-decider-spawn.js +185 -0
- package/dist/spawn/routing-decider-spawn.js.map +1 -0
- package/dist/spawn/routing-decider.d.ts +42 -0
- package/dist/spawn/routing-decider.d.ts.map +1 -0
- package/dist/spawn/routing-decider.js +95 -0
- package/dist/spawn/routing-decider.js.map +1 -0
- package/dist/spawn/shared.d.ts +8 -0
- package/dist/spawn/shared.d.ts.map +1 -1
- package/dist/spawn/shared.js +1 -1
- package/dist/spawn/shared.js.map +1 -1
- package/dist/spawn/spawn-decision.d.ts +24 -16
- package/dist/spawn/spawn-decision.d.ts.map +1 -1
- package/dist/spawn/spawn-decision.js +28 -11
- package/dist/spawn/spawn-decision.js.map +1 -1
- package/dist/spawn/worktree-session.d.ts +23 -7
- package/dist/spawn/worktree-session.d.ts.map +1 -1
- package/dist/spawn/worktree-session.js +54 -22
- package/dist/spawn/worktree-session.js.map +1 -1
- package/dist/testing/fixtures/index.js +1 -1
- package/dist/types/index.js +1 -1
- package/dist/update/auto-update.d.ts +20 -3
- package/dist/update/auto-update.d.ts.map +1 -1
- package/dist/update/auto-update.js +8 -24
- package/dist/update/auto-update.js.map +1 -1
- package/dist/update/self-update.d.ts +43 -4
- package/dist/update/self-update.d.ts.map +1 -1
- package/dist/update/self-update.js +61 -38
- package/dist/update/self-update.js.map +1 -1
- package/dist/update/version-check.d.ts +29 -45
- package/dist/update/version-check.d.ts.map +1 -1
- package/dist/update/version-check.js +48 -79
- package/dist/update/version-check.js.map +1 -1
- package/dist/verify/failure-signature.d.ts +91 -0
- package/dist/verify/failure-signature.d.ts.map +1 -0
- package/dist/verify/failure-signature.js +233 -0
- package/dist/verify/failure-signature.js.map +1 -0
- package/dist/verify/shortfall-repeat.d.ts +12 -9
- package/dist/verify/shortfall-repeat.d.ts.map +1 -1
- package/dist/verify/shortfall-repeat.js +38 -14
- package/dist/verify/shortfall-repeat.js.map +1 -1
- package/dist/verify/stall-detectors.d.ts +19 -43
- package/dist/verify/stall-detectors.d.ts.map +1 -1
- package/dist/verify/stall-detectors.js +26 -101
- package/dist/verify/stall-detectors.js.map +1 -1
- package/dist/verify/unreviewed-settle-detector.d.ts +8 -0
- package/dist/verify/unreviewed-settle-detector.d.ts.map +1 -1
- package/dist/verify/unreviewed-settle-detector.js +15 -1
- package/dist/verify/unreviewed-settle-detector.js.map +1 -1
- package/dist/verify/verification-deps-default.d.ts.map +1 -1
- package/dist/verify/verification-deps-default.js +14 -2
- package/dist/verify/verification-deps-default.js.map +1 -1
- package/dist/verify/verification-evidence-unavailable-feedback.d.ts.map +1 -1
- package/dist/verify/verification-evidence-unavailable-feedback.js +8 -2
- package/dist/verify/verification-evidence-unavailable-feedback.js.map +1 -1
- package/dist/verify/verification-feedback.d.ts +62 -14
- package/dist/verify/verification-feedback.d.ts.map +1 -1
- package/dist/verify/verification-feedback.js +101 -28
- package/dist/verify/verification-feedback.js.map +1 -1
- package/dist/verify/verify-gate-stall.d.ts +116 -0
- package/dist/verify/verify-gate-stall.d.ts.map +1 -0
- package/dist/verify/verify-gate-stall.js +169 -0
- package/dist/verify/verify-gate-stall.js.map +1 -0
- package/dist/verify/verify-restatement.d.ts.map +1 -1
- package/dist/verify/verify-restatement.js +2 -1
- package/dist/verify/verify-restatement.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,84 +1,119 @@
|
|
|
1
1
|
// PINNED-BY-BEGIN (generated by gen:pinned-by -- do not hand-edit; checked by lint:pinned-by)
|
|
2
|
-
// Pinned by: packages/daemon/src/
|
|
2
|
+
// Pinned by: packages/daemon/src/integration/resolution-class-routing-flow.test.ts,
|
|
3
|
+
// packages/daemon/src/queries/agent-resolvable-promotion-sweep.test.ts,
|
|
3
4
|
// packages/daemon/src/queries/agent-resolvable-promotion.test.ts,
|
|
4
|
-
// packages/daemon/src/queries/agent-resolvable-retired-focus.test.ts
|
|
5
|
+
// packages/daemon/src/queries/agent-resolvable-retired-focus.test.ts,
|
|
6
|
+
// packages/daemon/src/spawn/routing-decider-spawn.test.ts
|
|
5
7
|
// PINNED-BY-END
|
|
6
8
|
/**
|
|
7
|
-
* agent_resolvable
|
|
8
|
-
* reaches the human inbox", TEL-3).
|
|
9
|
+
* The agent_resolvable ROUTING HAND-OFF (formerly the promotion backstop).
|
|
9
10
|
*
|
|
10
|
-
* agent_resolvable filings are machine-owned: hidden from the operator inbox
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
11
|
+
* agent_resolvable filings are machine-owned: hidden from the operator inbox and
|
|
12
|
+
* ridden into the focus's next coding spawn, whose agent resolves or routes each.
|
|
13
|
+
* Every daemon detector files into this lane (the createEscalation chokepoint
|
|
14
|
+
* turns any other disposition into an OBSERVATION, metadata.observation), so the
|
|
15
|
+
* daemon has NO human-facing route of its own. Some observations no coding spawn
|
|
16
|
+
* will ever route: the focus is un-armed or stop-held, the filing has no focus,
|
|
17
|
+
* the condition outlives a converged focus, or spawns rode it in and none cleared
|
|
18
|
+
* it. This module is how those still reach a decision.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* pre-filter (don't even count sessions for a fresh filing, which matters for the
|
|
28
|
-
* org-wide sweep's up-to-500-row scan); the promotion DECISION is the attempt
|
|
29
|
-
* count, and the count is carried into the promoted row's metadata so the human
|
|
30
|
-
* lane shows the evidence of the machine attempts that failed.
|
|
20
|
+
* Only an AGENT makes a filing human-facing. The old backstop refiled such a
|
|
21
|
+
* filing as needs_operator deterministically -- surfacing it to a human with no
|
|
22
|
+
* agent deciding (the verify shortfall this delivery closes). Now the org-wide
|
|
23
|
+
* sweep HANDS each eligible observation to a one-shot ROUTING DECIDER
|
|
24
|
+
* (spawn/routing-decider.ts, injected here as `decide`): an agent that assembles
|
|
25
|
+
* the intent ladder, then resolves the condition itself, declines noise, keeps it
|
|
26
|
+
* in the lane with a recorded decision, or files ONE telora_agent escalate naming
|
|
27
|
+
* the specific act only a human can perform, with sourceEscalationId lineage.
|
|
31
28
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
29
|
+
* ELIGIBILITY (evaluateCandidate):
|
|
30
|
+
* - immediately, with no age bound:
|
|
31
|
+
* stop_held -- anchored to a focus a stop-kind hold blocks (isStopHoldObservation
|
|
32
|
+
* on any open observation of that focus): no coding team spawns there.
|
|
33
|
+
* unreachable -- no focus anchor, or an un-armed focus: no coding spawn reaches it.
|
|
34
|
+
* outlives_focus -- its focus retired but the condition outlives it (agent-resolvable-retirement.ts).
|
|
35
|
+
* - past the age bound (TELORA_AGENT_RESOLVABLE_PROMOTE_HOURS), on RECORDED ATTEMPT
|
|
36
|
+
* EVIDENCE (promotionBasis): 'exhausted' (>= minAttempts agent sessions ran on its
|
|
37
|
+
* focus and none cleared it) or 'unreachable' (a zero-attempt observation). Open-age
|
|
38
|
+
* alone never hands off a plain filing.
|
|
39
|
+
* A focus-scoped candidate on a retired focus is still retired auto_swept.
|
|
37
40
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
+
* BOUNDS: at most maxPerSweep hand-offs per sweep (ROUTING_DECIDER_MAX_PER_SWEEP),
|
|
42
|
+
* each needing a governor slot inside the decider. Each hand-off whose decider RAN is
|
|
43
|
+
* recorded on the observation (metadata decider_attempts / last_decided_at /
|
|
44
|
+
* last_decider_outcome) and is not re-handed inside the decider window. A decider
|
|
45
|
+
* that cannot run (no slot, spawn error) or fails leaves the observation OPEN in the
|
|
46
|
+
* lane -- there is no deterministic human fallback -- and is counted `undecided` in
|
|
47
|
+
* the sweep counts the loop tick logs.
|
|
48
|
+
*
|
|
49
|
+
* CLOSE THE SOURCE: an open human-lane row whose metadata.originalEscalationId names
|
|
50
|
+
* an open observation means a decider already made it human-facing; the sweep closes
|
|
51
|
+
* the observation auto_swept (no actor, so never judged -- the mis-route measurement
|
|
52
|
+
* judges the decider row's eventual resolver against this source's lane).
|
|
41
53
|
*/
|
|
42
|
-
import {
|
|
43
|
-
import { markEscalationResolved as defaultMarkEscalationResolved
|
|
44
|
-
import {
|
|
45
|
-
import { getActiveFocuses as defaultGetActiveFocuses } from './focus-discovery.js';
|
|
54
|
+
import { loadEnvConfig } from '@telora/daemon-core';
|
|
55
|
+
import { markEscalationResolved as defaultMarkEscalationResolved } from './escalations.js';
|
|
56
|
+
import { checkFocusRetirement, defaultFocusRetired, defaultPriorRetirementOfCause, defaultRetireSwept, memoizeFocusRetired, peekFocusRetirement, } from './agent-resolvable-retirement.js';
|
|
46
57
|
import { countFocusSessionsSince as defaultCountFocusSessionsSince } from './sessions.js';
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
import {
|
|
58
|
+
import { isMachineStopObservation } from './holds.js';
|
|
59
|
+
import { listOpenAgentResolvableForFocus as defaultListOpenAgentResolvableForFocus, listOpenAgentResolvableForOrg as defaultListOpenAgentResolvableForOrg, listOpenLineageCarriersForOrg as defaultListOpenLineageCarriersForOrg, } from './escalation-consumption.js';
|
|
60
|
+
import { callApi } from './shared.js';
|
|
50
61
|
import { createLogger } from '../shell/log.js';
|
|
51
62
|
const log = createLogger({ module: 'escalations' });
|
|
52
|
-
/** Default hours an agent_resolvable filing may stay open before
|
|
63
|
+
/** Default hours an agent_resolvable filing may stay open before the attempt-evidence gate applies. */
|
|
53
64
|
export const DEFAULT_AGENT_RESOLVABLE_PROMOTE_HOURS = 24;
|
|
54
65
|
/**
|
|
55
66
|
* Minimum recorded agent sessions on a filing's focus (since it was created)
|
|
56
|
-
* before
|
|
57
|
-
* spawns rode
|
|
58
|
-
* first attempt. A candidate below this floor is NOT promoted regardless of age.
|
|
67
|
+
* before an over-age filing is handed to a routing decider on the 'exhausted'
|
|
68
|
+
* basis: at least this many spawns rode it in and none cleared it.
|
|
59
69
|
*/
|
|
60
70
|
export const DEFAULT_AGENT_RESOLVABLE_PROMOTE_MIN_ATTEMPTS = 1;
|
|
61
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Hand-offs per sweep. Recorded choice: 2. Each hand-off is an Opus one-shot
|
|
73
|
+
* session (spawn/routing-decider-spawn.ts ROUTING_DECIDER_MODEL); on the 30-minute
|
|
74
|
+
* sweep cadence 2 bounds worst-case decider spend to ~96 sessions/day while a
|
|
75
|
+
* backlog still drains in priority order (stop_held first). Eligible observations
|
|
76
|
+
* past the cap are counted `deferred` and handed on a later sweep.
|
|
77
|
+
*/
|
|
78
|
+
export const ROUTING_DECIDER_MAX_PER_SWEEP = 2;
|
|
79
|
+
/** Pure: whether a filing's recorded attempts meet the floor. */
|
|
62
80
|
export function meetsAttemptFloor(attempts, minAttempts) {
|
|
63
81
|
return attempts >= minAttempts;
|
|
64
82
|
}
|
|
65
83
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
84
|
+
* Pure: the evidence basis on which an OVER-AGE candidate is handed off, or null
|
|
85
|
+
* (it stays in the machine lane).
|
|
86
|
+
* - 'exhausted': at least minAttempts agent sessions ran on its focus since it
|
|
87
|
+
* was filed and none cleared it.
|
|
88
|
+
* - 'unreachable': it is a detector OBSERVATION and NO agent session has reached it.
|
|
89
|
+
* A plain (non-observation) filing with zero attempts is not handed off.
|
|
90
|
+
*/
|
|
91
|
+
export function promotionBasis(attempts, minAttempts, isObservation) {
|
|
92
|
+
if (meetsAttemptFloor(attempts, minAttempts))
|
|
93
|
+
return 'exhausted';
|
|
94
|
+
if (attempts === 0 && isObservation)
|
|
95
|
+
return 'unreachable';
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/** Pure: whole hours a filing has been open (>= 1; an unparseable createdAt reads as 1). */
|
|
99
|
+
export function openAgeHours(createdAt, nowMs) {
|
|
100
|
+
const created = Date.parse(createdAt);
|
|
101
|
+
if (Number.isNaN(created))
|
|
102
|
+
return 1;
|
|
103
|
+
return Math.max(1, Math.floor((nowMs - created) / 3_600_000));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the age bound (hours -> ms) from the typed, Zod-validated env-config
|
|
68
107
|
* (packages/daemon-core/src/env-config.ts). An unset value yields the schema
|
|
69
|
-
* default
|
|
70
|
-
* at startup rather than silently degrading to a default here.
|
|
71
|
-
*
|
|
72
|
-
* Same shape as resolveStuckBranchThresholdMs in merge-back-loop.ts.
|
|
108
|
+
* default; an invalid one fails loudly at startup.
|
|
73
109
|
*/
|
|
74
110
|
export function resolveAgentResolvablePromoteMs(env = process.env) {
|
|
75
111
|
return loadEnvConfig(env).TELORA_AGENT_RESOLVABLE_PROMOTE_HOURS * 3_600_000;
|
|
76
112
|
}
|
|
77
113
|
/**
|
|
78
|
-
* Pure: split
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* as fresh (ride) -- promotion must never fire on a missing timestamp.
|
|
114
|
+
* Pure: split open filings into the ones still within the age bound (ride) and
|
|
115
|
+
* the ones past it (promote = over-age candidates). An absent/unparseable
|
|
116
|
+
* createdAt is treated as fresh -- nothing is ever decided on a missing timestamp.
|
|
82
117
|
*/
|
|
83
118
|
export function partitionAgentResolvableByAge(items, nowMs, thresholdMs) {
|
|
84
119
|
const ride = [];
|
|
@@ -94,303 +129,293 @@ export function partitionAgentResolvableByAge(items, nowMs, thresholdMs) {
|
|
|
94
129
|
}
|
|
95
130
|
return { ride, promote };
|
|
96
131
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
/** escalation_kind for a promoted (formerly agent_resolvable) filing. */
|
|
102
|
-
export const AGENT_RESOLVABLE_PROMOTED_KIND = 'agent_resolvable_promoted';
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
// Stop-kind holds
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
103
135
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* ending with a marker pointing at the original filing (which carries the full
|
|
108
|
-
* text) so nothing is silently dropped and no refile is ever rejected for length.
|
|
109
|
-
*
|
|
110
|
-
* Without this, a candidate filed at exactly the cap (Nexus notifications fit to
|
|
111
|
-
* ESCALATION_DESCRIPTION_MAX with zero headroom) produced a ~5300-char refile the
|
|
112
|
-
* edge rejected on every sweep tick -- the row could never leave the machine
|
|
113
|
-
* lane, and `description` is not writable through product-api to trim in place.
|
|
114
|
-
* Modeled on nexus/return.ts fitNotificationDescription (invariant head kept,
|
|
115
|
-
* variable tail trimmed with a pointer). Pure + exported for direct unit testing.
|
|
136
|
+
* Blocks respawn by KIND alone, whatever it proposes and whatever its disposition
|
|
137
|
+
* (queries/escalations.ts REVIEW_COVERAGE_INCOMPLETE_ESCALATION_KIND, parked by the
|
|
138
|
+
* respawn guard directly).
|
|
116
139
|
*/
|
|
117
|
-
|
|
118
|
-
const full = `${head}${body}`;
|
|
119
|
-
if (full.length <= cap)
|
|
120
|
-
return full;
|
|
121
|
-
const marker = `...\n\n[Original filing body truncated to fit the ${cap}-char escalation ` +
|
|
122
|
-
`cap -- full text on escalation ${originalFilingId}.]`;
|
|
123
|
-
const budget = cap - head.length - marker.length;
|
|
124
|
-
if (budget <= 0) {
|
|
125
|
-
// Even head + marker do not fit: keep the head, drop the body, point to the record.
|
|
126
|
-
const minimal = `${head}[Original filing body omitted for length -- see escalation ${originalFilingId}.]`;
|
|
127
|
-
return minimal.length <= cap ? minimal : head.slice(0, cap);
|
|
128
|
-
}
|
|
129
|
-
return `${head}${body.slice(0, budget)}${marker}`;
|
|
130
|
-
}
|
|
140
|
+
const COVERAGE_HOLD_KIND = 'review_coverage_incomplete';
|
|
131
141
|
/**
|
|
132
|
-
* Pure:
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* interactive MCP session -- carries no focus by construction, and the
|
|
139
|
-
* needs_operator refile does not need a focus to be VISIBLE: the operator inbox
|
|
140
|
-
* predicate (isOperatorInboxDisposition, src/lib/escalations.ts) keys on
|
|
141
|
-
* disposition alone, so an unanchored needs_operator promotion is renderable. An
|
|
142
|
-
* anchorless promotion simply files org-scoped with an empty anchor; a
|
|
143
|
-
* focus-anchored one still stamps the focus for provenance.
|
|
142
|
+
* Pure: whether an open filing is a STOP-kind hold observation -- one whose presence
|
|
143
|
+
* blocks the focus's respawn, so no coding team will reach that focus's observations.
|
|
144
|
+
* The rule is the holds contract's ONE definition, isMachineStopObservation
|
|
145
|
+
* (queries/holds.ts: a MACHINE_STOP_KINDS observation carrying its detector's
|
|
146
|
+
* proposed_human_act), plus a review_coverage_incomplete observation, which the
|
|
147
|
+
* respawn guard parks by kind.
|
|
144
148
|
*/
|
|
145
|
-
export function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
149
|
+
export function isStopHoldObservation(item) {
|
|
150
|
+
if (!item.observation)
|
|
151
|
+
return false;
|
|
152
|
+
if (item.escalationKind === COVERAGE_HOLD_KIND)
|
|
153
|
+
return true;
|
|
154
|
+
// The item's observation was already read from metadata.observation; hand the holds
|
|
155
|
+
// rule that same parsed observation so the two can never disagree.
|
|
156
|
+
return isMachineStopObservation({
|
|
157
|
+
escalationKind: item.escalationKind,
|
|
158
|
+
metadata: { ...(item.metadata ?? {}), observation: item.observation },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** Pure: the focuses a stop-kind hold observation blocks. */
|
|
162
|
+
export function stopHeldFocusIds(items) {
|
|
163
|
+
const held = new Set();
|
|
164
|
+
for (const item of items) {
|
|
165
|
+
if (item.focusId && isStopHoldObservation(item))
|
|
166
|
+
held.add(item.focusId);
|
|
167
|
+
}
|
|
168
|
+
return held;
|
|
169
|
+
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Decider attempts (recorded on the observation row)
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
/** metadata keys a hand-off writes on the observation (escalation_update shallow-merges them). */
|
|
174
|
+
export const DECIDER_ATTEMPTS_KEY = 'decider_attempts';
|
|
175
|
+
export const LAST_DECIDED_AT_KEY = 'last_decided_at';
|
|
176
|
+
export const LAST_DECIDER_OUTCOME_KEY = 'last_decider_outcome';
|
|
177
|
+
/** Pure: the recorded decider attempts on a row (0 / null when absent or malformed). */
|
|
178
|
+
export function readDeciderAttempts(metadata) {
|
|
179
|
+
const raw = metadata?.[DECIDER_ATTEMPTS_KEY];
|
|
180
|
+
const attempts = typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
|
|
181
|
+
const at = metadata?.[LAST_DECIDED_AT_KEY];
|
|
182
|
+
const parsed = typeof at === 'string' ? Date.parse(at) : NaN;
|
|
183
|
+
return { attempts, lastDecidedAtMs: Number.isNaN(parsed) ? null : parsed };
|
|
184
|
+
}
|
|
185
|
+
/** Pure: whether a decider already ran on this row inside the window (do not re-hand it). */
|
|
186
|
+
export function withinDeciderWindow(metadata, nowMs, windowMs) {
|
|
187
|
+
const { lastDecidedAtMs } = readDeciderAttempts(metadata);
|
|
188
|
+
return lastDecidedAtMs !== null && nowMs - lastDecidedAtMs < windowMs;
|
|
189
|
+
}
|
|
190
|
+
/** Record one decider attempt on the observation row (escalation_update merges fields.metadata). */
|
|
191
|
+
export async function recordDeciderAttempt(escalationId, patch, deps = {}) {
|
|
192
|
+
const call = deps.callApi ?? callApi;
|
|
193
|
+
await call('escalation_update', { escalationId, fields: { metadata: { ...patch } } });
|
|
194
|
+
}
|
|
195
|
+
/** The default when no decider is wired: nothing runs, every eligible observation stays in the lane (counted undecided). */
|
|
196
|
+
export const NO_ROUTING_DECIDER = async () => ({
|
|
197
|
+
ran: false,
|
|
198
|
+
decided: false,
|
|
199
|
+
detail: 'no routing decider wired',
|
|
200
|
+
});
|
|
201
|
+
const HANDOFF_PRIORITY = { stop_held: 0, unreachable: 1, outlives_focus: 2, exhausted: 3 };
|
|
202
|
+
/** Pure: hand-off order -- stop_held, unreachable, outlives_focus, exhausted; oldest first within a basis. */
|
|
203
|
+
export function orderHandoffs(requests) {
|
|
204
|
+
return [...requests].sort((a, b) => HANDOFF_PRIORITY[a.basis] - HANDOFF_PRIORITY[b.basis] || a.item.createdAt.localeCompare(b.item.createdAt));
|
|
185
205
|
}
|
|
206
|
+
/** The retired-focus defaults both paths wire. */
|
|
207
|
+
const defaultRetirementDeps = {
|
|
208
|
+
focusRetired: defaultFocusRetired,
|
|
209
|
+
retireSwept: defaultRetireSwept,
|
|
210
|
+
priorRetirementOfCause: (causeKey, focusId, excludeId) => defaultPriorRetirementOfCause(causeKey, focusId, excludeId),
|
|
211
|
+
};
|
|
186
212
|
function defaultDeps() {
|
|
187
213
|
return {
|
|
188
214
|
listOpen: defaultListOpenAgentResolvableForFocus,
|
|
189
|
-
markResolved: defaultMarkEscalationResolved,
|
|
190
|
-
file: defaultFileEscalationIdempotent,
|
|
191
|
-
countSessionsSince: defaultCountFocusSessionsSince,
|
|
192
215
|
now: () => Date.now(),
|
|
193
216
|
thresholdMs: resolveAgentResolvablePromoteMs(),
|
|
194
|
-
|
|
195
|
-
focusRetired: defaultFocusRetired,
|
|
196
|
-
retireSwept: defaultRetireSwept,
|
|
217
|
+
...defaultRetirementDeps,
|
|
197
218
|
};
|
|
198
219
|
}
|
|
199
220
|
/**
|
|
200
|
-
* Reconcile a focus's open agent_resolvable filings and return the ones that
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
* to ride (it will be retried next spawn), never thrown -- reconciling must never
|
|
207
|
-
* block a spawn. This is the single entry point the spawn path calls instead of
|
|
208
|
-
* listOpenAgentResolvableForFocus.
|
|
209
|
-
*/
|
|
210
|
-
/**
|
|
211
|
-
* Whether a candidate's focus has RETIRED -- converged (every delivery done or
|
|
212
|
-
* cancelled) or no longer active (its status is complete/cancelled, so it is
|
|
213
|
-
* absent from the org's active-focus set). A candidate anchored to such a focus
|
|
214
|
-
* describes work that is finished; promoting it to a human would dump a stale row
|
|
215
|
-
* on the operator inbox (2026-09-09: escalation 9465a40e promoted onto a focus
|
|
216
|
-
* closed for hours). Fail-quiet: any read error returns not-retired, so a
|
|
217
|
-
* transient DB blip never wrongly retires a live candidate (it promotes as before).
|
|
218
|
-
*/
|
|
219
|
-
export async function defaultFocusRetired(focusId, ctx) {
|
|
220
|
-
try {
|
|
221
|
-
const dels = await defaultGetFocusDeliveries(focusId);
|
|
222
|
-
if (dels.length > 0 &&
|
|
223
|
-
dels.every(x => x.executionStatus === DELIVERY_STATUS.DONE || x.executionStatus === DELIVERY_STATUS.CANCELLED)) {
|
|
224
|
-
return { retire: true, reason: 'focus converged -- all deliveries done/cancelled' };
|
|
225
|
-
}
|
|
226
|
-
const active = await defaultGetActiveFocuses(ctx.organizationId, ctx.productId);
|
|
227
|
-
if (!active.some(f => f.focus_id === focusId)) {
|
|
228
|
-
return { retire: true, reason: 'focus is not active (complete/cancelled)' };
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
// Fail-quiet: do not retire on a read error (promote as before).
|
|
233
|
-
}
|
|
234
|
-
return { retire: false, reason: '' };
|
|
235
|
-
}
|
|
236
|
-
/** Retire a candidate whose focus has converged/terminated: resolve auto_swept. */
|
|
237
|
-
function defaultRetireSwept(id, notes) {
|
|
238
|
-
return defaultMarkEscalationResolved(id, notes, {}, 'auto_swept');
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* If the candidate's focus has retired, resolve the filing auto_swept and return
|
|
242
|
-
* true (the caller then skips promotion). Best-effort; a retire failure falls
|
|
243
|
-
* through to the normal promotion path. Absent deps => never retires.
|
|
221
|
+
* Reconcile a focus's open agent_resolvable filings and return the ones that ride
|
|
222
|
+
* into THIS spawn. The spawned agent is itself a routing authority over what rides
|
|
223
|
+
* in, so nothing is filed here: an over-age focus-scoped filing on a retired focus is
|
|
224
|
+
* retired auto_swept; every other filing rides (a condition outliving the focus rides
|
|
225
|
+
* too -- the org-wide sweep hands it to a routing decider). Best-effort: never throws
|
|
226
|
+
* past the list read. The single entry point the spawn path calls.
|
|
244
227
|
*/
|
|
245
|
-
async function maybeRetireForRetiredFocus(d, itemId, focusId, ctx) {
|
|
246
|
-
if (!focusId || !d.focusRetired || !d.retireSwept)
|
|
247
|
-
return false;
|
|
248
|
-
let verdict;
|
|
249
|
-
try {
|
|
250
|
-
verdict = await d.focusRetired(focusId, ctx);
|
|
251
|
-
}
|
|
252
|
-
catch (err) {
|
|
253
|
-
log.warn(`[escalations] focusRetired check failed for ${itemId} (focus ${focusId}): ${err.message}`);
|
|
254
|
-
return false;
|
|
255
|
-
}
|
|
256
|
-
if (!verdict.retire)
|
|
257
|
-
return false;
|
|
258
|
-
try {
|
|
259
|
-
await d.retireSwept(itemId, `Focus retired (${verdict.reason}); this agent_resolvable filing is stale -- auto-swept, not promoted.`);
|
|
260
|
-
log.info(`[escalations] Retired (NOT promoted) agent_resolvable ${itemId}: ${verdict.reason} (focus ${focusId}).`);
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
catch (err) {
|
|
264
|
-
log.warn(`[escalations] Failed to retire agent_resolvable ${itemId} for retired focus ${focusId}: ${err.message}`);
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
228
|
export async function reconcileAgentResolvableForFocus(ctx, deps = {}) {
|
|
269
229
|
const d = { ...defaultDeps(), ...deps };
|
|
230
|
+
const retirement = { ...d, focusRetired: memoizeFocusRetired(d.focusRetired) };
|
|
270
231
|
const open = await d.listOpen(ctx.focusId);
|
|
271
|
-
const { ride, promote } = partitionAgentResolvableByAge(open, (d.now ?? Date.now)(), d.thresholdMs);
|
|
272
|
-
for (const item of
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (await maybeRetireForRetiredFocus(d, item.id, ctx.focusId, ctx))
|
|
276
|
-
continue;
|
|
277
|
-
const ageHours = Math.max(1, Math.floor(((d.now ?? Date.now)() - Date.parse(item.createdAt)) / 3_600_000));
|
|
278
|
-
// Attempt-evidence gate: only promote a candidate the machine actually TRIED
|
|
279
|
-
// and failed to clear. A candidate with zero agent sessions since it was
|
|
280
|
-
// filed is not exhausted -- it rides on (and the aging observer keeps it
|
|
281
|
-
// visible in the machine lane), never dumped on a human as a first attempt.
|
|
282
|
-
const attempts = await d.countSessionsSince(ctx.focusId, item.createdAt);
|
|
283
|
-
if (!meetsAttemptFloor(attempts, d.minAttempts)) {
|
|
284
|
-
log.info(`[escalations] Not promoting agent_resolvable ${item.id} (focus ${ctx.focusId}): ` +
|
|
285
|
-
`${attempts} recorded attempt(s) < floor ${d.minAttempts}; it rides on unattempted`);
|
|
286
|
-
ride.push(item);
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
try {
|
|
290
|
-
const result = await d.file(buildAgentResolvablePromotionParams(item, ctx, ageHours, attempts));
|
|
291
|
-
// Only retire the machine-routed row once the human-facing refile is in
|
|
292
|
-
// place (filed OR already suppressed by a prior promotion) -- never resolve
|
|
293
|
-
// the source without a landing spot, which would drop the signal entirely.
|
|
294
|
-
if (result.filed || result.suppressed) {
|
|
295
|
-
await d.markResolved(item.id, `Promoted to a needs_operator escalation after ~${ageHours}h open without ` +
|
|
296
|
-
`autonomous resolution (agent_resolvable -> needs_operator).`);
|
|
297
|
-
log.info(`[escalations] Promoted agent_resolvable ${item.id} to needs_operator (focus ${ctx.focusId}, ~${ageHours}h)`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
catch (err) {
|
|
301
|
-
log.warn(`[escalations] Failed to promote agent_resolvable ${item.id} for focus ${ctx.focusId}: ${err.message}`);
|
|
302
|
-
// Leave it to ride this spawn; next reconcile retries the promotion.
|
|
232
|
+
const { ride, promote: overAge } = partitionAgentResolvableByAge(open, (d.now ?? Date.now)(), d.thresholdMs);
|
|
233
|
+
for (const item of overAge) {
|
|
234
|
+
const retired = await checkFocusRetirement(retirement, item, ctx.focusId, ctx);
|
|
235
|
+
if (retired.kind !== 'retired')
|
|
303
236
|
ride.push(item);
|
|
304
|
-
}
|
|
305
237
|
}
|
|
306
238
|
return ride;
|
|
307
239
|
}
|
|
308
240
|
function defaultSweepDeps() {
|
|
309
241
|
return {
|
|
310
242
|
listOpenForOrg: defaultListOpenAgentResolvableForOrg,
|
|
243
|
+
listLineageCarriers: defaultListOpenLineageCarriersForOrg,
|
|
311
244
|
markResolved: defaultMarkEscalationResolved,
|
|
312
|
-
|
|
245
|
+
decide: NO_ROUTING_DECIDER,
|
|
246
|
+
recordDeciderAttempt: (id, patch) => recordDeciderAttempt(id, patch),
|
|
313
247
|
countSessionsSince: defaultCountFocusSessionsSince,
|
|
314
248
|
now: () => Date.now(),
|
|
315
249
|
thresholdMs: resolveAgentResolvablePromoteMs(),
|
|
316
250
|
minAttempts: DEFAULT_AGENT_RESOLVABLE_PROMOTE_MIN_ATTEMPTS,
|
|
317
|
-
|
|
318
|
-
|
|
251
|
+
maxPerSweep: ROUTING_DECIDER_MAX_PER_SWEEP,
|
|
252
|
+
...defaultRetirementDeps,
|
|
319
253
|
};
|
|
320
254
|
}
|
|
321
255
|
/**
|
|
322
|
-
* Sweep the org's OPEN agent_resolvable filings
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* The refile is org-scoped, so the ONLY hard requirement is an organizationId; a
|
|
329
|
-
* filing that lacks even that cannot be refiled and is skipped (logged). A focus
|
|
330
|
-
* anchor is NOT required: an interactive-session filing carries no focus by
|
|
331
|
-
* construction, yet its needs_operator promotion is still visible (the operator
|
|
332
|
-
* inbox keys on disposition, not on a focus), so an anchorless over-age filing IS
|
|
333
|
-
* promoted -- it is exactly the write-only-sink case this backstop must drain.
|
|
256
|
+
* Sweep the org's OPEN agent_resolvable filings: close the observations a decider
|
|
257
|
+
* already made human-facing, retire stale ones, and hand every eligible observation
|
|
258
|
+
* (see the module ELIGIBILITY) to the routing decider, bounded by maxPerSweep. The
|
|
259
|
+
* sweep itself never files an escalation. Best-effort per filing: a failure is
|
|
260
|
+
* logged and the filing left open for the next sweep, never thrown.
|
|
334
261
|
*/
|
|
335
262
|
export async function sweepAgentResolvablePromotions(deps = {}) {
|
|
336
263
|
const d = { ...defaultSweepDeps(), ...deps };
|
|
337
264
|
const open = await d.listOpenForOrg();
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
265
|
+
const nowMs = (d.now ?? Date.now)();
|
|
266
|
+
const counts = {
|
|
267
|
+
scanned: open.length, retired: 0, sourcesClosed: 0, handed: 0, decided: 0, undecided: 0, deferred: 0,
|
|
268
|
+
};
|
|
269
|
+
const lane = await closeDecidedSources(d, open, counts);
|
|
270
|
+
const ordered = orderHandoffs(await collectHandoffs(d, lane, nowMs, counts));
|
|
271
|
+
const batch = ordered.slice(0, Math.max(0, d.maxPerSweep));
|
|
272
|
+
counts.deferred = ordered.length - batch.length;
|
|
273
|
+
counts.handed = batch.length;
|
|
274
|
+
const outcomes = await Promise.all(batch.map((request) => handOff(d, request)));
|
|
275
|
+
for (const decided of outcomes) {
|
|
276
|
+
if (decided)
|
|
277
|
+
counts.decided++;
|
|
278
|
+
else
|
|
279
|
+
counts.undecided++;
|
|
280
|
+
}
|
|
281
|
+
return counts;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Close every open observation an open human-lane row names as its source
|
|
285
|
+
* (auto_swept -- no actor). Returns the rest of the lane. A failed carrier read
|
|
286
|
+
* closes nothing this sweep; a failed close leaves the source open but still
|
|
287
|
+
* withholds it from a second decider (it has been decided).
|
|
288
|
+
*/
|
|
289
|
+
async function closeDecidedSources(d, open, counts) {
|
|
290
|
+
let carriers;
|
|
291
|
+
try {
|
|
292
|
+
carriers = await d.listLineageCarriers();
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
log.warn(`[escalations] lineage-carrier read failed; no sources closed this sweep: ${err.message}`);
|
|
296
|
+
return [...open];
|
|
297
|
+
}
|
|
298
|
+
const carrierBySource = new Map(carriers.map((c) => [c.sourceEscalationId, c.id]));
|
|
299
|
+
const lane = [];
|
|
300
|
+
for (const item of open) {
|
|
301
|
+
const carrierId = carrierBySource.get(item.id);
|
|
302
|
+
if (carrierId === undefined) {
|
|
303
|
+
lane.push(item);
|
|
345
304
|
continue;
|
|
346
305
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
retired++;
|
|
352
|
-
continue;
|
|
306
|
+
try {
|
|
307
|
+
await d.markResolved(item.id, 'auto_swept', `A routing decider made this observation human-facing as escalation ${carrierId} ` +
|
|
308
|
+
`(metadata.originalEscalationId). Closed here; that row's resolver is what the routing measurement judges.`);
|
|
309
|
+
counts.sourcesClosed++;
|
|
353
310
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
// attempts and is NOT promoted (new anchorless filings are refused at the
|
|
357
|
-
// filing boundary; the aging observer keeps any residue visible). A
|
|
358
|
-
// focus-anchored candidate promotes only once the machine has actually tried
|
|
359
|
-
// and failed it minAttempts times.
|
|
360
|
-
const attempts = item.focusId
|
|
361
|
-
? await d.countSessionsSince(item.focusId, item.createdAt)
|
|
362
|
-
: 0;
|
|
363
|
-
if (!meetsAttemptFloor(attempts, d.minAttempts)) {
|
|
364
|
-
log.info(`[escalations] Org-wide sweep not promoting agent_resolvable ${item.id} ` +
|
|
365
|
-
`(focus ${item.focusId ?? '<none>'}): ${attempts} recorded attempt(s) < floor ${d.minAttempts}`);
|
|
366
|
-
continue;
|
|
311
|
+
catch (err) {
|
|
312
|
+
log.warn(`[escalations] Failed to close decided observation ${item.id} (carrier ${carrierId}): ${err.message}`);
|
|
367
313
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
314
|
+
}
|
|
315
|
+
return lane;
|
|
316
|
+
}
|
|
317
|
+
/** Evaluate every lane filing outside the decider window; returns the eligible hand-offs. */
|
|
318
|
+
async function collectHandoffs(d, lane, nowMs, counts) {
|
|
319
|
+
const stopHeld = stopHeldFocusIds(lane);
|
|
320
|
+
// One retired-focus read per focus for the whole sweep, however many candidates share it.
|
|
321
|
+
const retirement = { ...d, focusRetired: memoizeFocusRetired(d.focusRetired) };
|
|
322
|
+
const windowMs = d.deciderWindowMs ?? d.thresholdMs;
|
|
323
|
+
const requests = [];
|
|
324
|
+
for (const item of lane) {
|
|
325
|
+
if (withinDeciderWindow(item.metadata, nowMs, windowMs))
|
|
326
|
+
continue;
|
|
327
|
+
const verdict = await evaluateCandidate(d, retirement, item, nowMs, stopHeld);
|
|
328
|
+
if (verdict === 'retired')
|
|
329
|
+
counts.retired++;
|
|
330
|
+
else if (verdict !== null)
|
|
331
|
+
requests.push(verdict);
|
|
332
|
+
}
|
|
333
|
+
return requests;
|
|
334
|
+
}
|
|
335
|
+
/** Pure: whether a filing is past the age bound (an unparseable createdAt is not). */
|
|
336
|
+
function isOverAge(createdAt, nowMs, thresholdMs) {
|
|
337
|
+
const created = Date.parse(createdAt);
|
|
338
|
+
return !Number.isNaN(created) && nowMs - created > thresholdMs;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* One lane filing: retire it, hand it off (a request), or leave it (null). See the
|
|
342
|
+
* module ELIGIBILITY. An over-age candidate goes through the full retired-focus guard
|
|
343
|
+
* (which may retire it); a fresh one only READS its focus's state.
|
|
344
|
+
*/
|
|
345
|
+
async function evaluateCandidate(d, retirement, item, nowMs, stopHeld) {
|
|
346
|
+
if (!item.organizationId) {
|
|
347
|
+
log.warn(`[escalations] agent_resolvable ${item.id} lacks an org anchor; not handed to a routing decider`);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
const base = {
|
|
351
|
+
item,
|
|
352
|
+
ageHours: openAgeHours(item.createdAt, nowMs),
|
|
353
|
+
deciderAttempts: readDeciderAttempts(item.metadata).attempts,
|
|
354
|
+
};
|
|
355
|
+
if (!item.focusId)
|
|
356
|
+
return { ...base, basis: 'unreachable', attempts: 0 };
|
|
357
|
+
const scope = { organizationId: item.organizationId, ...(item.productId ? { productId: item.productId } : {}) };
|
|
358
|
+
const overAge = isOverAge(item.createdAt, nowMs, d.thresholdMs);
|
|
359
|
+
const state = overAge
|
|
360
|
+
? await checkFocusRetirement(retirement, item, item.focusId, scope)
|
|
361
|
+
: await peekFocusRetirement(retirement, item, item.focusId, scope);
|
|
362
|
+
if (state.kind === 'retired')
|
|
363
|
+
return 'retired';
|
|
364
|
+
if (state.kind === 'awaiting_retirement')
|
|
365
|
+
return null;
|
|
366
|
+
if (state.kind === 'outlives') {
|
|
367
|
+
return { ...base, basis: 'outlives_focus', attempts: 0, retiredFocusReason: state.reason };
|
|
368
|
+
}
|
|
369
|
+
const withFocus = state.focus ? { focus: state.focus } : {};
|
|
370
|
+
if (stopHeld.has(item.focusId))
|
|
371
|
+
return { ...base, ...withFocus, basis: 'stop_held', attempts: 0 };
|
|
372
|
+
if (state.focus && !state.focus.armed)
|
|
373
|
+
return { ...base, ...withFocus, basis: 'unreachable', attempts: 0 };
|
|
374
|
+
if (!overAge)
|
|
375
|
+
return null;
|
|
376
|
+
const attempts = await d.countSessionsSince(item.focusId, item.createdAt);
|
|
377
|
+
const basis = promotionBasis(attempts, d.minAttempts, item.observation !== undefined);
|
|
378
|
+
if (!basis) {
|
|
379
|
+
log.info(`[escalations] Not handing off agent_resolvable ${item.id} (focus ${item.focusId}): ` +
|
|
380
|
+
`${attempts} recorded attempt(s) < floor ${d.minAttempts}; it stays in the lane`);
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
return { ...base, ...withFocus, basis, attempts };
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Hand one observation to the decider. A decider that RAN records the attempt on the
|
|
387
|
+
* row (so it is not re-handed inside the window); one that did not complete cleanly
|
|
388
|
+
* leaves the observation in the lane -- no deterministic human fallback. Returns
|
|
389
|
+
* whether it decided.
|
|
390
|
+
*/
|
|
391
|
+
async function handOff(d, request) {
|
|
392
|
+
const { item, basis } = request;
|
|
393
|
+
let result;
|
|
394
|
+
try {
|
|
395
|
+
result = await d.decide(request);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
result = { ran: false, decided: false, detail: `decider threw: ${err.message}` };
|
|
399
|
+
}
|
|
400
|
+
if (result.ran) {
|
|
374
401
|
try {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
await d.markResolved(item.id, `Promoted to a needs_operator escalation after ~${ageHours}h open without ` +
|
|
381
|
-
`autonomous resolution, via the org-wide sweep (its focus never spawned a ` +
|
|
382
|
-
`team). agent_resolvable -> needs_operator.`);
|
|
383
|
-
promoted++;
|
|
384
|
-
log.info(`[escalations] Org-wide sweep promoted agent_resolvable ${item.id} to needs_operator ` +
|
|
385
|
-
`(focus ${item.focusId ?? '<none>'}, ~${ageHours}h)`);
|
|
386
|
-
}
|
|
402
|
+
await d.recordDeciderAttempt(item.id, {
|
|
403
|
+
[DECIDER_ATTEMPTS_KEY]: request.deciderAttempts + 1,
|
|
404
|
+
[LAST_DECIDED_AT_KEY]: new Date((d.now ?? Date.now)()).toISOString(),
|
|
405
|
+
[LAST_DECIDER_OUTCOME_KEY]: result.detail,
|
|
406
|
+
});
|
|
387
407
|
}
|
|
388
408
|
catch (err) {
|
|
389
|
-
log.warn(`[escalations]
|
|
390
|
-
`${err.message}`);
|
|
391
|
-
// Left open; the next sweep retries.
|
|
409
|
+
log.warn(`[escalations] Failed to record decider attempt on ${item.id}: ${err.message}`);
|
|
392
410
|
}
|
|
393
411
|
}
|
|
394
|
-
|
|
412
|
+
if (result.decided) {
|
|
413
|
+
log.info(`[escalations] Routing decider ran on agent_resolvable ${item.id} (basis ${basis}): ${result.detail}`);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
log.warn(`[escalations] UNDECIDED agent_resolvable ${item.id} (basis ${basis}, focus ${item.focusId ?? '<none>'}): ` +
|
|
417
|
+
`${result.detail}. It stays in the machine lane; no deterministic human fallback.`);
|
|
418
|
+
}
|
|
419
|
+
return result.decided;
|
|
395
420
|
}
|
|
396
421
|
//# sourceMappingURL=agent-resolvable-promotion.js.map
|