@generacy-ai/orchestrator 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +15 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/schema.d.ts +30 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +13 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/health.d.ts +7 -0
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +5 -0
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/internal-relay-events.d.ts +2 -2
- package/dist/routes/internal-relay-events.d.ts.map +1 -1
- package/dist/routes/internal-relay-events.js +12 -0
- package/dist/routes/internal-relay-events.js.map +1 -1
- package/dist/routes/retained-tunnel-event.d.ts +20 -0
- package/dist/routes/retained-tunnel-event.d.ts.map +1 -0
- package/dist/routes/retained-tunnel-event.js +67 -0
- package/dist/routes/retained-tunnel-event.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +197 -34
- package/dist/server.js.map +1 -1
- package/dist/services/adaptive-poll-controller.d.ts +75 -0
- package/dist/services/adaptive-poll-controller.d.ts.map +1 -0
- package/dist/services/adaptive-poll-controller.js +96 -0
- package/dist/services/adaptive-poll-controller.js.map +1 -0
- package/dist/services/clarification-answer-monitor-service.d.ts +93 -0
- package/dist/services/clarification-answer-monitor-service.d.ts.map +1 -0
- package/dist/services/clarification-answer-monitor-service.js +452 -0
- package/dist/services/clarification-answer-monitor-service.js.map +1 -0
- package/dist/services/failure-fingerprint-tracker.d.ts +28 -0
- package/dist/services/failure-fingerprint-tracker.d.ts.map +1 -0
- package/dist/services/failure-fingerprint-tracker.js +38 -0
- package/dist/services/failure-fingerprint-tracker.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +3 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/label-monitor-service.d.ts +21 -2
- package/dist/services/label-monitor-service.d.ts.map +1 -1
- package/dist/services/label-monitor-service.js +47 -16
- package/dist/services/label-monitor-service.js.map +1 -1
- package/dist/services/merge-conflict-monitor-service.d.ts +14 -1
- package/dist/services/merge-conflict-monitor-service.d.ts.map +1 -1
- package/dist/services/merge-conflict-monitor-service.js +46 -13
- package/dist/services/merge-conflict-monitor-service.js.map +1 -1
- package/dist/services/pr-feedback-monitor-service.d.ts +14 -1
- package/dist/services/pr-feedback-monitor-service.d.ts.map +1 -1
- package/dist/services/pr-feedback-monitor-service.js +46 -16
- package/dist/services/pr-feedback-monitor-service.js.map +1 -1
- package/dist/services/relay-bridge.d.ts +1 -0
- package/dist/services/relay-bridge.d.ts.map +1 -1
- package/dist/services/relay-bridge.js +21 -0
- package/dist/services/relay-bridge.js.map +1 -1
- package/dist/services/smee-channel-resolver.d.ts +60 -0
- package/dist/services/smee-channel-resolver.d.ts.map +1 -0
- package/dist/services/smee-channel-resolver.js +177 -0
- package/dist/services/smee-channel-resolver.js.map +1 -0
- package/dist/services/smee-receiver.d.ts +39 -21
- package/dist/services/smee-receiver.d.ts.map +1 -1
- package/dist/services/smee-receiver.js +134 -28
- package/dist/services/smee-receiver.js.map +1 -1
- package/dist/services/webhook-setup-service.d.ts +115 -111
- package/dist/services/webhook-setup-service.d.ts.map +1 -1
- package/dist/services/webhook-setup-service.js +377 -209
- package/dist/services/webhook-setup-service.js.map +1 -1
- package/dist/types/api.d.ts +3 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/api.js +1 -0
- package/dist/types/api.js.map +1 -1
- package/dist/types/monitor.d.ts +18 -2
- package/dist/types/monitor.d.ts.map +1 -1
- package/dist/worker/clarification-markers.d.ts +72 -0
- package/dist/worker/clarification-markers.d.ts.map +1 -1
- package/dist/worker/clarification-markers.js +111 -0
- package/dist/worker/clarification-markers.js.map +1 -1
- package/dist/worker/clarification-poster.d.ts +102 -6
- package/dist/worker/clarification-poster.d.ts.map +1 -1
- package/dist/worker/clarification-poster.js +299 -89
- package/dist/worker/clarification-poster.js.map +1 -1
- package/dist/worker/claude-cli-worker.d.ts +7 -0
- package/dist/worker/claude-cli-worker.d.ts.map +1 -1
- package/dist/worker/claude-cli-worker.js +3 -0
- package/dist/worker/claude-cli-worker.js.map +1 -1
- package/dist/worker/failure-fingerprint.d.ts +41 -0
- package/dist/worker/failure-fingerprint.d.ts.map +1 -0
- package/dist/worker/failure-fingerprint.js +75 -0
- package/dist/worker/failure-fingerprint.js.map +1 -0
- package/dist/worker/label-manager.d.ts +42 -0
- package/dist/worker/label-manager.d.ts.map +1 -1
- package/dist/worker/label-manager.js +108 -4
- package/dist/worker/label-manager.js.map +1 -1
- package/dist/worker/phase-loop.d.ts +30 -0
- package/dist/worker/phase-loop.d.ts.map +1 -1
- package/dist/worker/phase-loop.js +124 -26
- package/dist/worker/phase-loop.js.map +1 -1
- package/dist/worker/pr-feedback-handler.d.ts +13 -0
- package/dist/worker/pr-feedback-handler.d.ts.map +1 -1
- package/dist/worker/pr-feedback-handler.js +48 -0
- package/dist/worker/pr-feedback-handler.js.map +1 -1
- package/dist/worker/stage-comment-manager.d.ts +5 -0
- package/dist/worker/stage-comment-manager.d.ts.map +1 -1
- package/dist/worker/stage-comment-manager.js +8 -1
- package/dist/worker/stage-comment-manager.js.map +1 -1
- package/dist/worker/terminal-label-op-error.d.ts +1 -1
- package/dist/worker/terminal-label-op-error.d.ts.map +1 -1
- package/dist/worker/terminal-label-op-error.js.map +1 -1
- package/dist/worker/types.d.ts +31 -0
- package/dist/worker/types.d.ts.map +1 -1
- package/dist/worker/types.js +11 -0
- package/dist/worker/types.js.map +1 -1
- package/package.json +7 -6
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #953 — Shared adaptive-polling decision function used by
|
|
3
|
+
* `LabelMonitorService`, `PrFeedbackMonitorService`, and
|
|
4
|
+
* `MergeConflictMonitorService`. Extracted to eliminate the three-way
|
|
5
|
+
* copy-paste that produced the "smee-less cluster stuck at base cadence" bug.
|
|
6
|
+
*
|
|
7
|
+
* Pure: no I/O, no `this`, no `Date.now()`. Time flows in via `params.nowMs`.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Fast interval calculation shared between `decideAdaptivePoll` and callers
|
|
11
|
+
* that need the value at construction time.
|
|
12
|
+
*/
|
|
13
|
+
export function computeFastInterval(basePollIntervalMs, adaptiveDivisor, minPollIntervalMs) {
|
|
14
|
+
return Math.max(minPollIntervalMs, Math.floor(basePollIntervalMs / adaptiveDivisor));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Decide the next poll cadence given current state and tuning.
|
|
18
|
+
*
|
|
19
|
+
* Branch summary:
|
|
20
|
+
* - webhooksConfigured=false, adaptivePolling=true:
|
|
21
|
+
* cycle 1 → { fast, healthy=false, to-fast, webhooks-not-configured }
|
|
22
|
+
* cycle 2+ → { fast, healthy=false, none, webhooks-not-configured }
|
|
23
|
+
* - webhooksConfigured=false, adaptivePolling=false:
|
|
24
|
+
* always → { base, healthy=true, none, operator-opt-out }
|
|
25
|
+
* - webhooksConfigured=true, lastWebhookEvent===null:
|
|
26
|
+
* always → { unchanged, unchanged, none, quiet }
|
|
27
|
+
* - webhooksConfigured=true, lastWebhookEvent!==null:
|
|
28
|
+
* staleness = nowMs - lastWebhookEvent; threshold = basePoll * 2
|
|
29
|
+
* if (staleness > threshold && webhookHealthy) → to-fast, webhook-stale
|
|
30
|
+
* else → { unchanged, unchanged, none, quiet }
|
|
31
|
+
*
|
|
32
|
+
* The `recordWebhookEvent` handler in each service also delegates here for
|
|
33
|
+
* the recovery path — it flips `webhookHealthy=true` and `lastWebhookEvent`
|
|
34
|
+
* before calling; the helper returns `to-base, webhook-recovered` when the
|
|
35
|
+
* caller was previously at the fast interval.
|
|
36
|
+
*/
|
|
37
|
+
export function decideAdaptivePoll(params) {
|
|
38
|
+
const { webhooksConfigured, adaptivePolling, basePollIntervalMs, currentPollIntervalMs, lastWebhookEvent, webhookHealthy, adaptiveDivisor, minPollIntervalMs, nowMs, } = params;
|
|
39
|
+
const fastIntervalMs = computeFastInterval(basePollIntervalMs, adaptiveDivisor, minPollIntervalMs);
|
|
40
|
+
if (!webhooksConfigured) {
|
|
41
|
+
if (!adaptivePolling) {
|
|
42
|
+
return {
|
|
43
|
+
currentPollIntervalMs: basePollIntervalMs,
|
|
44
|
+
webhookHealthy: true,
|
|
45
|
+
transition: 'none',
|
|
46
|
+
reason: 'operator-opt-out',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// adaptivePolling === true, webhooksConfigured === false:
|
|
50
|
+
// fast interval, unhealthy. Emit `to-fast` on the transition edge only.
|
|
51
|
+
const transition = webhookHealthy ? 'to-fast' : 'none';
|
|
52
|
+
return {
|
|
53
|
+
currentPollIntervalMs: fastIntervalMs,
|
|
54
|
+
webhookHealthy: false,
|
|
55
|
+
transition,
|
|
56
|
+
reason: 'webhooks-not-configured',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// webhooksConfigured === true from here on.
|
|
60
|
+
if (lastWebhookEvent === null) {
|
|
61
|
+
// Grace: never received an event, don't ratchet down cadence.
|
|
62
|
+
return {
|
|
63
|
+
currentPollIntervalMs,
|
|
64
|
+
webhookHealthy,
|
|
65
|
+
transition: 'none',
|
|
66
|
+
reason: 'quiet',
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const staleness = nowMs - lastWebhookEvent;
|
|
70
|
+
const staleThreshold = basePollIntervalMs * 2;
|
|
71
|
+
// Recovery path: caller set webhookHealthy=true before delegating.
|
|
72
|
+
// If we're on the fast interval, transition back to base.
|
|
73
|
+
if (webhookHealthy && currentPollIntervalMs !== basePollIntervalMs) {
|
|
74
|
+
return {
|
|
75
|
+
currentPollIntervalMs: basePollIntervalMs,
|
|
76
|
+
webhookHealthy: true,
|
|
77
|
+
transition: 'to-base',
|
|
78
|
+
reason: 'webhook-recovered',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (staleness > staleThreshold && webhookHealthy) {
|
|
82
|
+
return {
|
|
83
|
+
currentPollIntervalMs: fastIntervalMs,
|
|
84
|
+
webhookHealthy: false,
|
|
85
|
+
transition: 'to-fast',
|
|
86
|
+
reason: 'webhook-stale',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
currentPollIntervalMs,
|
|
91
|
+
webhookHealthy,
|
|
92
|
+
transition: 'none',
|
|
93
|
+
reason: 'quiet',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=adaptive-poll-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adaptive-poll-controller.js","sourceRoot":"","sources":["../../src/services/adaptive-poll-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiDH;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,kBAA0B,EAC1B,eAAuB,EACvB,iBAAyB;IAEzB,OAAO,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,IAAI,CAAC,KAAK,CAAC,kBAAkB,GAAG,eAAe,CAAC,CAAC,CAAC;AACvF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA0B;IAC3D,MAAM,EACJ,kBAAkB,EAClB,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,iBAAiB,EACjB,KAAK,GACN,GAAG,MAAM,CAAC;IAEX,MAAM,cAAc,GAAG,mBAAmB,CAAC,kBAAkB,EAAE,eAAe,EAAE,iBAAiB,CAAC,CAAC;IAEnG,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACxB,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,OAAO;gBACL,qBAAqB,EAAE,kBAAkB;gBACzC,cAAc,EAAE,IAAI;gBACpB,UAAU,EAAE,MAAM;gBAClB,MAAM,EAAE,kBAAkB;aAC3B,CAAC;QACJ,CAAC;QACD,0DAA0D;QAC1D,wEAAwE;QACxE,MAAM,UAAU,GAAG,cAAc,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC;QACvD,OAAO;YACL,qBAAqB,EAAE,cAAc;YACrC,cAAc,EAAE,KAAK;YACrB,UAAU;YACV,MAAM,EAAE,yBAAyB;SAClC,CAAC;IACJ,CAAC;IAED,4CAA4C;IAE5C,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;QAC9B,8DAA8D;QAC9D,OAAO;YACL,qBAAqB;YACrB,cAAc;YACd,UAAU,EAAE,MAAM;YAClB,MAAM,EAAE,OAAO;SAChB,CAAC;IACJ,CAAC;IAED,MAAM,SAAS,GAAG,KAAK,GAAG,gBAAgB,CAAC;IAC3C,MAAM,cAAc,GAAG,kBAAkB,GAAG,CAAC,CAAC;IAE9C,mEAAmE;IACnE,0DAA0D;IAC1D,IAAI,cAAc,IAAI,qBAAqB,KAAK,kBAAkB,EAAE,CAAC;QACnE,OAAO;YACL,qBAAqB,EAAE,kBAAkB;YACzC,cAAc,EAAE,IAAI;YACpB,UAAU,EAAE,SAAS;YACrB,MAAM,EAAE,mBAAmB;SAC5B,CAAC;IACJ,CAAC;IAED,IAAI,SAAS,GAAG,cAAc,IAAI,cAAc,EAAE,CAAC;QACjD,OAAO;YACL,qBAAqB,EAAE,cAAc;YACrC,cAAc,EAAE,KAAK;YACrB,UAAU,EAAE,SAAS;YACrB,MAAM,EAAE,eAAe;SACxB,CAAC;IACJ,CAAC;IAED,OAAO;QACL,qBAAqB;QACrB,cAAc;QACd,UAAU,EAAE,MAAM;QAClB,MAAM,EAAE,OAAO;KAChB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #958 T013 — Poll-based monitor that enqueues `continue` resume queue items
|
|
3
|
+
* for issues sitting at `waiting-for:clarification` + `agent:paused` when a
|
|
4
|
+
* new human-authored comment appears.
|
|
5
|
+
*
|
|
6
|
+
* Shape template: `merge-conflict-monitor-service.ts` (spec §Assumptions).
|
|
7
|
+
* Contract: `specs/958-found-during-local-snappoll/contracts/
|
|
8
|
+
* clarification-answer-monitor.md`.
|
|
9
|
+
*
|
|
10
|
+
* Key divergences from the merge-conflict monitor (per data-model.md):
|
|
11
|
+
* - Precondition label: `waiting-for:clarification` (was `waiting-for:merge-conflicts`).
|
|
12
|
+
* - Additional predicate: at least one comment with `viewerDidAuthor === false`
|
|
13
|
+
* that also passes `isTrustedCommentAuthor(_, 'answer-scanner', trustCtx)`.
|
|
14
|
+
* Comments carrying the engine-written answer marker are treated as
|
|
15
|
+
* cluster-self (not counted).
|
|
16
|
+
* - Queue command: `command: 'continue'` (was `'resolve-merge-conflicts'`).
|
|
17
|
+
*
|
|
18
|
+
* Non-behavior (contract):
|
|
19
|
+
* - MUST NOT apply `completed:clarification`. That label is reserved for
|
|
20
|
+
* the human's explicit force-advance override (FR-011).
|
|
21
|
+
* - MUST NOT modify `clarifications.md` — the monitor has no checkout; the
|
|
22
|
+
* phase loop performs integration on the resume path.
|
|
23
|
+
* - MUST NOT clear `waiting-for:clarification` or `agent:paused`.
|
|
24
|
+
*/
|
|
25
|
+
import { type GitHubClientFactory } from '@generacy-ai/workflow-engine';
|
|
26
|
+
import type { MonitorState, QueueManager } from '../types/monitor.js';
|
|
27
|
+
import type { RepositoryConfig, PrMonitorConfig } from '../config/schema.js';
|
|
28
|
+
import type { Logger } from '../worker/types.js';
|
|
29
|
+
import type { AuthHealthSink } from './label-monitor-service.js';
|
|
30
|
+
export interface ClarificationAnswerEvent {
|
|
31
|
+
owner: string;
|
|
32
|
+
repo: string;
|
|
33
|
+
issueNumber: number;
|
|
34
|
+
issueLabels: string[];
|
|
35
|
+
source: 'poll';
|
|
36
|
+
}
|
|
37
|
+
export interface ClarificationAnswerMonitorOptions {
|
|
38
|
+
repositories: RepositoryConfig[];
|
|
39
|
+
pollIntervalMs: number;
|
|
40
|
+
adaptivePolling: boolean;
|
|
41
|
+
maxConcurrentPolls: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* #987: options for the runtime `setWebhooksConfigured(true, opts?)` flip.
|
|
45
|
+
* See specs/987-summary-cluster-where-smee/contracts/setter-contract.md.
|
|
46
|
+
*/
|
|
47
|
+
export interface SetWebhooksConfiguredOptions {
|
|
48
|
+
basePollIntervalMs?: number;
|
|
49
|
+
}
|
|
50
|
+
export declare class ClarificationAnswerMonitorService {
|
|
51
|
+
private readonly logger;
|
|
52
|
+
private readonly createClient;
|
|
53
|
+
private readonly tokenProvider?;
|
|
54
|
+
private readonly queueManager;
|
|
55
|
+
private readonly options;
|
|
56
|
+
private readonly clusterGithubUsername;
|
|
57
|
+
private readonly authHealth;
|
|
58
|
+
private readonly githubAppCredentialId;
|
|
59
|
+
private abortController;
|
|
60
|
+
private state;
|
|
61
|
+
constructor(logger: Logger, createClient: GitHubClientFactory, queueManager: QueueManager, config: PrMonitorConfig, repositories: RepositoryConfig[], clusterGithubUsername?: string, tokenProvider?: () => Promise<string | undefined>, authHealth?: AuthHealthSink, githubAppCredentialId?: string, webhooksConfigured?: boolean);
|
|
62
|
+
/**
|
|
63
|
+
* Process a clarification-answer event: verify preconditions, check for a
|
|
64
|
+
* new human-authored comment, enqueue a resume via `enqueueIfAbsent`.
|
|
65
|
+
*
|
|
66
|
+
* Returns true if enqueued, false if skipped or duplicate.
|
|
67
|
+
*/
|
|
68
|
+
processClarificationAnswerEvent(event: ClarificationAnswerEvent): Promise<boolean>;
|
|
69
|
+
/**
|
|
70
|
+
* #993 — positive answer predicate. Returns the first comment that qualifies
|
|
71
|
+
* as a real clarification answer, or `undefined` if none exists on the issue.
|
|
72
|
+
*
|
|
73
|
+
* Short-circuits when the issue carries no question-marker comment (data-
|
|
74
|
+
* integrity signal — the clarify phase always posts one before applying
|
|
75
|
+
* `waiting-for:clarification`).
|
|
76
|
+
*/
|
|
77
|
+
private findAnswerCandidate;
|
|
78
|
+
startPolling(): Promise<void>;
|
|
79
|
+
stopPolling(): void;
|
|
80
|
+
poll(): Promise<void>;
|
|
81
|
+
private pollRepo;
|
|
82
|
+
recordWebhookEvent(): void;
|
|
83
|
+
private updateAdaptivePolling;
|
|
84
|
+
getState(): Readonly<MonitorState>;
|
|
85
|
+
/**
|
|
86
|
+
* #987: flip `webhooksConfigured` to `true` at runtime. Setter is one-way
|
|
87
|
+
* (Q1); `adaptivePolling` stays untouched so the staleness safety net is
|
|
88
|
+
* reachable (Q2). See specs/987-summary-cluster-where-smee/clarifications.md.
|
|
89
|
+
*/
|
|
90
|
+
setWebhooksConfigured(configured: true, opts?: SetWebhooksConfiguredOptions): void;
|
|
91
|
+
private sleep;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=clarification-answer-monitor-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clarification-answer-monitor-service.d.ts","sourceRoot":"","sources":["../../src/services/clarification-answer-monitor-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EAEb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAEjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAcjE,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iCAAiC;IAChD,YAAY,EAAE,gBAAgB,EAAE,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,4BAA4B;IAC3C,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AA4BD,qBAAa,iCAAiC;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAsB;IACnD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAoC;IACnE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAqB;IAC3D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAiB;IAC5C,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAqB;IAC3D,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,KAAK,CAAe;gBAG1B,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,mBAAmB,EACjC,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,eAAe,EACvB,YAAY,EAAE,gBAAgB,EAAE,EAChC,qBAAqB,CAAC,EAAE,MAAM,EAC9B,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,EACjD,UAAU,CAAC,EAAE,cAAc,EAC3B,qBAAqB,CAAC,EAAE,MAAM,EAC9B,kBAAkB,GAAE,OAAe;IA6BrC;;;;;OAKG;IACG,+BAA+B,CACnC,KAAK,EAAE,wBAAwB,GAC9B,OAAO,CAAC,OAAO,CAAC;IA+FnB;;;;;;;OAOG;IACH,OAAO,CAAC,mBAAmB;IAyCrB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCnC,WAAW,IAAI,IAAI;IAQb,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAkBb,QAAQ;IAoFtB,kBAAkB,IAAI,IAAI;IAwB1B,OAAO,CAAC,qBAAqB;IA0B7B,QAAQ,IAAI,QAAQ,CAAC,YAAY,CAAC;IAIlC;;;;OAIG;IACH,qBAAqB,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,4BAA4B,GAAG,IAAI;IAalF,OAAO,CAAC,KAAK;CAcd"}
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #958 T013 — Poll-based monitor that enqueues `continue` resume queue items
|
|
3
|
+
* for issues sitting at `waiting-for:clarification` + `agent:paused` when a
|
|
4
|
+
* new human-authored comment appears.
|
|
5
|
+
*
|
|
6
|
+
* Shape template: `merge-conflict-monitor-service.ts` (spec §Assumptions).
|
|
7
|
+
* Contract: `specs/958-found-during-local-snappoll/contracts/
|
|
8
|
+
* clarification-answer-monitor.md`.
|
|
9
|
+
*
|
|
10
|
+
* Key divergences from the merge-conflict monitor (per data-model.md):
|
|
11
|
+
* - Precondition label: `waiting-for:clarification` (was `waiting-for:merge-conflicts`).
|
|
12
|
+
* - Additional predicate: at least one comment with `viewerDidAuthor === false`
|
|
13
|
+
* that also passes `isTrustedCommentAuthor(_, 'answer-scanner', trustCtx)`.
|
|
14
|
+
* Comments carrying the engine-written answer marker are treated as
|
|
15
|
+
* cluster-self (not counted).
|
|
16
|
+
* - Queue command: `command: 'continue'` (was `'resolve-merge-conflicts'`).
|
|
17
|
+
*
|
|
18
|
+
* Non-behavior (contract):
|
|
19
|
+
* - MUST NOT apply `completed:clarification`. That label is reserved for
|
|
20
|
+
* the human's explicit force-advance override (FR-011).
|
|
21
|
+
* - MUST NOT modify `clarifications.md` — the monitor has no checkout; the
|
|
22
|
+
* phase loop performs integration on the resume path.
|
|
23
|
+
* - MUST NOT clear `waiting-for:clarification` or `agent:paused`.
|
|
24
|
+
*/
|
|
25
|
+
import { GhAuthError, isTrustedCommentAuthor, tryLoadCommentTrustConfig, } from '@generacy-ai/workflow-engine';
|
|
26
|
+
import { JitTokenError } from '@generacy-ai/control-plane';
|
|
27
|
+
import { filterByAssignee } from './identity.js';
|
|
28
|
+
import { commentCarriesAnswerMarker, commentCarriesMachineMarker, matchClarificationQuestionMarker, } from '../worker/clarification-markers.js';
|
|
29
|
+
import { decideAdaptivePoll } from './adaptive-poll-controller.js';
|
|
30
|
+
const WAITING_FOR_CLARIFICATION_LABEL = 'waiting-for:clarification';
|
|
31
|
+
const AGENT_PAUSED_LABEL = 'agent:paused';
|
|
32
|
+
const MIN_POLL_INTERVAL_MS = 10_000;
|
|
33
|
+
const ADAPTIVE_DIVISOR = 2;
|
|
34
|
+
/**
|
|
35
|
+
* Adapter: pino-style logger (WorkerContext.Logger) → workflow-engine Logger.
|
|
36
|
+
* Same bridge used by `clarification-poster.ts` — kept private to this file
|
|
37
|
+
* so the merge-conflict monitor's DI surface stays untouched.
|
|
38
|
+
*/
|
|
39
|
+
function toEngineLogger(logger) {
|
|
40
|
+
return {
|
|
41
|
+
info: (msg, meta) => {
|
|
42
|
+
if (meta && typeof meta === 'object')
|
|
43
|
+
logger.info(meta, msg);
|
|
44
|
+
else
|
|
45
|
+
logger.info(msg);
|
|
46
|
+
},
|
|
47
|
+
warn: (msg, meta) => {
|
|
48
|
+
if (meta && typeof meta === 'object')
|
|
49
|
+
logger.warn(meta, msg);
|
|
50
|
+
else
|
|
51
|
+
logger.warn(msg);
|
|
52
|
+
},
|
|
53
|
+
error: (msg, meta) => {
|
|
54
|
+
if (meta && typeof meta === 'object')
|
|
55
|
+
logger.error(meta, msg);
|
|
56
|
+
else
|
|
57
|
+
logger.error(msg);
|
|
58
|
+
},
|
|
59
|
+
debug: (msg, meta) => {
|
|
60
|
+
if (meta && typeof meta === 'object')
|
|
61
|
+
logger.debug(meta, msg);
|
|
62
|
+
else
|
|
63
|
+
logger.debug(msg);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export class ClarificationAnswerMonitorService {
|
|
68
|
+
logger;
|
|
69
|
+
createClient;
|
|
70
|
+
tokenProvider;
|
|
71
|
+
queueManager;
|
|
72
|
+
options;
|
|
73
|
+
clusterGithubUsername;
|
|
74
|
+
authHealth;
|
|
75
|
+
githubAppCredentialId;
|
|
76
|
+
abortController = null;
|
|
77
|
+
state;
|
|
78
|
+
constructor(logger, createClient, queueManager, config, repositories, clusterGithubUsername, tokenProvider, authHealth, githubAppCredentialId, webhooksConfigured = false) {
|
|
79
|
+
this.logger = logger;
|
|
80
|
+
this.createClient = createClient;
|
|
81
|
+
this.tokenProvider = tokenProvider;
|
|
82
|
+
this.queueManager = queueManager;
|
|
83
|
+
this.clusterGithubUsername = clusterGithubUsername;
|
|
84
|
+
this.authHealth = authHealth ?? { recordResult: () => undefined };
|
|
85
|
+
this.githubAppCredentialId = githubAppCredentialId;
|
|
86
|
+
this.options = {
|
|
87
|
+
repositories,
|
|
88
|
+
pollIntervalMs: config.pollIntervalMs,
|
|
89
|
+
adaptivePolling: config.adaptivePolling,
|
|
90
|
+
maxConcurrentPolls: config.maxConcurrentPolls,
|
|
91
|
+
};
|
|
92
|
+
this.state = {
|
|
93
|
+
isPolling: false,
|
|
94
|
+
webhookHealthy: true,
|
|
95
|
+
lastWebhookEvent: null,
|
|
96
|
+
currentPollIntervalMs: config.pollIntervalMs,
|
|
97
|
+
basePollIntervalMs: config.pollIntervalMs,
|
|
98
|
+
webhooksConfigured,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ==========================================================================
|
|
102
|
+
// Event Processing
|
|
103
|
+
// ==========================================================================
|
|
104
|
+
/**
|
|
105
|
+
* Process a clarification-answer event: verify preconditions, check for a
|
|
106
|
+
* new human-authored comment, enqueue a resume via `enqueueIfAbsent`.
|
|
107
|
+
*
|
|
108
|
+
* Returns true if enqueued, false if skipped or duplicate.
|
|
109
|
+
*/
|
|
110
|
+
async processClarificationAnswerEvent(event) {
|
|
111
|
+
const { owner, repo, issueNumber, issueLabels, source } = event;
|
|
112
|
+
const hasWaitingFor = issueLabels.includes(WAITING_FOR_CLARIFICATION_LABEL);
|
|
113
|
+
const hasPaused = issueLabels.includes(AGENT_PAUSED_LABEL);
|
|
114
|
+
if (!hasWaitingFor || !hasPaused) {
|
|
115
|
+
this.logger.debug({ owner, repo, issueNumber, source, hasWaitingFor, hasPaused }, 'Precondition failed for clarification-answer enqueue (need waiting-for:clarification + agent:paused)');
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const blockedLabel = issueLabels.find((l) => l.startsWith('blocked:'));
|
|
119
|
+
if (blockedLabel) {
|
|
120
|
+
this.logger.info({ owner, repo, issueNumber, blockedLabel, reason: 'blocked-label-present' }, 'Skipping clarification-answer enqueue while blocked:* label is present');
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
// Fetch comments and require ≥1 human-authored (viewerDidAuthor === false)
|
|
124
|
+
// trusted comment. `viewerDidAuthor === true` comments (cluster-self) do
|
|
125
|
+
// not count as "new human answer" — even if they carry the answer marker,
|
|
126
|
+
// the phase loop is the source of truth for integrating cluster-relayed
|
|
127
|
+
// answers, not the monitor.
|
|
128
|
+
const client = this.createClient(undefined, this.tokenProvider);
|
|
129
|
+
let comments;
|
|
130
|
+
try {
|
|
131
|
+
comments = await client.getIssueCommentsWithViewerAuth(owner, repo, issueNumber);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
this.logger.warn({ err: String(err), owner, repo, issueNumber }, 'Failed to fetch comments during clarification-answer event — skipping');
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const trustConfig = tryLoadCommentTrustConfig(process.cwd(), toEngineLogger(this.logger));
|
|
138
|
+
const trustCtx = {
|
|
139
|
+
logger: toEngineLogger(this.logger),
|
|
140
|
+
...(this.clusterGithubUsername ? { botLogin: this.clusterGithubUsername } : {}),
|
|
141
|
+
...(trustConfig ? { config: trustConfig } : {}),
|
|
142
|
+
};
|
|
143
|
+
const candidate = this.findAnswerCandidate(comments, trustCtx);
|
|
144
|
+
if (!candidate) {
|
|
145
|
+
this.logger.debug({ owner, repo, issueNumber }, 'No answer candidate found — nothing to resume on (no question-marker comment, or no comment newer than the latest question by a non-bot trusted author)');
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const workflowLabel = issueLabels.find((l) => l.startsWith('workflow:'));
|
|
149
|
+
const workflowName = workflowLabel
|
|
150
|
+
? workflowLabel.slice('workflow:'.length)
|
|
151
|
+
: 'speckit-feature';
|
|
152
|
+
const queueItem = {
|
|
153
|
+
owner,
|
|
154
|
+
repo,
|
|
155
|
+
issueNumber,
|
|
156
|
+
workflowName,
|
|
157
|
+
command: 'continue',
|
|
158
|
+
priority: Date.now(),
|
|
159
|
+
enqueuedAt: new Date().toISOString(),
|
|
160
|
+
metadata: {},
|
|
161
|
+
queueReason: 'resume',
|
|
162
|
+
};
|
|
163
|
+
const itemKey = `${owner}/${repo}#${issueNumber}`;
|
|
164
|
+
const enqueued = await this.queueManager.enqueueIfAbsent(queueItem);
|
|
165
|
+
if (!enqueued) {
|
|
166
|
+
this.logger.info({ itemKey, reason: 'in-flight', owner, repo, issueNumber }, 'Dropping clarification-answer enqueue (item already in flight)');
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
this.logger.info({
|
|
170
|
+
event: 'clarification-answer-resume-enqueued',
|
|
171
|
+
owner,
|
|
172
|
+
repo,
|
|
173
|
+
issueNumber,
|
|
174
|
+
source,
|
|
175
|
+
}, 'Clarification-answer resume enqueued');
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* #993 — positive answer predicate. Returns the first comment that qualifies
|
|
180
|
+
* as a real clarification answer, or `undefined` if none exists on the issue.
|
|
181
|
+
*
|
|
182
|
+
* Short-circuits when the issue carries no question-marker comment (data-
|
|
183
|
+
* integrity signal — the clarify phase always posts one before applying
|
|
184
|
+
* `waiting-for:clarification`).
|
|
185
|
+
*/
|
|
186
|
+
findAnswerCandidate(comments, trustCtx) {
|
|
187
|
+
// `created_at`-only newness is intentional (replay-safe); blocks the
|
|
188
|
+
// `<!-- generacy-stage:specification -->` summary re-trigger vector where
|
|
189
|
+
// the bot updates its own summary and advances `updated_at` every poll.
|
|
190
|
+
const questionAnchor = latestQuestionCommentCreatedAt(comments);
|
|
191
|
+
if (questionAnchor === undefined)
|
|
192
|
+
return undefined;
|
|
193
|
+
for (const c of comments) {
|
|
194
|
+
// `[bot]`-suffix authors are dropped upstream of the trust helper.
|
|
195
|
+
// Cluster-relayed answers flow through the `completed:clarification`
|
|
196
|
+
// label / LabelMonitorService path, never through this monitor.
|
|
197
|
+
if (isBotAuthoredLogin(c.author))
|
|
198
|
+
continue;
|
|
199
|
+
if (c.created_at <= questionAnchor)
|
|
200
|
+
continue;
|
|
201
|
+
// Branch (a) runs before the machine-marker skip: `commentCarriesAnswerMarker`
|
|
202
|
+
// is a positive signal, and the answer-marker prefix is present in
|
|
203
|
+
// MACHINE_MARKERS as a #976 engine-noise entry. Without this order, a
|
|
204
|
+
// non-bot human posting `<!-- generacy-clarification-answers:1 -->` would
|
|
205
|
+
// be filtered out as machine noise (SC-003 regression).
|
|
206
|
+
if (commentCarriesAnswerMarker(c.body))
|
|
207
|
+
return c;
|
|
208
|
+
if (commentCarriesMachineMarker(c.body))
|
|
209
|
+
continue;
|
|
210
|
+
const decision = isTrustedCommentAuthor(c, 'answer-scanner', trustCtx);
|
|
211
|
+
if (decision.trusted &&
|
|
212
|
+
decision.reason !== 'bot' &&
|
|
213
|
+
decision.reason !== 'self-authored') {
|
|
214
|
+
return c;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
// ==========================================================================
|
|
220
|
+
// Polling
|
|
221
|
+
// ==========================================================================
|
|
222
|
+
async startPolling() {
|
|
223
|
+
if (this.state.isPolling) {
|
|
224
|
+
this.logger.warn('Clarification-answer monitor polling already running');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const ac = new AbortController();
|
|
228
|
+
this.abortController = ac;
|
|
229
|
+
this.state.isPolling = true;
|
|
230
|
+
this.logger.info({ intervalMs: this.state.currentPollIntervalMs, repos: this.options.repositories.length }, 'Starting clarification-answer monitor polling');
|
|
231
|
+
while (!ac.signal.aborted) {
|
|
232
|
+
try {
|
|
233
|
+
await this.poll();
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
this.logger.error({ err: error }, 'Error during clarification-answer poll cycle');
|
|
237
|
+
}
|
|
238
|
+
if (this.options.adaptivePolling) {
|
|
239
|
+
this.updateAdaptivePolling();
|
|
240
|
+
}
|
|
241
|
+
await this.sleep(this.state.currentPollIntervalMs, ac.signal);
|
|
242
|
+
}
|
|
243
|
+
this.state.isPolling = false;
|
|
244
|
+
this.logger.info('Clarification-answer monitor polling loop stopped');
|
|
245
|
+
}
|
|
246
|
+
stopPolling() {
|
|
247
|
+
if (this.abortController) {
|
|
248
|
+
this.abortController.abort();
|
|
249
|
+
this.abortController = null;
|
|
250
|
+
this.logger.info('Clarification-answer monitor polling stop requested');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
async poll() {
|
|
254
|
+
const repos = this.options.repositories;
|
|
255
|
+
if (repos.length === 0)
|
|
256
|
+
return;
|
|
257
|
+
const semaphore = new Semaphore(this.options.maxConcurrentPolls);
|
|
258
|
+
const pollTasks = repos.map(({ owner, repo }) => semaphore.acquire().then(async (release) => {
|
|
259
|
+
try {
|
|
260
|
+
await this.pollRepo(owner, repo);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
release();
|
|
264
|
+
}
|
|
265
|
+
}));
|
|
266
|
+
await Promise.allSettled(pollTasks);
|
|
267
|
+
}
|
|
268
|
+
async pollRepo(owner, repo) {
|
|
269
|
+
const client = this.createClient(undefined, this.tokenProvider);
|
|
270
|
+
let allIssues;
|
|
271
|
+
try {
|
|
272
|
+
allIssues = await client.listIssuesWithLabel(owner, repo, WAITING_FOR_CLARIFICATION_LABEL);
|
|
273
|
+
if (this.githubAppCredentialId) {
|
|
274
|
+
this.authHealth.recordResult(this.githubAppCredentialId, { ok: true });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error instanceof JitTokenError) {
|
|
279
|
+
this.logger.warn({ code: error.code, message: error.message, owner, repo }, 'JIT GitHub token refresh failed — skipping clarification-answer monitor cycle');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (error instanceof GhAuthError) {
|
|
283
|
+
if (this.githubAppCredentialId) {
|
|
284
|
+
this.authHealth.recordResult(this.githubAppCredentialId, { ok: false, statusCode: error.statusCode });
|
|
285
|
+
}
|
|
286
|
+
this.logger.warn({
|
|
287
|
+
credentialId: this.githubAppCredentialId,
|
|
288
|
+
statusCode: error.statusCode,
|
|
289
|
+
owner,
|
|
290
|
+
repo,
|
|
291
|
+
}, 'GitHub authentication failing on clarification-answer monitor — investigate credential refresh chain');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.logger.warn({ err: String(error), owner, repo }, 'Error polling repository for clarification-answer pauses');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const issues = filterByAssignee(allIssues, this.clusterGithubUsername, this.logger);
|
|
298
|
+
if (issues.length === 0) {
|
|
299
|
+
this.logger.debug({ owner, repo }, 'No clarification-answer pauses found this cycle');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.logger.info({ owner, repo, count: issues.length }, 'Clarification-answer pauses found');
|
|
303
|
+
for (const issue of issues) {
|
|
304
|
+
const event = {
|
|
305
|
+
owner,
|
|
306
|
+
repo,
|
|
307
|
+
issueNumber: issue.number,
|
|
308
|
+
issueLabels: issue.labels.map((l) => l.name),
|
|
309
|
+
source: 'poll',
|
|
310
|
+
};
|
|
311
|
+
try {
|
|
312
|
+
await this.processClarificationAnswerEvent(event);
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
this.logger.warn({ err: String(err), owner, repo, issueNumber: issue.number }, 'Error processing clarification-answer event — continuing to next issue');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// ==========================================================================
|
|
320
|
+
// Adaptive Polling
|
|
321
|
+
// ==========================================================================
|
|
322
|
+
recordWebhookEvent() {
|
|
323
|
+
this.state.lastWebhookEvent = Date.now();
|
|
324
|
+
this.state.webhookHealthy = true;
|
|
325
|
+
const decision = decideAdaptivePoll({
|
|
326
|
+
webhooksConfigured: this.state.webhooksConfigured,
|
|
327
|
+
adaptivePolling: this.options.adaptivePolling,
|
|
328
|
+
basePollIntervalMs: this.state.basePollIntervalMs,
|
|
329
|
+
currentPollIntervalMs: this.state.currentPollIntervalMs,
|
|
330
|
+
lastWebhookEvent: this.state.lastWebhookEvent,
|
|
331
|
+
webhookHealthy: this.state.webhookHealthy,
|
|
332
|
+
adaptiveDivisor: ADAPTIVE_DIVISOR,
|
|
333
|
+
minPollIntervalMs: MIN_POLL_INTERVAL_MS,
|
|
334
|
+
nowMs: Date.now(),
|
|
335
|
+
});
|
|
336
|
+
this.state.currentPollIntervalMs = decision.currentPollIntervalMs;
|
|
337
|
+
this.state.webhookHealthy = decision.webhookHealthy;
|
|
338
|
+
if (decision.transition !== 'none') {
|
|
339
|
+
this.logger.info({ intervalMs: this.state.currentPollIntervalMs, reason: decision.reason }, 'Webhook reconnected, restoring clarification-answer monitor poll interval');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
updateAdaptivePolling() {
|
|
343
|
+
const decision = decideAdaptivePoll({
|
|
344
|
+
webhooksConfigured: this.state.webhooksConfigured,
|
|
345
|
+
adaptivePolling: this.options.adaptivePolling,
|
|
346
|
+
basePollIntervalMs: this.state.basePollIntervalMs,
|
|
347
|
+
currentPollIntervalMs: this.state.currentPollIntervalMs,
|
|
348
|
+
lastWebhookEvent: this.state.lastWebhookEvent,
|
|
349
|
+
webhookHealthy: this.state.webhookHealthy,
|
|
350
|
+
adaptiveDivisor: ADAPTIVE_DIVISOR,
|
|
351
|
+
minPollIntervalMs: MIN_POLL_INTERVAL_MS,
|
|
352
|
+
nowMs: Date.now(),
|
|
353
|
+
});
|
|
354
|
+
this.state.currentPollIntervalMs = decision.currentPollIntervalMs;
|
|
355
|
+
this.state.webhookHealthy = decision.webhookHealthy;
|
|
356
|
+
if (decision.transition !== 'none') {
|
|
357
|
+
this.logger.info({ intervalMs: this.state.currentPollIntervalMs, reason: decision.reason }, 'Webhooks appear unhealthy, increasing clarification-answer poll frequency');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// ==========================================================================
|
|
361
|
+
// State access
|
|
362
|
+
// ==========================================================================
|
|
363
|
+
getState() {
|
|
364
|
+
return { ...this.state };
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* #987: flip `webhooksConfigured` to `true` at runtime. Setter is one-way
|
|
368
|
+
* (Q1); `adaptivePolling` stays untouched so the staleness safety net is
|
|
369
|
+
* reachable (Q2). See specs/987-summary-cluster-where-smee/clarifications.md.
|
|
370
|
+
*/
|
|
371
|
+
setWebhooksConfigured(configured, opts) {
|
|
372
|
+
void configured;
|
|
373
|
+
this.state.webhooksConfigured = true;
|
|
374
|
+
if (opts?.basePollIntervalMs !== undefined) {
|
|
375
|
+
this.state.basePollIntervalMs = opts.basePollIntervalMs;
|
|
376
|
+
this.state.currentPollIntervalMs = opts.basePollIntervalMs;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// ==========================================================================
|
|
380
|
+
// Utilities
|
|
381
|
+
// ==========================================================================
|
|
382
|
+
sleep(ms, signal) {
|
|
383
|
+
return new Promise((resolve) => {
|
|
384
|
+
if (signal.aborted) {
|
|
385
|
+
resolve();
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const timer = setTimeout(resolve, ms);
|
|
389
|
+
const onAbort = () => {
|
|
390
|
+
clearTimeout(timer);
|
|
391
|
+
resolve();
|
|
392
|
+
};
|
|
393
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* #993 FR-001 — case-insensitive, whitespace-tolerant `[bot]`-suffix test.
|
|
399
|
+
* Mirrors `normalizeLogin`'s trim+lowercase shape without stripping the
|
|
400
|
+
* suffix (we're detecting it, not removing it). Any App bot identity
|
|
401
|
+
* (`generacy-ai[bot]`, `staging-generacy[bot]`, `dependabot[bot]`, …)
|
|
402
|
+
* is treated identically.
|
|
403
|
+
*/
|
|
404
|
+
function isBotAuthoredLogin(author) {
|
|
405
|
+
return author.trim().toLowerCase().endsWith('[bot]');
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* #993 FR-004 — newness anchor. Returns the newest `created_at` (ISO-8601
|
|
409
|
+
* lexicographic compare) among comments carrying a question marker.
|
|
410
|
+
* `undefined` when the issue has no question-marker comment.
|
|
411
|
+
*/
|
|
412
|
+
function latestQuestionCommentCreatedAt(comments) {
|
|
413
|
+
let latest;
|
|
414
|
+
for (const c of comments) {
|
|
415
|
+
if (matchClarificationQuestionMarker(c.body) === undefined)
|
|
416
|
+
continue;
|
|
417
|
+
if (latest === undefined || c.created_at > latest)
|
|
418
|
+
latest = c.created_at;
|
|
419
|
+
}
|
|
420
|
+
return latest;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Simple semaphore for bounded concurrency. Copy of the merge-conflict
|
|
424
|
+
* monitor's helper — deliberately duplicated to keep the two monitors
|
|
425
|
+
* shape-parallel and diffable.
|
|
426
|
+
*/
|
|
427
|
+
class Semaphore {
|
|
428
|
+
count;
|
|
429
|
+
waiting = [];
|
|
430
|
+
constructor(max) {
|
|
431
|
+
this.count = max;
|
|
432
|
+
}
|
|
433
|
+
async acquire() {
|
|
434
|
+
if (this.count > 0) {
|
|
435
|
+
this.count--;
|
|
436
|
+
return () => this.release();
|
|
437
|
+
}
|
|
438
|
+
return new Promise((resolve) => {
|
|
439
|
+
this.waiting.push(() => {
|
|
440
|
+
this.count--;
|
|
441
|
+
resolve(() => this.release());
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
release() {
|
|
446
|
+
this.count++;
|
|
447
|
+
const next = this.waiting.shift();
|
|
448
|
+
if (next)
|
|
449
|
+
next();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
//# sourceMappingURL=clarification-answer-monitor-service.js.map
|