@telora/daemon 0.20.57 → 0.20.78
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/audit-phase.d.ts +12 -18
- package/dist/audit-phase.d.ts.map +1 -1
- package/dist/audit-phase.js +17 -60
- package/dist/audit-phase.js.map +1 -1
- package/dist/audit-prompt.d.ts +44 -0
- package/dist/audit-prompt.d.ts.map +1 -0
- package/dist/audit-prompt.js +194 -0
- package/dist/audit-prompt.js.map +1 -0
- package/dist/ci-sensor-loop.d.ts +38 -1
- package/dist/ci-sensor-loop.d.ts.map +1 -1
- package/dist/ci-sensor-loop.js +49 -5
- package/dist/ci-sensor-loop.js.map +1 -1
- package/dist/delivery-reconciler.d.ts +3 -0
- package/dist/delivery-reconciler.d.ts.map +1 -1
- package/dist/delivery-reconciler.js +16 -0
- package/dist/delivery-reconciler.js.map +1 -1
- package/dist/focus-engine.d.ts.map +1 -1
- package/dist/focus-engine.js +12 -1
- package/dist/focus-engine.js.map +1 -1
- package/dist/focus-executor.d.ts.map +1 -1
- package/dist/focus-executor.js +4 -1
- package/dist/focus-executor.js.map +1 -1
- package/dist/focus-merge.d.ts.map +1 -1
- package/dist/focus-merge.js +37 -3
- package/dist/focus-merge.js.map +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts.map +1 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.js +9 -1
- package/dist/focus-stage-lifecycle/wedge-watchdog.js.map +1 -1
- package/dist/listener-auto-advance.d.ts.map +1 -1
- package/dist/listener-auto-advance.js +58 -10
- package/dist/listener-auto-advance.js.map +1 -1
- package/dist/merge-back-loop.d.ts +8 -2
- package/dist/merge-back-loop.d.ts.map +1 -1
- package/dist/merge-back-loop.js +17 -11
- package/dist/merge-back-loop.js.map +1 -1
- package/dist/merge-failure-classify.d.ts +39 -0
- package/dist/merge-failure-classify.d.ts.map +1 -0
- package/dist/merge-failure-classify.js +58 -0
- package/dist/merge-failure-classify.js.map +1 -0
- package/dist/prompt-sections/execution-sections.d.ts +20 -14
- package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
- package/dist/prompt-sections/execution-sections.js +53 -138
- package/dist/prompt-sections/execution-sections.js.map +1 -1
- package/dist/prompt-sections/scoping-sections.d.ts +30 -0
- package/dist/prompt-sections/scoping-sections.d.ts.map +1 -0
- package/dist/prompt-sections/scoping-sections.js +179 -0
- package/dist/prompt-sections/scoping-sections.js.map +1 -0
- package/dist/prompt-sections/shared.d.ts +7 -0
- package/dist/prompt-sections/shared.d.ts.map +1 -1
- package/dist/prompt-sections/shared.js.map +1 -1
- package/dist/queries/agent-resolvable-promotion.d.ts +87 -0
- package/dist/queries/agent-resolvable-promotion.d.ts.map +1 -0
- package/dist/queries/agent-resolvable-promotion.js +152 -0
- package/dist/queries/agent-resolvable-promotion.js.map +1 -0
- package/dist/queries/cause-key.d.ts +64 -0
- package/dist/queries/cause-key.d.ts.map +1 -0
- package/dist/queries/cause-key.js +79 -0
- package/dist/queries/cause-key.js.map +1 -0
- package/dist/queries/escalation-suppression.d.ts +89 -0
- package/dist/queries/escalation-suppression.d.ts.map +1 -0
- package/dist/queries/escalation-suppression.js +98 -0
- package/dist/queries/escalation-suppression.js.map +1 -0
- package/dist/queries/holds.d.ts +177 -0
- package/dist/queries/holds.d.ts.map +1 -0
- package/dist/queries/holds.js +232 -0
- package/dist/queries/holds.js.map +1 -0
- package/dist/queries/issues.d.ts +41 -99
- package/dist/queries/issues.d.ts.map +1 -1
- package/dist/queries/issues.js +57 -137
- package/dist/queries/issues.js.map +1 -1
- package/dist/queries/verification.d.ts +11 -0
- package/dist/queries/verification.d.ts.map +1 -1
- package/dist/queries/verification.js.map +1 -1
- package/dist/resolvers/shared/git-diff.d.ts +24 -3
- package/dist/resolvers/shared/git-diff.d.ts.map +1 -1
- package/dist/resolvers/shared/git-diff.js +41 -3
- package/dist/resolvers/shared/git-diff.js.map +1 -1
- package/dist/spawn/prompt-assembly.d.ts +3 -0
- package/dist/spawn/prompt-assembly.d.ts.map +1 -1
- package/dist/spawn/prompt-assembly.js +2 -1
- package/dist/spawn/prompt-assembly.js.map +1 -1
- package/dist/spawn/worktree-session.d.ts +2 -0
- package/dist/spawn/worktree-session.d.ts.map +1 -1
- package/dist/spawn/worktree-session.js +20 -1
- package/dist/spawn/worktree-session.js.map +1 -1
- package/dist/staleness-sensor.d.ts +24 -1
- package/dist/staleness-sensor.d.ts.map +1 -1
- package/dist/staleness-sensor.js +31 -4
- package/dist/staleness-sensor.js.map +1 -1
- package/dist/stall-detectors.d.ts +46 -13
- package/dist/stall-detectors.d.ts.map +1 -1
- package/dist/stall-detectors.js +78 -18
- package/dist/stall-detectors.js.map +1 -1
- package/dist/team-prompt-base.d.ts +1 -1
- package/dist/team-prompt-base.d.ts.map +1 -1
- package/dist/team-prompt-base.js +4 -2
- package/dist/team-prompt-base.js.map +1 -1
- package/dist/verification-default-on.d.ts.map +1 -1
- package/dist/verification-default-on.js +5 -0
- package/dist/verification-default-on.js.map +1 -1
- package/dist/verification-engine.d.ts +8 -0
- package/dist/verification-engine.d.ts.map +1 -1
- package/dist/verification-engine.js +8 -75
- package/dist/verification-engine.js.map +1 -1
- package/dist/verification-evidence.d.ts +74 -0
- package/dist/verification-evidence.d.ts.map +1 -0
- package/dist/verification-evidence.js +116 -0
- package/dist/verification-evidence.js.map +1 -0
- package/dist/verification-feedback.d.ts +8 -7
- package/dist/verification-feedback.d.ts.map +1 -1
- package/dist/verification-feedback.js +49 -39
- package/dist/verification-feedback.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent_resolvable promotion safety net (delivery "agent_resolvable never
|
|
3
|
+
* reaches the human inbox", TEL-3).
|
|
4
|
+
*
|
|
5
|
+
* agent_resolvable filings are machine-owned: hidden from the operator inbox
|
|
6
|
+
* (TEL-1) and ridden into the focus's next coding spawn to be resolved (TEL-2).
|
|
7
|
+
* That is only safe if a filing the machine CANNOT actually clear eventually
|
|
8
|
+
* surfaces to a human -- otherwise it lingers forever, invisible. This module is
|
|
9
|
+
* that backstop: an agent_resolvable filing that has stayed open past a bound
|
|
10
|
+
* (it rode into many spawn cycles and none resolved it -- "bounded failures") is
|
|
11
|
+
* PROMOTED to a human by resolving the machine-routed row and refiling the same
|
|
12
|
+
* content as a needs_operator escalation, which the operator inbox DOES show.
|
|
13
|
+
*
|
|
14
|
+
* Why age, not a per-row attempt counter: escalation_update accepts only
|
|
15
|
+
* status/notes/answer (no disposition or metadata writes without a separate edge
|
|
16
|
+
* deploy), and there is no per-focus "sessions since T" query. Open-age is the
|
|
17
|
+
* deploy-safe, restart-safe, count-free proxy for "it survived N spawn cycles
|
|
18
|
+
* unresolved" -- every spawn that rode it in and exited without clearing it is a
|
|
19
|
+
* failed attempt, and those accumulate as elapsed open time.
|
|
20
|
+
*
|
|
21
|
+
* Promotion is idempotent: the needs_operator refile rides the shared cause-keyed
|
|
22
|
+
* gate keyed on the original escalation id, and the source row is resolved in the
|
|
23
|
+
* same step, so a filing is promoted at most once.
|
|
24
|
+
*/
|
|
25
|
+
import { type AgentResolvableItem } from './issues.js';
|
|
26
|
+
import { type FileEscalationIdempotentParams, type FileEscalationIdempotentResult } from './escalation-suppression.js';
|
|
27
|
+
/** Default hours an agent_resolvable filing may stay open before promotion. */
|
|
28
|
+
export declare const DEFAULT_AGENT_RESOLVABLE_PROMOTE_HOURS = 24;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the promotion threshold (hours -> ms) from the typed, Zod-validated
|
|
31
|
+
* env-config -- the single declaration site for every operational knob
|
|
32
|
+
* (packages/daemon-core/src/env-config.ts). An unset value yields the schema
|
|
33
|
+
* default (DEFAULT_AGENT_RESOLVABLE_PROMOTE_HOURS); an invalid one fails loudly
|
|
34
|
+
* at startup rather than silently degrading to a default here.
|
|
35
|
+
*
|
|
36
|
+
* Same shape as resolveStuckBranchThresholdMs in merge-back-loop.ts.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveAgentResolvablePromoteMs(env?: NodeJS.ProcessEnv): number;
|
|
39
|
+
/**
|
|
40
|
+
* Pure: split the open agent_resolvable filings into the ones that should still
|
|
41
|
+
* RIDE into this spawn and the ones that are past the age bound and should be
|
|
42
|
+
* PROMOTED to a human. A filing with an unparseable/absent createdAt is treated
|
|
43
|
+
* as fresh (ride) -- promotion must never fire on a missing timestamp.
|
|
44
|
+
*/
|
|
45
|
+
export declare function partitionAgentResolvableByAge(items: readonly AgentResolvableItem[], nowMs: number, thresholdMs: number): {
|
|
46
|
+
ride: AgentResolvableItem[];
|
|
47
|
+
promote: AgentResolvableItem[];
|
|
48
|
+
};
|
|
49
|
+
/** The cause key that makes a promotion refile idempotent (one per original filing). */
|
|
50
|
+
export declare function agentResolvablePromotionCauseKey(originalEscalationId: string): string;
|
|
51
|
+
/** escalation_kind for a promoted (formerly agent_resolvable) filing. */
|
|
52
|
+
export declare const AGENT_RESOLVABLE_PROMOTED_KIND = "agent_resolvable_promoted";
|
|
53
|
+
/**
|
|
54
|
+
* Pure: build the needs_operator refile params for a filing the machine could
|
|
55
|
+
* not resolve autonomously. Carries the original description + kind so the
|
|
56
|
+
* operator sees what was tried and why it now needs them; anchored to the focus.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildAgentResolvablePromotionParams(item: AgentResolvableItem, ctx: {
|
|
59
|
+
organizationId: string;
|
|
60
|
+
focusId: string;
|
|
61
|
+
productId?: string;
|
|
62
|
+
}, ageHours: number): FileEscalationIdempotentParams;
|
|
63
|
+
/** Injected IO for the promotion reconcile (real defaults; tests inject stubs). */
|
|
64
|
+
export interface AgentResolvablePromotionDeps {
|
|
65
|
+
listOpen: (focusId: string) => Promise<AgentResolvableItem[]>;
|
|
66
|
+
markResolved: (id: string, notes?: string) => Promise<void>;
|
|
67
|
+
file: (params: FileEscalationIdempotentParams) => Promise<FileEscalationIdempotentResult>;
|
|
68
|
+
now: () => number;
|
|
69
|
+
thresholdMs: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Reconcile a focus's open agent_resolvable filings and return the ones that
|
|
73
|
+
* should ride into THIS spawn. Over-age filings are promoted (needs_operator
|
|
74
|
+
* refile + resolve the source) so they leave the machine lane and reach the
|
|
75
|
+
* human inbox; the rest are returned to ride in as before.
|
|
76
|
+
*
|
|
77
|
+
* Best-effort per filing: a promotion failure is logged and the filing is left
|
|
78
|
+
* to ride (it will be retried next spawn), never thrown -- reconciling must never
|
|
79
|
+
* block a spawn. This is the single entry point the spawn path calls instead of
|
|
80
|
+
* listOpenAgentResolvableForFocus.
|
|
81
|
+
*/
|
|
82
|
+
export declare function reconcileAgentResolvableForFocus(ctx: {
|
|
83
|
+
organizationId: string;
|
|
84
|
+
focusId: string;
|
|
85
|
+
productId?: string;
|
|
86
|
+
}, deps?: Partial<AgentResolvablePromotionDeps>): Promise<AgentResolvableItem[]>;
|
|
87
|
+
//# sourceMappingURL=agent-resolvable-promotion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-resolvable-promotion.d.ts","sourceRoot":"","sources":["../../src/queries/agent-resolvable-promotion.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAOH,OAAO,EAGL,KAAK,mBAAmB,EACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAEL,KAAK,8BAA8B,EACnC,KAAK,8BAA8B,EACpC,MAAM,6BAA6B,CAAC;AAMrC,+EAA+E;AAC/E,eAAO,MAAM,sCAAsC,KAAK,CAAC;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAE5F;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,SAAS,mBAAmB,EAAE,EACrC,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB;IAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC;IAAC,OAAO,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAYjE;AAED,wFAAwF;AACxF,wBAAgB,gCAAgC,CAAC,oBAAoB,EAAE,MAAM,GAAG,MAAM,CAErF;AAED,yEAAyE;AACzE,eAAO,MAAM,8BAA8B,8BAA8B,CAAC;AAE1E;;;;GAIG;AACH,wBAAgB,mCAAmC,CACjD,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EACpE,QAAQ,EAAE,MAAM,GACf,8BAA8B,CAiChC;AAED,mFAAmF;AACnF,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAAC;IAC9D,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,IAAI,EAAE,CAAC,MAAM,EAAE,8BAA8B,KAAK,OAAO,CAAC,8BAA8B,CAAC,CAAC;IAC1F,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAYD;;;;;;;;;;GAUG;AACH,wBAAsB,gCAAgC,CACpD,GAAG,EAAE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EACpE,IAAI,GAAE,OAAO,CAAC,4BAA4B,CAAM,GAC/C,OAAO,CAAC,mBAAmB,EAAE,CAAC,CA2BhC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent_resolvable promotion safety net (delivery "agent_resolvable never
|
|
3
|
+
* reaches the human inbox", TEL-3).
|
|
4
|
+
*
|
|
5
|
+
* agent_resolvable filings are machine-owned: hidden from the operator inbox
|
|
6
|
+
* (TEL-1) and ridden into the focus's next coding spawn to be resolved (TEL-2).
|
|
7
|
+
* That is only safe if a filing the machine CANNOT actually clear eventually
|
|
8
|
+
* surfaces to a human -- otherwise it lingers forever, invisible. This module is
|
|
9
|
+
* that backstop: an agent_resolvable filing that has stayed open past a bound
|
|
10
|
+
* (it rode into many spawn cycles and none resolved it -- "bounded failures") is
|
|
11
|
+
* PROMOTED to a human by resolving the machine-routed row and refiling the same
|
|
12
|
+
* content as a needs_operator escalation, which the operator inbox DOES show.
|
|
13
|
+
*
|
|
14
|
+
* Why age, not a per-row attempt counter: escalation_update accepts only
|
|
15
|
+
* status/notes/answer (no disposition or metadata writes without a separate edge
|
|
16
|
+
* deploy), and there is no per-focus "sessions since T" query. Open-age is the
|
|
17
|
+
* deploy-safe, restart-safe, count-free proxy for "it survived N spawn cycles
|
|
18
|
+
* unresolved" -- every spawn that rode it in and exited without clearing it is a
|
|
19
|
+
* failed attempt, and those accumulate as elapsed open time.
|
|
20
|
+
*
|
|
21
|
+
* Promotion is idempotent: the needs_operator refile rides the shared cause-keyed
|
|
22
|
+
* gate keyed on the original escalation id, and the source row is resolved in the
|
|
23
|
+
* same step, so a filing is promoted at most once.
|
|
24
|
+
*/
|
|
25
|
+
import { ESCALATION_REASONS, ESCALATION_DISPOSITIONS, loadEnvConfig, } from '@telora/daemon-core';
|
|
26
|
+
import { markEscalationResolved as defaultMarkEscalationResolved, listOpenAgentResolvableForFocus as defaultListOpenAgentResolvableForFocus, } from './issues.js';
|
|
27
|
+
import { fileEscalationIdempotent as defaultFileEscalationIdempotent, } from './escalation-suppression.js';
|
|
28
|
+
import { computeCauseKey } from './cause-key.js';
|
|
29
|
+
import { createLogger } from '../log.js';
|
|
30
|
+
const log = createLogger({ module: 'escalations' });
|
|
31
|
+
/** Default hours an agent_resolvable filing may stay open before promotion. */
|
|
32
|
+
export const DEFAULT_AGENT_RESOLVABLE_PROMOTE_HOURS = 24;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the promotion threshold (hours -> ms) from the typed, Zod-validated
|
|
35
|
+
* env-config -- the single declaration site for every operational knob
|
|
36
|
+
* (packages/daemon-core/src/env-config.ts). An unset value yields the schema
|
|
37
|
+
* default (DEFAULT_AGENT_RESOLVABLE_PROMOTE_HOURS); an invalid one fails loudly
|
|
38
|
+
* at startup rather than silently degrading to a default here.
|
|
39
|
+
*
|
|
40
|
+
* Same shape as resolveStuckBranchThresholdMs in merge-back-loop.ts.
|
|
41
|
+
*/
|
|
42
|
+
export function resolveAgentResolvablePromoteMs(env = process.env) {
|
|
43
|
+
return loadEnvConfig(env).TELORA_AGENT_RESOLVABLE_PROMOTE_HOURS * 3_600_000;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Pure: split the open agent_resolvable filings into the ones that should still
|
|
47
|
+
* RIDE into this spawn and the ones that are past the age bound and should be
|
|
48
|
+
* PROMOTED to a human. A filing with an unparseable/absent createdAt is treated
|
|
49
|
+
* as fresh (ride) -- promotion must never fire on a missing timestamp.
|
|
50
|
+
*/
|
|
51
|
+
export function partitionAgentResolvableByAge(items, nowMs, thresholdMs) {
|
|
52
|
+
const ride = [];
|
|
53
|
+
const promote = [];
|
|
54
|
+
for (const item of items) {
|
|
55
|
+
const createdMs = Date.parse(item.createdAt);
|
|
56
|
+
if (Number.isNaN(createdMs) || nowMs - createdMs <= thresholdMs) {
|
|
57
|
+
ride.push(item);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
promote.push(item);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { ride, promote };
|
|
64
|
+
}
|
|
65
|
+
/** The cause key that makes a promotion refile idempotent (one per original filing). */
|
|
66
|
+
export function agentResolvablePromotionCauseKey(originalEscalationId) {
|
|
67
|
+
return computeCauseKey('agent_resolvable_promoted', { escalationId: originalEscalationId });
|
|
68
|
+
}
|
|
69
|
+
/** escalation_kind for a promoted (formerly agent_resolvable) filing. */
|
|
70
|
+
export const AGENT_RESOLVABLE_PROMOTED_KIND = 'agent_resolvable_promoted';
|
|
71
|
+
/**
|
|
72
|
+
* Pure: build the needs_operator refile params for a filing the machine could
|
|
73
|
+
* not resolve autonomously. Carries the original description + kind so the
|
|
74
|
+
* operator sees what was tried and why it now needs them; anchored to the focus.
|
|
75
|
+
*/
|
|
76
|
+
export function buildAgentResolvablePromotionParams(item, ctx, ageHours) {
|
|
77
|
+
const originalKind = item.escalationKind ?? 'agent_resolvable';
|
|
78
|
+
return {
|
|
79
|
+
causeKey: agentResolvablePromotionCauseKey(item.id),
|
|
80
|
+
organizationId: ctx.organizationId,
|
|
81
|
+
issueId: null,
|
|
82
|
+
reasonType: ESCALATION_REASONS.BLOCKED_BY_EXTERNAL,
|
|
83
|
+
// It was machine-routable, but the machine had ~ageHours of spawn cycles to
|
|
84
|
+
// clear it and did not -- a human must now take it.
|
|
85
|
+
disposition: ESCALATION_DISPOSITIONS.NEEDS_OPERATOR,
|
|
86
|
+
anchor: { focusId: ctx.focusId },
|
|
87
|
+
...(ctx.productId ? { productId: ctx.productId } : {}),
|
|
88
|
+
escalationKind: AGENT_RESOLVABLE_PROMOTED_KIND,
|
|
89
|
+
description: `A machine-routable (agent_resolvable) filing has stayed open ~${ageHours}h ` +
|
|
90
|
+
`without the daemon's spawns clearing it, so it is promoted to you.\n\n` +
|
|
91
|
+
`**Original kind:** ${originalKind}\n` +
|
|
92
|
+
`**Original filing:** ${item.id}\n\n` +
|
|
93
|
+
`${item.description}`,
|
|
94
|
+
whatWasTried: `The filing rode into the focus's coding spawns for ~${ageHours}h ` +
|
|
95
|
+
`(disposition agent_resolvable) but was never resolved, so the machine has ` +
|
|
96
|
+
`exhausted its autonomous attempts.`,
|
|
97
|
+
helpNeeded: `Resolve the underlying condition manually, or re-file with clearer intent if ` +
|
|
98
|
+
`the machine cannot act on it.`,
|
|
99
|
+
metadata: {
|
|
100
|
+
cause: AGENT_RESOLVABLE_PROMOTED_KIND,
|
|
101
|
+
originalEscalationId: item.id,
|
|
102
|
+
originalKind,
|
|
103
|
+
ageHours,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function defaultDeps() {
|
|
108
|
+
return {
|
|
109
|
+
listOpen: defaultListOpenAgentResolvableForFocus,
|
|
110
|
+
markResolved: defaultMarkEscalationResolved,
|
|
111
|
+
file: defaultFileEscalationIdempotent,
|
|
112
|
+
now: () => Date.now(),
|
|
113
|
+
thresholdMs: resolveAgentResolvablePromoteMs(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Reconcile a focus's open agent_resolvable filings and return the ones that
|
|
118
|
+
* should ride into THIS spawn. Over-age filings are promoted (needs_operator
|
|
119
|
+
* refile + resolve the source) so they leave the machine lane and reach the
|
|
120
|
+
* human inbox; the rest are returned to ride in as before.
|
|
121
|
+
*
|
|
122
|
+
* Best-effort per filing: a promotion failure is logged and the filing is left
|
|
123
|
+
* to ride (it will be retried next spawn), never thrown -- reconciling must never
|
|
124
|
+
* block a spawn. This is the single entry point the spawn path calls instead of
|
|
125
|
+
* listOpenAgentResolvableForFocus.
|
|
126
|
+
*/
|
|
127
|
+
export async function reconcileAgentResolvableForFocus(ctx, deps = {}) {
|
|
128
|
+
const d = { ...defaultDeps(), ...deps };
|
|
129
|
+
const open = await d.listOpen(ctx.focusId);
|
|
130
|
+
const { ride, promote } = partitionAgentResolvableByAge(open, d.now(), d.thresholdMs);
|
|
131
|
+
for (const item of promote) {
|
|
132
|
+
const ageHours = Math.max(1, Math.floor((d.now() - Date.parse(item.createdAt)) / 3_600_000));
|
|
133
|
+
try {
|
|
134
|
+
const result = await d.file(buildAgentResolvablePromotionParams(item, ctx, ageHours));
|
|
135
|
+
// Only retire the machine-routed row once the human-facing refile is in
|
|
136
|
+
// place (filed OR already suppressed by a prior promotion) -- never resolve
|
|
137
|
+
// the source without a landing spot, which would drop the signal entirely.
|
|
138
|
+
if (result.filed || result.suppressed) {
|
|
139
|
+
await d.markResolved(item.id, `Promoted to a needs_operator escalation after ~${ageHours}h open without ` +
|
|
140
|
+
`autonomous resolution (agent_resolvable -> needs_operator).`);
|
|
141
|
+
log.info(`[escalations] Promoted agent_resolvable ${item.id} to needs_operator (focus ${ctx.focusId}, ~${ageHours}h)`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
log.warn(`[escalations] Failed to promote agent_resolvable ${item.id} for focus ${ctx.focusId}: ${err.message}`);
|
|
146
|
+
// Leave it to ride this spawn; next reconcile retries the promotion.
|
|
147
|
+
ride.push(item);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return ride;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=agent-resolvable-promotion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-resolvable-promotion.js","sourceRoot":"","sources":["../../src/queries/agent-resolvable-promotion.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic escalation cause keys (PURE, no I/O).
|
|
3
|
+
*
|
|
4
|
+
* An escalation's CAUSE KEY identifies the underlying cause a detector observed,
|
|
5
|
+
* derived from the escalation's kind plus its identifying SUBJECT. Two filings
|
|
6
|
+
* with the same cause key are about the same underlying fact; the idempotent
|
|
7
|
+
* filing gate (queries/escalation-suppression.ts) uses the key so one cause
|
|
8
|
+
* appears in the inbox at most once while unresolved, and a dismissed cause stays
|
|
9
|
+
* silent until its subject state actually changes.
|
|
10
|
+
*
|
|
11
|
+
* The key encodes the subject, so "the subject state changed" is expressed
|
|
12
|
+
* simply: a new subject (a new sha going red) yields a DIFFERENT key and re-files
|
|
13
|
+
* naturally, while the same subject (the same sha re-observed) yields the SAME
|
|
14
|
+
* key and is suppressed. This is what makes dismissal-memory fall out of a plain
|
|
15
|
+
* string comparison instead of a bespoke diff.
|
|
16
|
+
*
|
|
17
|
+
* Recipes (from the focus "An escalation is a verdict, not a log line"):
|
|
18
|
+
* - ci_run_red -> sha + branch
|
|
19
|
+
* - verify_gate_unclearable / verify_evidence_unavailable -> deliveryId + guard
|
|
20
|
+
* - stuck_focus_branch / wedged_focus (stuck/wedged detectors) -> focusId + blockage kind
|
|
21
|
+
* - team_failure (team-exit filings) -> focusId + session-exit marker
|
|
22
|
+
*/
|
|
23
|
+
/** A subject is a flat bag of identifying strings (nullable/undefined dropped). */
|
|
24
|
+
export type CauseKeySubject = Record<string, string | null | undefined>;
|
|
25
|
+
/**
|
|
26
|
+
* Compute a deterministic cause key from a kind + subject bag.
|
|
27
|
+
*
|
|
28
|
+
* Deterministic and order-insensitive: the subject entries are sorted by key, so
|
|
29
|
+
* `{sha, branch}` and `{branch, sha}` yield the same key. Null/undefined values
|
|
30
|
+
* are dropped (an absent subject field never changes the key vs. omitting it).
|
|
31
|
+
* Same (kind, subject) -> same key; any changed subject value -> different key.
|
|
32
|
+
*/
|
|
33
|
+
export declare function computeCauseKey(kind: string, subject: CauseKeySubject): string;
|
|
34
|
+
/** ci_run_red: keyed on the sha + branch the red CI run belongs to. */
|
|
35
|
+
export declare function ciRunRedCauseKey(sha: string, branch: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* verify_gate_unclearable / verify_evidence_unavailable and any other
|
|
38
|
+
* delivery+guard detector: keyed on the delivery and (optionally) the blocking
|
|
39
|
+
* guard. Omit `guard` for a strictly PER-DELIVERY key: both stall detectors file
|
|
40
|
+
* at most one open row per delivery regardless of which guard is blocking, so
|
|
41
|
+
* they key on the delivery alone (a per-guard key would let a guard-name change
|
|
42
|
+
* re-file). Detectors that genuinely differ by guard pass it through.
|
|
43
|
+
*/
|
|
44
|
+
export declare function deliveryGuardCauseKey(kind: string, deliveryId: string, guard?: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* stuck/wedged focus detectors (stuck_focus_branch, wedged_focus, ...): keyed on
|
|
47
|
+
* the focus and the kind of blockage observed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function focusBlockageCauseKey(kind: string, focusId: string, blockage: string): string;
|
|
50
|
+
/**
|
|
51
|
+
* team-exit filings (team_failure): keyed on the focus and a session-exit marker
|
|
52
|
+
* so one blocked team exit collapses to a single cause key regardless of how many
|
|
53
|
+
* partial filings a team attempts (the burst pattern).
|
|
54
|
+
*/
|
|
55
|
+
export declare function teamExitCauseKey(focusId: string, sessionExit: string): string;
|
|
56
|
+
/**
|
|
57
|
+
* daemon-staleness handover (propagation_stale): keyed on the specific version
|
|
58
|
+
* gap (running + newest). A per-daemon singleton signal -- one running version --
|
|
59
|
+
* so it needs no product/focus in the key. A dismissal silences THIS exact gap;
|
|
60
|
+
* when `newest` advances (a different gap) it re-files, and a catch-up simply
|
|
61
|
+
* stops the signal (auto-resolved).
|
|
62
|
+
*/
|
|
63
|
+
export declare function daemonStalenessCauseKey(running: string, newest: string): string;
|
|
64
|
+
//# sourceMappingURL=cause-key.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cause-key.d.ts","sourceRoot":"","sources":["../../src/queries/cause-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,mFAAmF;AACnF,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;AAExE;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,CAM9E;AAID,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9F;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE/E"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic escalation cause keys (PURE, no I/O).
|
|
3
|
+
*
|
|
4
|
+
* An escalation's CAUSE KEY identifies the underlying cause a detector observed,
|
|
5
|
+
* derived from the escalation's kind plus its identifying SUBJECT. Two filings
|
|
6
|
+
* with the same cause key are about the same underlying fact; the idempotent
|
|
7
|
+
* filing gate (queries/escalation-suppression.ts) uses the key so one cause
|
|
8
|
+
* appears in the inbox at most once while unresolved, and a dismissed cause stays
|
|
9
|
+
* silent until its subject state actually changes.
|
|
10
|
+
*
|
|
11
|
+
* The key encodes the subject, so "the subject state changed" is expressed
|
|
12
|
+
* simply: a new subject (a new sha going red) yields a DIFFERENT key and re-files
|
|
13
|
+
* naturally, while the same subject (the same sha re-observed) yields the SAME
|
|
14
|
+
* key and is suppressed. This is what makes dismissal-memory fall out of a plain
|
|
15
|
+
* string comparison instead of a bespoke diff.
|
|
16
|
+
*
|
|
17
|
+
* Recipes (from the focus "An escalation is a verdict, not a log line"):
|
|
18
|
+
* - ci_run_red -> sha + branch
|
|
19
|
+
* - verify_gate_unclearable / verify_evidence_unavailable -> deliveryId + guard
|
|
20
|
+
* - stuck_focus_branch / wedged_focus (stuck/wedged detectors) -> focusId + blockage kind
|
|
21
|
+
* - team_failure (team-exit filings) -> focusId + session-exit marker
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Compute a deterministic cause key from a kind + subject bag.
|
|
25
|
+
*
|
|
26
|
+
* Deterministic and order-insensitive: the subject entries are sorted by key, so
|
|
27
|
+
* `{sha, branch}` and `{branch, sha}` yield the same key. Null/undefined values
|
|
28
|
+
* are dropped (an absent subject field never changes the key vs. omitting it).
|
|
29
|
+
* Same (kind, subject) -> same key; any changed subject value -> different key.
|
|
30
|
+
*/
|
|
31
|
+
export function computeCauseKey(kind, subject) {
|
|
32
|
+
const parts = Object.entries(subject)
|
|
33
|
+
.filter(([, v]) => v !== null && v !== undefined && v !== '')
|
|
34
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
35
|
+
.sort();
|
|
36
|
+
return `${kind}::${parts.join('|')}`;
|
|
37
|
+
}
|
|
38
|
+
// ── Named recipes (one per detector family) ──────────────────────────
|
|
39
|
+
/** ci_run_red: keyed on the sha + branch the red CI run belongs to. */
|
|
40
|
+
export function ciRunRedCauseKey(sha, branch) {
|
|
41
|
+
return computeCauseKey('ci_run_red', { sha, branch });
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* verify_gate_unclearable / verify_evidence_unavailable and any other
|
|
45
|
+
* delivery+guard detector: keyed on the delivery and (optionally) the blocking
|
|
46
|
+
* guard. Omit `guard` for a strictly PER-DELIVERY key: both stall detectors file
|
|
47
|
+
* at most one open row per delivery regardless of which guard is blocking, so
|
|
48
|
+
* they key on the delivery alone (a per-guard key would let a guard-name change
|
|
49
|
+
* re-file). Detectors that genuinely differ by guard pass it through.
|
|
50
|
+
*/
|
|
51
|
+
export function deliveryGuardCauseKey(kind, deliveryId, guard) {
|
|
52
|
+
return computeCauseKey(kind, { deliveryId, guard });
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* stuck/wedged focus detectors (stuck_focus_branch, wedged_focus, ...): keyed on
|
|
56
|
+
* the focus and the kind of blockage observed.
|
|
57
|
+
*/
|
|
58
|
+
export function focusBlockageCauseKey(kind, focusId, blockage) {
|
|
59
|
+
return computeCauseKey(kind, { focusId, blockage });
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* team-exit filings (team_failure): keyed on the focus and a session-exit marker
|
|
63
|
+
* so one blocked team exit collapses to a single cause key regardless of how many
|
|
64
|
+
* partial filings a team attempts (the burst pattern).
|
|
65
|
+
*/
|
|
66
|
+
export function teamExitCauseKey(focusId, sessionExit) {
|
|
67
|
+
return computeCauseKey('team_failure', { focusId, sessionExit });
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* daemon-staleness handover (propagation_stale): keyed on the specific version
|
|
71
|
+
* gap (running + newest). A per-daemon singleton signal -- one running version --
|
|
72
|
+
* so it needs no product/focus in the key. A dismissal silences THIS exact gap;
|
|
73
|
+
* when `newest` advances (a different gap) it re-files, and a catch-up simply
|
|
74
|
+
* stops the signal (auto-resolved).
|
|
75
|
+
*/
|
|
76
|
+
export function daemonStalenessCauseKey(running, newest) {
|
|
77
|
+
return computeCauseKey('propagation_stale', { running, newest });
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=cause-key.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cause-key.js","sourceRoot":"","sources":["../../src/queries/cause-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAKH;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,OAAwB;IACpE,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;SAClC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,EAAE,CAAC;SAC5D,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;SAC5B,IAAI,EAAE,CAAC;IACV,OAAO,GAAG,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;AACvC,CAAC;AAED,wEAAwE;AAExE,uEAAuE;AACvE,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,MAAc;IAC1D,OAAO,eAAe,CAAC,YAAY,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,UAAkB,EAAE,KAAc;IACpF,OAAO,eAAe,CAAC,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;AACtD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,OAAe,EAAE,QAAgB;IACnF,OAAO,eAAe,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,WAAmB;IACnE,OAAO,eAAe,CAAC,cAAc,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAe,EAAE,MAAc;IACrE,OAAO,eAAe,CAAC,mBAAmB,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;AACnE,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cause-keyed escalation suppression + idempotent filing.
|
|
3
|
+
*
|
|
4
|
+
* Generalizes the narrow per-delivery precedent (hasOpenEscalationForDelivery in
|
|
5
|
+
* queries/issues.ts) to EVERY daemon detector via a deterministic cause key
|
|
6
|
+
* (queries/cause-key.ts). Two properties, both DB-backed so they survive a daemon
|
|
7
|
+
* restart:
|
|
8
|
+
*
|
|
9
|
+
* 1. Idempotent while open. While a filing carrying this cause key is OPEN
|
|
10
|
+
* (pending or in_review), a detector re-observing the same cause does not
|
|
11
|
+
* file again -- across ticks, restarts, and detector kinds.
|
|
12
|
+
*
|
|
13
|
+
* 2. Dismissal memory. A DISMISSED filing carrying this cause key keeps the
|
|
14
|
+
* cause silent: the same cause cannot re-file until its subject state
|
|
15
|
+
* changes. Because the subject is encoded IN the cause key, a changed subject
|
|
16
|
+
* (a NEW sha going red) yields a DIFFERENT key with no matching dismissed row
|
|
17
|
+
* and re-files naturally; the SAME sha re-observed yields the SAME key and is
|
|
18
|
+
* suppressed no matter how many scans re-observe it.
|
|
19
|
+
*
|
|
20
|
+
* A RESOLVED filing does NOT suppress: resolve means the cause was addressed, so a
|
|
21
|
+
* recurrence should re-file. This is achieved simply by excluding 'resolved' from
|
|
22
|
+
* the suppression status set below.
|
|
23
|
+
*
|
|
24
|
+
* The cause key is stamped into agent_escalations.metadata.cause_key at file time.
|
|
25
|
+
* metadata JSONB is already persisted by escalation_create and returned by
|
|
26
|
+
* escalation_list, so this stays entirely daemon-side -- no migration or edge
|
|
27
|
+
* change is required (the AC's sanctioned "the cause key can live in metadata plus
|
|
28
|
+
* a suppression check at file time" path).
|
|
29
|
+
*/
|
|
30
|
+
import { createEscalation, type CreateEscalationParams } from './issues.js';
|
|
31
|
+
/** Minimal escalation row shape the cause-key suppression check reads. */
|
|
32
|
+
interface CauseKeyEscalationRow {
|
|
33
|
+
id: string;
|
|
34
|
+
status?: string | null;
|
|
35
|
+
metadata?: Record<string, unknown> | null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Dependency seam. The real implementations query the Telora API / file via
|
|
39
|
+
* createEscalation; tests inject stubs to avoid the fragile bare-specifier
|
|
40
|
+
* `mock.module` path under tsx (same rationale as the stall-detectors /
|
|
41
|
+
* verification-feedback deps seams).
|
|
42
|
+
*/
|
|
43
|
+
export interface EscalationSuppressionDeps {
|
|
44
|
+
/** Fetch the open+dismissed escalation rows to scan for a matching cause key. */
|
|
45
|
+
listSuppressingEscalations?: (opts: {
|
|
46
|
+
focusId?: string;
|
|
47
|
+
}) => Promise<CauseKeyEscalationRow[]>;
|
|
48
|
+
/** File a fresh escalation (defaults to the real createEscalation). */
|
|
49
|
+
createEscalation?: typeof createEscalation;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* True when an OPEN (pending/in_review) or DISMISSED escalation already carries
|
|
53
|
+
* this cause key -- i.e. a new filing for the same cause must be suppressed.
|
|
54
|
+
*
|
|
55
|
+
* Narrows server-side by open+dismissed statuses (and by focusId when supplied,
|
|
56
|
+
* honored by the live escalation_list handler) so the 500-row page is scoped to
|
|
57
|
+
* the relevant rows rather than the org's newest 500. The cause-key match itself
|
|
58
|
+
* is client-side because cause_key lives in metadata JSONB, not a column.
|
|
59
|
+
*/
|
|
60
|
+
export declare function hasSuppressedCauseKey(causeKey: string, opts?: {
|
|
61
|
+
focusId?: string;
|
|
62
|
+
}, deps?: EscalationSuppressionDeps): Promise<boolean>;
|
|
63
|
+
/** Params for an idempotent, cause-keyed filing. */
|
|
64
|
+
export interface FileEscalationIdempotentParams extends CreateEscalationParams {
|
|
65
|
+
/** The deterministic cause key (queries/cause-key.ts). Stamped into metadata.cause_key. */
|
|
66
|
+
causeKey: string;
|
|
67
|
+
}
|
|
68
|
+
/** Outcome of an idempotent filing attempt. */
|
|
69
|
+
export interface FileEscalationIdempotentResult {
|
|
70
|
+
/** Whether a new escalation row was created this call. */
|
|
71
|
+
filed: boolean;
|
|
72
|
+
/** Whether the call was suppressed by an existing open/dismissed cause-key row. */
|
|
73
|
+
suppressed: boolean;
|
|
74
|
+
/** The new escalation id when filed; null when suppressed or the create returned no id. */
|
|
75
|
+
escalationId: string | null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* File an escalation ONLY if its cause key is not already suppressed (open or
|
|
79
|
+
* dismissed). The cause key is stamped into metadata.cause_key so a later
|
|
80
|
+
* re-observation of the same cause is suppressed by hasSuppressedCauseKey.
|
|
81
|
+
*
|
|
82
|
+
* This is the single filing entry point every daemon detector should route
|
|
83
|
+
* through. It replaces per-detector ad-hoc dedup (hasOpenEscalationForDelivery,
|
|
84
|
+
* hasOpenWorktreeDirtyEscalation, hasOpenWedgedFocusEscalation, in-memory Sets)
|
|
85
|
+
* with one cause-keyed gate that also carries dismissal memory.
|
|
86
|
+
*/
|
|
87
|
+
export declare function fileEscalationIdempotent(params: FileEscalationIdempotentParams, deps?: EscalationSuppressionDeps): Promise<FileEscalationIdempotentResult>;
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=escalation-suppression.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escalation-suppression.d.ts","sourceRoot":"","sources":["../../src/queries/escalation-suppression.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EAAE,gBAAgB,EAAE,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAY5E,0EAA0E;AAC1E,UAAU,qBAAqB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC3C;AAED;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACxC,iFAAiF;IACjF,0BAA0B,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAAC;IAC9F,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,OAAO,gBAAgB,CAAC;CAC5C;AAsBD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,MAAM,EAChB,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,EAC/B,IAAI,GAAE,yBAA8B,GACnC,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED,oDAAoD;AACpD,MAAM,WAAW,8BAA+B,SAAQ,sBAAsB;IAC5E,2FAA2F;IAC3F,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,+CAA+C;AAC/C,MAAM,WAAW,8BAA8B;IAC7C,0DAA0D;IAC1D,KAAK,EAAE,OAAO,CAAC;IACf,mFAAmF;IACnF,UAAU,EAAE,OAAO,CAAC;IACpB,2FAA2F;IAC3F,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,8BAA8B,EACtC,IAAI,GAAE,yBAA8B,GACnC,OAAO,CAAC,8BAA8B,CAAC,CAmBzC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cause-keyed escalation suppression + idempotent filing.
|
|
3
|
+
*
|
|
4
|
+
* Generalizes the narrow per-delivery precedent (hasOpenEscalationForDelivery in
|
|
5
|
+
* queries/issues.ts) to EVERY daemon detector via a deterministic cause key
|
|
6
|
+
* (queries/cause-key.ts). Two properties, both DB-backed so they survive a daemon
|
|
7
|
+
* restart:
|
|
8
|
+
*
|
|
9
|
+
* 1. Idempotent while open. While a filing carrying this cause key is OPEN
|
|
10
|
+
* (pending or in_review), a detector re-observing the same cause does not
|
|
11
|
+
* file again -- across ticks, restarts, and detector kinds.
|
|
12
|
+
*
|
|
13
|
+
* 2. Dismissal memory. A DISMISSED filing carrying this cause key keeps the
|
|
14
|
+
* cause silent: the same cause cannot re-file until its subject state
|
|
15
|
+
* changes. Because the subject is encoded IN the cause key, a changed subject
|
|
16
|
+
* (a NEW sha going red) yields a DIFFERENT key with no matching dismissed row
|
|
17
|
+
* and re-files naturally; the SAME sha re-observed yields the SAME key and is
|
|
18
|
+
* suppressed no matter how many scans re-observe it.
|
|
19
|
+
*
|
|
20
|
+
* A RESOLVED filing does NOT suppress: resolve means the cause was addressed, so a
|
|
21
|
+
* recurrence should re-file. This is achieved simply by excluding 'resolved' from
|
|
22
|
+
* the suppression status set below.
|
|
23
|
+
*
|
|
24
|
+
* The cause key is stamped into agent_escalations.metadata.cause_key at file time.
|
|
25
|
+
* metadata JSONB is already persisted by escalation_create and returned by
|
|
26
|
+
* escalation_list, so this stays entirely daemon-side -- no migration or edge
|
|
27
|
+
* change is required (the AC's sanctioned "the cause key can live in metadata plus
|
|
28
|
+
* a suppression check at file time" path).
|
|
29
|
+
*/
|
|
30
|
+
import { callApi } from './shared.js';
|
|
31
|
+
import { createEscalation } from './issues.js';
|
|
32
|
+
import { createLogger } from '../log.js';
|
|
33
|
+
const log = createLogger({ module: 'escalations' });
|
|
34
|
+
/**
|
|
35
|
+
* Statuses that SUPPRESS a re-file for a cause key: OPEN (pending/in_review)
|
|
36
|
+
* gives idempotency-while-open; DISMISSED gives dismissal-memory. 'resolved' is
|
|
37
|
+
* deliberately ABSENT -- a resolved cause may recur and should re-file.
|
|
38
|
+
*/
|
|
39
|
+
const SUPPRESSING_STATUSES = ['pending', 'in_review', 'dismissed'];
|
|
40
|
+
async function defaultListSuppressingEscalations(opts) {
|
|
41
|
+
const result = await callApi('escalation_list', {
|
|
42
|
+
fields: {
|
|
43
|
+
statuses: SUPPRESSING_STATUSES,
|
|
44
|
+
...(opts.focusId ? { focusId: opts.focusId } : {}),
|
|
45
|
+
},
|
|
46
|
+
limit: 500,
|
|
47
|
+
});
|
|
48
|
+
return result.escalations ?? [];
|
|
49
|
+
}
|
|
50
|
+
/** Read metadata.cause_key off a row, tolerating the loose JSONB shape. */
|
|
51
|
+
function rowCauseKey(row) {
|
|
52
|
+
const meta = row.metadata ?? null;
|
|
53
|
+
const key = meta?.cause_key;
|
|
54
|
+
return typeof key === 'string' ? key : null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* True when an OPEN (pending/in_review) or DISMISSED escalation already carries
|
|
58
|
+
* this cause key -- i.e. a new filing for the same cause must be suppressed.
|
|
59
|
+
*
|
|
60
|
+
* Narrows server-side by open+dismissed statuses (and by focusId when supplied,
|
|
61
|
+
* honored by the live escalation_list handler) so the 500-row page is scoped to
|
|
62
|
+
* the relevant rows rather than the org's newest 500. The cause-key match itself
|
|
63
|
+
* is client-side because cause_key lives in metadata JSONB, not a column.
|
|
64
|
+
*/
|
|
65
|
+
export async function hasSuppressedCauseKey(causeKey, opts = {}, deps = {}) {
|
|
66
|
+
const list = deps.listSuppressingEscalations ?? defaultListSuppressingEscalations;
|
|
67
|
+
const rows = await list({ focusId: opts.focusId });
|
|
68
|
+
return rows.some((e) => {
|
|
69
|
+
if (e.status && !SUPPRESSING_STATUSES.includes(e.status))
|
|
70
|
+
return false;
|
|
71
|
+
return rowCauseKey(e) === causeKey;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* File an escalation ONLY if its cause key is not already suppressed (open or
|
|
76
|
+
* dismissed). The cause key is stamped into metadata.cause_key so a later
|
|
77
|
+
* re-observation of the same cause is suppressed by hasSuppressedCauseKey.
|
|
78
|
+
*
|
|
79
|
+
* This is the single filing entry point every daemon detector should route
|
|
80
|
+
* through. It replaces per-detector ad-hoc dedup (hasOpenEscalationForDelivery,
|
|
81
|
+
* hasOpenWorktreeDirtyEscalation, hasOpenWedgedFocusEscalation, in-memory Sets)
|
|
82
|
+
* with one cause-keyed gate that also carries dismissal memory.
|
|
83
|
+
*/
|
|
84
|
+
export async function fileEscalationIdempotent(params, deps = {}) {
|
|
85
|
+
const { causeKey, ...createParams } = params;
|
|
86
|
+
const _createEscalation = deps.createEscalation ?? createEscalation;
|
|
87
|
+
const suppressed = await hasSuppressedCauseKey(causeKey, { focusId: createParams.focusId ?? createParams.anchor?.focusId }, deps);
|
|
88
|
+
if (suppressed) {
|
|
89
|
+
log.info(`[escalations] Suppressed duplicate filing for cause key "${causeKey}" (open or dismissed row exists)`);
|
|
90
|
+
return { filed: false, suppressed: true, escalationId: null };
|
|
91
|
+
}
|
|
92
|
+
const { escalationId } = await _createEscalation({
|
|
93
|
+
...createParams,
|
|
94
|
+
metadata: { ...(createParams.metadata ?? {}), cause_key: causeKey },
|
|
95
|
+
});
|
|
96
|
+
return { filed: true, suppressed: false, escalationId };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=escalation-suppression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escalation-suppression.js","sourceRoot":"","sources":["../../src/queries/escalation-suppression.ts"],"names":[],"mappings":"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"}
|