@telora/daemon 0.20.60 → 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.
Files changed (110) hide show
  1. package/build-info.json +4 -3
  2. package/dist/audit-phase.d.ts +12 -18
  3. package/dist/audit-phase.d.ts.map +1 -1
  4. package/dist/audit-phase.js +17 -60
  5. package/dist/audit-phase.js.map +1 -1
  6. package/dist/audit-prompt.d.ts +44 -0
  7. package/dist/audit-prompt.d.ts.map +1 -0
  8. package/dist/audit-prompt.js +194 -0
  9. package/dist/audit-prompt.js.map +1 -0
  10. package/dist/ci-sensor-loop.d.ts +38 -1
  11. package/dist/ci-sensor-loop.d.ts.map +1 -1
  12. package/dist/ci-sensor-loop.js +49 -5
  13. package/dist/ci-sensor-loop.js.map +1 -1
  14. package/dist/delivery-reconciler.d.ts +3 -0
  15. package/dist/delivery-reconciler.d.ts.map +1 -1
  16. package/dist/delivery-reconciler.js +16 -0
  17. package/dist/delivery-reconciler.js.map +1 -1
  18. package/dist/focus-engine.d.ts.map +1 -1
  19. package/dist/focus-engine.js +12 -1
  20. package/dist/focus-engine.js.map +1 -1
  21. package/dist/focus-executor.d.ts.map +1 -1
  22. package/dist/focus-executor.js +4 -1
  23. package/dist/focus-executor.js.map +1 -1
  24. package/dist/focus-merge.d.ts.map +1 -1
  25. package/dist/focus-merge.js +37 -3
  26. package/dist/focus-merge.js.map +1 -1
  27. package/dist/focus-stage-lifecycle/wedge-watchdog.d.ts.map +1 -1
  28. package/dist/focus-stage-lifecycle/wedge-watchdog.js +9 -1
  29. package/dist/focus-stage-lifecycle/wedge-watchdog.js.map +1 -1
  30. package/dist/listener-auto-advance.d.ts.map +1 -1
  31. package/dist/listener-auto-advance.js +58 -10
  32. package/dist/listener-auto-advance.js.map +1 -1
  33. package/dist/merge-back-loop.d.ts +8 -2
  34. package/dist/merge-back-loop.d.ts.map +1 -1
  35. package/dist/merge-back-loop.js +17 -11
  36. package/dist/merge-back-loop.js.map +1 -1
  37. package/dist/merge-failure-classify.d.ts +39 -0
  38. package/dist/merge-failure-classify.d.ts.map +1 -0
  39. package/dist/merge-failure-classify.js +58 -0
  40. package/dist/merge-failure-classify.js.map +1 -0
  41. package/dist/prompt-sections/execution-sections.d.ts +20 -14
  42. package/dist/prompt-sections/execution-sections.d.ts.map +1 -1
  43. package/dist/prompt-sections/execution-sections.js +53 -138
  44. package/dist/prompt-sections/execution-sections.js.map +1 -1
  45. package/dist/prompt-sections/scoping-sections.d.ts +30 -0
  46. package/dist/prompt-sections/scoping-sections.d.ts.map +1 -0
  47. package/dist/prompt-sections/scoping-sections.js +179 -0
  48. package/dist/prompt-sections/scoping-sections.js.map +1 -0
  49. package/dist/prompt-sections/shared.d.ts +7 -0
  50. package/dist/prompt-sections/shared.d.ts.map +1 -1
  51. package/dist/prompt-sections/shared.js.map +1 -1
  52. package/dist/queries/agent-resolvable-promotion.d.ts +87 -0
  53. package/dist/queries/agent-resolvable-promotion.d.ts.map +1 -0
  54. package/dist/queries/agent-resolvable-promotion.js +152 -0
  55. package/dist/queries/agent-resolvable-promotion.js.map +1 -0
  56. package/dist/queries/cause-key.d.ts +64 -0
  57. package/dist/queries/cause-key.d.ts.map +1 -0
  58. package/dist/queries/cause-key.js +79 -0
  59. package/dist/queries/cause-key.js.map +1 -0
  60. package/dist/queries/escalation-suppression.d.ts +89 -0
  61. package/dist/queries/escalation-suppression.d.ts.map +1 -0
  62. package/dist/queries/escalation-suppression.js +98 -0
  63. package/dist/queries/escalation-suppression.js.map +1 -0
  64. package/dist/queries/issues.d.ts +38 -0
  65. package/dist/queries/issues.d.ts.map +1 -1
  66. package/dist/queries/issues.js +53 -0
  67. package/dist/queries/issues.js.map +1 -1
  68. package/dist/queries/verification.d.ts +11 -0
  69. package/dist/queries/verification.d.ts.map +1 -1
  70. package/dist/queries/verification.js.map +1 -1
  71. package/dist/resolvers/shared/git-diff.d.ts +24 -3
  72. package/dist/resolvers/shared/git-diff.d.ts.map +1 -1
  73. package/dist/resolvers/shared/git-diff.js +41 -3
  74. package/dist/resolvers/shared/git-diff.js.map +1 -1
  75. package/dist/spawn/prompt-assembly.d.ts +3 -0
  76. package/dist/spawn/prompt-assembly.d.ts.map +1 -1
  77. package/dist/spawn/prompt-assembly.js +2 -1
  78. package/dist/spawn/prompt-assembly.js.map +1 -1
  79. package/dist/spawn/worktree-session.d.ts +2 -0
  80. package/dist/spawn/worktree-session.d.ts.map +1 -1
  81. package/dist/spawn/worktree-session.js +20 -1
  82. package/dist/spawn/worktree-session.js.map +1 -1
  83. package/dist/staleness-sensor.d.ts +24 -1
  84. package/dist/staleness-sensor.d.ts.map +1 -1
  85. package/dist/staleness-sensor.js +31 -4
  86. package/dist/staleness-sensor.js.map +1 -1
  87. package/dist/stall-detectors.d.ts +13 -9
  88. package/dist/stall-detectors.d.ts.map +1 -1
  89. package/dist/stall-detectors.js +14 -13
  90. package/dist/stall-detectors.js.map +1 -1
  91. package/dist/team-prompt-base.d.ts +1 -1
  92. package/dist/team-prompt-base.d.ts.map +1 -1
  93. package/dist/team-prompt-base.js +4 -2
  94. package/dist/team-prompt-base.js.map +1 -1
  95. package/dist/verification-default-on.d.ts.map +1 -1
  96. package/dist/verification-default-on.js +5 -0
  97. package/dist/verification-default-on.js.map +1 -1
  98. package/dist/verification-engine.d.ts +8 -0
  99. package/dist/verification-engine.d.ts.map +1 -1
  100. package/dist/verification-engine.js +8 -75
  101. package/dist/verification-engine.js.map +1 -1
  102. package/dist/verification-evidence.d.ts +74 -0
  103. package/dist/verification-evidence.d.ts.map +1 -0
  104. package/dist/verification-evidence.js +116 -0
  105. package/dist/verification-evidence.js.map +1 -0
  106. package/dist/verification-feedback.d.ts +8 -7
  107. package/dist/verification-feedback.d.ts.map +1 -1
  108. package/dist/verification-feedback.js +49 -39
  109. package/dist/verification-feedback.js.map +1 -1
  110. 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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"}